bindings/python/wscript
author Gustavo J. A. M. Carneiro <gjc@inescporto.pt>
Fri, 29 Aug 2008 14:56:24 +0100
changeset 3567 728eb3f583b3
parent 3559 6c3efec9cbe6
child 3574 b6804efbe16b
permissions -rw-r--r--
Require new PyBindGen (now ignores the badly scanned anonymous containers that cause problems on cygwin) and rescan API.

## -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-

import re
import Params
import Configure
import Object
import Action
import os
import Task
import pproc as subprocess
from Params import fatal, warning
import shutil

## Adjust python path to look for our local copy of pybindgen
LOCAL_PYBINDGEN_PATH = os.path.join(os.getcwd(), "bindings", "python", "pybindgen")
#PYBINDGEN_BRANCH = 'lp:pybindgen'
PYBINDGEN_BRANCH = 'https://launchpad.net/pybindgen'
if os.environ.get('PYTHONPATH', ''):
    os.environ['PYTHONPATH'] = LOCAL_PYBINDGEN_PATH + os.pathsep + os.environ.get('PYTHONPATH')
else:
    os.environ['PYTHONPATH'] = LOCAL_PYBINDGEN_PATH

## https://launchpad.net/pybindgen/
REQUIRED_PYBINDGEN_VERSION = (0, 9, 0, 570)
REQUIRED_PYGCCXML_VERSION = (0, 9, 5)


def set_options(opt):
    opt.tool_options('python')
    opt.add_option('--python-disable',
                   help=("Don't build Python bindings."),
                   action="store_true", default=False,
                   dest='python_disable')
    opt.add_option('--python-scan',
                   help=("Rescan Python bindings.  Needs working GCCXML / pygccxml environment."),
                   action="store_true", default=False,
                   dest='python_scan')
    opt.add_option('--pybindgen-checkout',
                   help=("During configure, force checkout of pybingen inside ns-3, "
                         "instead of using the system installed version."),
                   action="store_true", default=False,
                   dest='pybindgen_checkout')

def fetch_pybindgen(conf):
    """
    Fetches pybindgen from launchpad as bindings/python/pybindgen.
    Returns True if successful, False it not.
    """
    bzr = conf.find_program("bzr")
    if not bzr:
        warning("the program 'bzr' is needed in order to fetch pybindgen")
        return False
    if len(REQUIRED_PYBINDGEN_VERSION) == 4:
        rev = "-rrevno:%i" % REQUIRED_PYBINDGEN_VERSION[3]
    else:
        rev = "-rtag:%s" % '.'.join([str(x) for x in REQUIRED_PYBINDGEN_VERSION])
    if os.path.exists(LOCAL_PYBINDGEN_PATH):
        print "Trying to update pybindgen; this will fail if no network connection is available."

        cmd = [bzr, "pull", rev, PYBINDGEN_BRANCH]
        print " => ", ' '.join(cmd)
        if subprocess.Popen(cmd, cwd=LOCAL_PYBINDGEN_PATH).wait():
            return False
        print "Update was successful."
    else:
        print "Trying to fetch pybindgen; this will fail if no network connection is available."
        cmd = [bzr, "checkout", rev, PYBINDGEN_BRANCH, LOCAL_PYBINDGEN_PATH]
        print " => ", ' '.join(cmd)
        if subprocess.Popen(cmd).wait():
            return False
        print "Fetch was successful."

    ## generate a fake version.py file in pybindgen it's safer this
    ## way, since the normal version generation process requires
    ## bazaar python bindings, which may not be available.
    vfile = open(os.path.join(LOCAL_PYBINDGEN_PATH, "pybindgen", "version.py"), "wt")
    vfile.write("""
# (fake version generated by ns-3)
__version__ = %r
""" % list(REQUIRED_PYBINDGEN_VERSION))
    vfile.close()

    return True


def configure(conf):
    conf.env['ENABLE_PYTHON_BINDINGS'] = False
    if Params.g_options.python_disable:
        return

    conf.check_tool('misc')

    ## Check for Python
    try:
        conf.check_tool('python')
        conf.check_python_version((2,3))
        conf.check_python_headers()
    except Configure.ConfigurationError:
        return

    ## Check for pybindgen
    if Params.g_options.pybindgen_checkout:
        fetch_pybindgen(conf)

    try:
        conf.check_python_module('pybindgen')
    except Configure.ConfigurationError:
        warning("pybindgen missing")
        if not fetch_pybindgen(conf):
            return
    else:
        out = subprocess.Popen([conf.env['PYTHON'], "-c",
                                "import pybindgen.version; "
                                "print '.'.join([str(x) for x in pybindgen.version.__version__])"],
                                stdout=subprocess.PIPE).communicate()[0]
        pybindgen_version_str = out.strip()
        pybindgen_version = tuple([int(x) for x in pybindgen_version_str.split('.')])
        conf.check_message('pybindgen', 'version',
                           (pybindgen_version >= REQUIRED_PYBINDGEN_VERSION),
                           pybindgen_version_str)
        if not (pybindgen_version >= REQUIRED_PYBINDGEN_VERSION):
            warning("pybindgen (found %s) is too old (need %s)" %
                    (pybindgen_version_str,
                     '.'.join([str(x) for x in REQUIRED_PYBINDGEN_VERSION])))
            if not fetch_pybindgen(conf):
                return

    ## If all has gone well, we finally enable the Python bindings
    conf.env['ENABLE_PYTHON_BINDINGS'] = True

    ## Check for pygccxml
    try:
        conf.check_python_module('pygccxml')
    except Configure.ConfigurationError:
        return

    out = subprocess.Popen([conf.env['PYTHON'], "-c",
                            "import pygccxml; print pygccxml.__version__"],
                            stdout=subprocess.PIPE).communicate()[0]
    pygccxml_version_str = out.strip()
    pygccxml_version = tuple([int(x) for x in pygccxml_version_str.split('.')])
    conf.check_message('pygccxml', 'version',
                       (pygccxml_version >= REQUIRED_PYGCCXML_VERSION),
                       pygccxml_version_str)
    if not (pygccxml_version >= REQUIRED_PYGCCXML_VERSION):
        warning("pygccxml (found %s) is too old (need %s) => "
                "automatic scanning of API definitions will not be possible" %
                (pygccxml_version_str,
                 '.'.join([str(x) for x in REQUIRED_PYGCCXML_VERSION])))
        return
    

    ## Check gccxml version
    gccxml = conf.find_program('gccxml', var='GCCXML')
    if not gccxml:
        warning("gccxml missing; automatic scanning of API definitions will not be possible")
        return

    gccxml_version_line = os.popen(gccxml + " --version").readline().strip()
    m = re.match( "^GCC-XML version (\d\.\d(\.\d)?)$", gccxml_version_line)
    gccxml_version = m.group(1)
    gccxml_version_ok = ([int(s) for s in gccxml_version.split('.')] >= [0, 9])
    conf.check_message('gccxml', 'version', True, gccxml_version)
    if not gccxml_version_ok:
        warning("gccxml too old, need version >= 0.9; automatic scanning of API definitions will not be possible")
        return
    
    ## If we reached
    conf.env['ENABLE_PYTHON_SCANNING'] = True


prio_headers = {
    -2: (
        "string.h", # work around http://www.gccxml.org/Bug/view.php?id=6682
        ),
    -1: (
        "propagation-delay-model.h",
        "propagation-loss-model.h",
        "net-device.h",
        )
     }

def get_header_prio(header):
    for prio, headers in prio_headers.iteritems():
        if header in headers:
            return prio
    return 1


def calc_header_include(path):
    (head, tail) = os.path.split (path)
    if tail == 'ns3':
        return ''
    else:
        return os.path.join (calc_header_include (head), tail)


def gen_ns3_metaheader(task):
    assert len(task.m_outputs) == 1

    header_files = [calc_header_include(node.abspath(task.m_env)) for node in task.m_inputs]
    outfile = file(task.m_outputs[0].bldpath(task.m_env), "w")

    def sort_func(h1, h2):
        return cmp((get_header_prio(h1), h1), (get_header_prio(h1), h2))

    header_files.sort(sort_func)

    for header in header_files:
        print >> outfile, "#include \"ns3/%s\"" % (header,)

    print >> outfile, """
namespace ns3 {
static inline Ptr<Object>
__dummy_function_to_force_template_instantiation (Ptr<Object> obj, TypeId typeId)
{
   return obj->GetObject<Object> (typeId);
}

}
"""
    outfile.close()
    return 0



class all_ns3_headers_taskgen(Object.task_gen):
    """Generates a 'everything.h' header file that includes some/all public ns3 headers.
    This single header file is to be parsed only once by gccxml, for greater efficiency.
    """
    def __init__(self, *features):
        Object.task_gen.__init__(self, *features)
        self.inst_var = 0#'INCLUDEDIR'
        #self.inst_dir = 'ns3'

    def apply(self):
        ## get all of the ns3 headers
        ns3_dir_node = Params.g_build.m_srcnode.find_dir("ns3")
        all_headers_inputs = []

        for filename in self.to_list(self.source):
            src_node = ns3_dir_node.find_build(filename)
            if src_node is None:
                Params.fatal("source ns3 header file %s not found" % (filename,))
            all_headers_inputs.append(src_node)

        ## if self.source was empty, include all ns3 headers in enabled modules
        if not all_headers_inputs:
            for ns3headers in Object.g_allobjs:
                if type(ns3headers).__name__ == 'ns3header_taskgen': # XXX: find less hackish way to compare
                    ## skip headers not part of enabled modules
                    if self.env['NS3_ENABLED_MODULES']:
                        if ("ns3-%s" % ns3headers.module) not in self.env['NS3_ENABLED_MODULES']:
                            continue

                    for source in ns3headers.to_list(ns3headers.source):
                        #source = os.path.basename(source)
                        node = ns3_dir_node.find_build(source)
                        if node is None:
                            fatal("missing header file %s" % (source,))
                        all_headers_inputs.append(node)
        assert all_headers_inputs
        all_headers_outputs = [self.path.find_build("everything.h")]
        task = self.create_task('gen-ns3-metaheader', self.env, 4)
        task.set_inputs(all_headers_inputs)
        task.set_outputs(all_headers_outputs)

    def install(self):
        pass


def get_modules_and_headers():
    """
    Gets a dict of
       module_name => ([module_dep1, module_dep2, ...], [module_header1, module_header2, ...])
    tuples, one for each module.
    """

    retval = {}
    for module in Object.g_allobjs:
        if not module.name.startswith('ns3-'):
            continue
        module_name = module.name[4:] # strip the ns3- prefix
        ## find the headers object for this module
        headers = []
        for ns3headers in Object.g_allobjs:
            if type(ns3headers).__name__ != 'ns3header_taskgen': # XXX: find less hackish way to compare
                continue
            if ns3headers.module != module_name:
                continue
            for source in ns3headers.to_list(ns3headers.source):
                headers.append(source)
        retval[module_name] = (list(module.module_deps), headers)
    return retval



class PythonScanTask(Task.TaskBase):
    """Uses gccxml to scan the file 'everything.h' and extract API definitions.
    """
    def __init__(self, curdirnode, env):
        self.m_display = 'python-scan'
        self.prio = 5 # everything.h has prio 4
        super(PythonScanTask, self).__init__()
        self.curdirnode = curdirnode
        self.env = env

    def run(self):
        #print "Rescanning the python bindings..."
        argv = [
            self.env['PYTHON'],
            os.path.join(self.curdirnode.abspath(), 'ns3modulescan.py'), # scanning script
            self.curdirnode.find_dir('../..').abspath(self.env), # include path (where the ns3 include dir is)
            self.curdirnode.find_build('everything.h').abspath(self.env),
            os.path.join(self.curdirnode.abspath(), 'ns3modulegen_generated.py'), # output file
            ]
        scan = subprocess.Popen(argv, stdin=subprocess.PIPE)
        scan.stdin.write(repr(get_modules_and_headers()))
        scan.stdin.close()
        if scan.wait():
            raise SystemExit(1)


def build(bld):
    if Params.g_options.python_disable:
        return

    env = bld.env_of_name('default')
    #Object.register('all-ns3-headers', AllNs3Headers)
    Action.Action('gen-ns3-metaheader', func=gen_ns3_metaheader, color='BLUE')

    if env['ENABLE_PYTHON_BINDINGS']:
        obj = bld.create_obj('all_ns3_headers')

    if Params.g_options.python_scan:
        if not env['ENABLE_PYTHON_SCANNING']:
            Params.fatal("Cannot re-scan python bindings: (py)gccxml not available")
        PythonScanTask(bld.m_curdirnode, env)

    ## Get a list of scanned modules; the set of scanned modules
    ## may be smaller than the set of all modules, in case a new
    ## ns3 module is being developed which wasn't scanned yet.
    scanned_modules = []
    for filename in os.listdir(bld.m_curdirnode.abspath()):
        m = re.match(r"^ns3_module_(.+)\.py$", filename)
        if m is None:
            continue
        scanned_modules.append(m.group(1))

    if env['ENABLE_PYTHON_BINDINGS']:
        bindgen = bld.create_obj('command-output')
        bindgen.name = 'pybindgen'
        bindgen.command = env['PYTHON']
        bindgen.command_is_external = True
        bindgen.stderr = 'ns3modulegen.log'
        bindgen.argv = [
            bindgen.input_file("ns3modulegen.py"),
            bindgen.output_file("ns3module.cc"),
            ]
        bindgen.argv.extend(get_modules_and_headers().iterkeys())
        bindgen.hidden_inputs = ['everything.h',
                                 'ns3modulegen_generated.py',
                                 'ns3modulegen_core_customizations.py']

        for module in scanned_modules:
            bindgen.hidden_inputs.append("ns3_module_%s.py" % module)

        bindgen.hidden_outputs = ['ns3module.h']
        for module in scanned_modules:
            bindgen.hidden_outputs.append("ns3_module_%s.cc" % module)

        bindgen.prio = 50

        bindgen.os_env = dict(os.environ)
        if not env['ENABLE_GTK_CONFIG_STORE']:
            bindgen.os_env['DISABLE_GTK_CONFIG_STORE'] = "1"


    ## we build python bindings if either we have the tools to
    ## generate them or if the pregenerated source file is already
    ## present in the source dir.
    if env['ENABLE_PYTHON_BINDINGS'] \
            or os.path.exists(os.path.join(bld.m_curdirnode.abspath(), 'ns3module.cc')):
        pymod = bld.create_obj('cpp', 'shlib', 'pyext')
        pymod.source = ['ns3module.cc', 'ns3module_helpers.cc']
        pymod.includes = '.'
        for module in scanned_modules:
            pymod.source.append("ns3_module_%s.cc" % module)
        pymod.target = 'ns3/_ns3'
        pymod.name = 'ns3module'
        pymod.uselib_local = "ns3"
        pymod.env.append_value('CXXDEFINES', ['NS_DEPRECATED=""', 'NS3_DEPRECATED_H'])

        # copy the __init__.py file to the build dir waf can't handle
        # this, it's against waf's principles to have build dir files
        # with the same name as source dir files, apparently.
        dirnode = bld.m_curdirnode.find_dir('ns3')
        src = os.path.join(dirnode.abspath(), '__init__.py')
        dst = os.path.join(dirnode.abspath(env), '__init__.py')
        try:
            need_copy = os.stat(src).st_mtime > os.stat(dst).st_mtime
        except OSError:
            need_copy = True
        if need_copy:
            try:
                os.mkdir(os.path.dirname(dst))
            except OSError:
                pass
            print "%r -> %r" % (src, dst)
            shutil.copy2(src, dst)