bindings/python/wscript
author Tom Henderson <tomh@tomh.org>
Fri, 20 Aug 2010 12:17:19 -0700
changeset 6589 9c325569fb01
parent 6574 a6007ea4f1cd
permissions -rw-r--r--
Help waf to guess release versions of nsc and pybindgen

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

import re
import os
import pproc as subprocess
import shutil
import sys

import Task
import Options
import Configure
import TaskGen
import Logs
import Build
import Utils

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



from TaskGen import feature, after
import Task, ccroot

@feature('pch')
@after('apply_link')
def process_pch(self):
    node = self.path.find_resource(self.pch)
    assert node
    tsk = self.create_task('gchx')
    tsk.set_inputs(node)
    tsk.set_outputs(node.parent.find_or_declare(node.name + '.gch'))

comp_line = '${CXX} ${CXXFLAGS} ${CPPFLAGS} ${_CXXINCFLAGS} ${_CXXDEFFLAGS} ${SRC} -o ${TGT}'
cls = Task.simple_task_type('gchx', comp_line, before='cc cxx')
cls.scan = ccroot.scan




def add_to_python_path(path):
    if os.environ.get('PYTHONPATH', ''):
        os.environ['PYTHONPATH'] = path + os.pathsep + os.environ.get('PYTHONPATH')
    else:
        os.environ['PYTHONPATH'] = path

def set_pybindgen_pythonpath(env):
    if env['WITH_PYBINDGEN']:
        add_to_python_path(env['WITH_PYBINDGEN'])


def set_options(opt):
    opt.tool_options('python')
    opt.add_option('--disable-python',
                   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('--with-pybindgen',
                   help=('Path to an existing pybindgen source tree to use.'),
                   default=None,
                   dest='with_pybindgen', type="string")
    opt.add_option('--enable-python-pch',
                   help=("Enable precompiled headers when compiling python bindings, to speed up compilation."),
                   action="store_true", default=False,
                   dest='enable_python_pch')


def configure(conf):
    conf.env['ENABLE_PYTHON_PCH'] = Options.options.enable_python_pch
    conf.env['ENABLE_PYTHON_BINDINGS'] = False
    if Options.options.python_disable:
        conf.report_optional_feature("python", "Python Bindings", False,
                                     "disabled by user request")
        return

    conf.check_tool('misc')

    if sys.platform == 'cygwin':
        conf.report_optional_feature("python", "Python Bindings", False,
                                     "unsupported platform 'cygwin'")
        Logs.warn("Python is not supported in CygWin environment.  Try MingW instead.")
        return

    ## Check for Python
    try:
        conf.check_tool('python')
        conf.check_python_version((2,3))
        conf.check_python_headers()
    except Configure.ConfigurationError, ex:
        conf.report_optional_feature("python", "Python Bindings", False, str(ex))
        return

    # -fvisibility=hidden optimization
    if (conf.env['CXX_NAME'] == 'gcc' and [int(x) for x in conf.env['CC_VERSION']] >= [4,0,0]
        and conf.check_compilation_flag('-fvisibility=hidden')):
        conf.env.append_value('CXXFLAGS_PYEXT', '-fvisibility=hidden')
        conf.env.append_value('CCFLAGS_PYEXT', '-fvisibility=hidden')

    # Check for the location of pybindgen
    if Options.options.with_pybindgen is not None:
        if os.path.isdir(Options.options.with_pybindgen):
            conf.check_message("pybindgen location", '', True, ("%s (given)" % Options.options.with_pybindgen))
            conf.env['WITH_PYBINDGEN'] = os.path.abspath(Options.options.with_pybindgen)
    else:
        # ns-3-dev uses ../pybindgen, while ns-3 releases use ../REQUIRED_PYBINDGEN_VERSION
        pybindgen_dir = os.path.join('..', "pybindgen")
        pybindgen_release_str = "pybindgen-" + '.'.join([str(x) for x in REQUIRED_PYBINDGEN_VERSION])
        pybindgen_release_dir = os.path.join('..', pybindgen_release_str)
        if os.path.isdir(pybindgen_dir):
            conf.check_message("pybindgen location", '', True, ("%s (guessed)" % pybindgen_dir))
            conf.env['WITH_PYBINDGEN'] = os.path.abspath(pybindgen_dir)
        elif os.path.isdir(pybindgen_release_dir):
            conf.check_message("pybindgen location", '', True, ("%s (guessed)" % pybindgen_release_dir))
            conf.env['WITH_PYBINDGEN'] = os.path.abspath(pybindgen_release_dir)
        del pybindgen_dir
        del pybindgen_release_dir
    if not conf.env['WITH_PYBINDGEN']:
        conf.check_message("pybindgen location", '', False)

    # Check for pybindgen

    set_pybindgen_pythonpath(conf.env)

    try:
        conf.check_python_module('pybindgen')
    except Configure.ConfigurationError:
        Logs.warn("pybindgen missing => no python bindings")
        conf.report_optional_feature("python", "Python Bindings", False,
                                     "PyBindGen missing")
        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):
            Logs.warn("pybindgen (found %s), (need %s)" %
                    (pybindgen_version_str,
                     '.'.join([str(x) for x in REQUIRED_PYBINDGEN_VERSION])))
            conf.report_optional_feature("python", "Python Bindings", False,
                                         "PyBindGen version not correct and newer version could not be retrieved")
            return


    def test(t1, t2):
        test_program = '''
#include <stdint.h>
#include <vector>

int main ()
{
   std::vector< %(type1)s > t = std::vector< %(type2)s > ();
   return 0;
}
''' % dict(type1=t1, type2=t2)

        try:
            ret = conf.run_c_code(code=test_program,
                                  env=conf.env.copy(), compile_filename='test.cc',
                                  compile_mode='cxx',type='cprogram', execute=False)
        except Configure.ConfigurationError:
            ret = 1
        conf.check_message_custom('types %s and %s' % (t1, t2), 'equivalency', (ret and 'no' or 'yes'))
        return not ret

    uint64_is_long = test("uint64_t", "unsigned long")
    uint64_is_long_long = test("uint64_t", "unsigned long long")

    if uint64_is_long:
        conf.env['PYTHON_BINDINGS_APIDEFS'] = 'gcc-LP64'
    elif uint64_is_long_long:
        conf.env['PYTHON_BINDINGS_APIDEFS'] = 'gcc-ILP32'
    else:
        conf.env['PYTHON_BINDINGS_APIDEFS'] = None
    if conf.env['PYTHON_BINDINGS_APIDEFS'] is None:
        msg = 'none available'
    else:
        msg = conf.env['PYTHON_BINDINGS_APIDEFS']

    conf.check_message_custom('the apidefs that can be used for Python bindings', '', msg)

    if conf.env['PYTHON_BINDINGS_APIDEFS'] is None:
        conf.report_optional_feature("python", "Python Bindings", False,
                                     "No apidefs are available that can be used in this system")
        return

    ## If all has gone well, we finally enable the Python bindings
    conf.env['ENABLE_PYTHON_BINDINGS'] = True
    conf.report_optional_feature("python", "Python Bindings", True, None)

    

    ## Check for pygccxml
    try:
        conf.check_python_module('pygccxml')
    except Configure.ConfigurationError:
        conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
                                     "Missing 'pygccxml' Python module")
        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):
        Logs.warn("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])))
        conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
                                     "pygccxml too old")
        return
    

    ## Check gccxml version
    gccxml = conf.find_program('gccxml', var='GCCXML')
    if not gccxml:
        Logs.warn("gccxml missing; automatic scanning of API definitions will not be possible")
        conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
                                     "gccxml missing")
        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:
        Logs.warn("gccxml too old, need version >= 0.9; automatic scanning of API definitions will not be possible")
        conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
                                     "gccxml too old")
        return
    
    ## If we reached
    conf.env['ENABLE_PYTHON_SCANNING'] = True
    conf.report_optional_feature("pygccxml", "Python API Scanning Support", True, None)


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",
        "ipv4-interface.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)


class gen_everything_h_task(Task.Task):
    before = 'cc cxx gchx'
    after = 'ns3header_task'
    color = 'BLUE'

    def run(self):
        assert len(self.outputs) == 1

        header_files = [calc_header_include(node.abspath(self.env)) for node in self.inputs]
        outfile = file(self.outputs[0].bldpath(self.env), "w")

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

        header_files.sort(sort_func)

        print >> outfile, """

/* http://www.nsnam.org/bugzilla/show_bug.cgi?id=413 */
#ifdef ECHO
# undef ECHO
#endif

    """
        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);
}


static inline void
__dummy_function_to_force_template_instantiation_v2 ()
{
   Time t1, t2, t3;
   t1 = t2 + t3;
   t1 = t2 - t3;
   TimeSquare tsq = t2*t3;
   Time tsqdiv = tsq/Seconds(1);
   Scalar scal = t2/t3;
   TimeInvert inv = scal/t3;
   t1 = scal*t1;
   t1 = t1/scal;
   t1 < t2;
   t1 <= t2;
   t1 == t2;
   t1 != t2;
   t1 >= t2;
   t1 > t2;
}


}
"""
        outfile.close()
        return 0



class all_ns3_headers_taskgen(TaskGen.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, *args, **kwargs):
        super(all_ns3_headers_taskgen, self).__init__(*args, **kwargs)
        self.install_path = None
        #self.inst_dir = 'ns3'

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

        for filename in self.to_list(self.source):
            src_node = ns3_dir_node.find_or_declare(filename)
            if src_node is None:
                raise Utils.WafError("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 self.bld.all_task_gen:
                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_or_declare(os.path.basename(source))
                        if node is None:
                            raise Utils.WafError("missing header file %s" % (source,))
                        all_headers_inputs.append(node)
        assert all_headers_inputs
        all_headers_outputs = [self.path.find_or_declare("everything.h")]
        task = self.create_task('gen_everything_h', env=self.env)
        task.set_inputs(all_headers_inputs)
        task.set_outputs(all_headers_outputs)

    def install(self):
        pass


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

    retval = {}
    for module in bld.all_task_gen:
        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 bld.all_task_gen:
            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 python_scan_task(Task.TaskBase):
    """Uses gccxml to scan the file 'everything.h' and extract API definitions.
    """
    after = 'gen_everything_h_task'
    before = 'cc cxx gchx'
    color = "BLUE"
    def __init__(self, curdirnode, env, bld, target, cflags):
        self.bld = bld
        super(python_scan_task, self).__init__(generator=self)
        self.curdirnode = curdirnode
        self.env = env
        self.target = target
        self.cflags = cflags

    def display(self):
        return 'python-scan-%s\n' % (self.target,)

    def run(self):
        defsdir = os.path.join(self.curdirnode.abspath(), 'apidefs', self.target)
        try:
            os.mkdir(defsdir)
        except OSError:
            pass
        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_or_declare('everything.h').abspath(self.env),
            os.path.join(defsdir, 'ns3modulegen_generated.py'), # output file
            self.cflags,
            ]
        scan = subprocess.Popen(argv, stdin=subprocess.PIPE)
        print >> scan.stdin, repr(get_modules_and_headers(self.bld))
        scan.stdin.close()
        retval = scan.wait()
        return retval

class python_scan_task_collector(Task.TaskBase):
    """Tasks that waits for the python-scan-* tasks to complete and then signals WAF to exit
    """
    after = 'python_scan_task'
    before = 'cc cxx'
    color = "BLUE"
    def __init__(self, curdirnode, env, bld):
        self.bld = bld
        super(python_scan_task_collector, self).__init__(generator=self)
        self.curdirnode = curdirnode
        self.env = env

    def display(self):
        return 'python-scan-collector\n'

    def run(self):
        # signal stop (we generated files into the source dir and WAF
        # can't cope with it, so we have to force the user to restart
        # WAF)
        self.bld.generator.stop = 1
        return 0


def build(bld):
    if Options.options.python_disable:
        return

    env = bld.env
    curdir = bld.path.abspath()

    set_pybindgen_pythonpath(env)

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

    if Options.options.python_scan:
        if not env['ENABLE_PYTHON_SCANNING']:
            raise Utils.WafError("Cannot re-scan python bindings: (py)gccxml not available")
        scan_targets = []
        if sys.platform == 'cygwin':
            scan_targets.append(('gcc-cygwin', ''))
        else:
            import struct
            if struct.calcsize('I') == 4 and struct.calcsize('L') == 8 and struct.calcsize('P') == 8:
                scan_targets.extend([('gcc-ILP32', '-m32'), ('gcc-LP64', '-m64')])
            elif struct.calcsize('I') == 4 and struct.calcsize('L') == 4 and struct.calcsize('P') == 4:
                scan_targets.append(('gcc-ILP32', ''))
            else:
                raise Utils.WafError("Cannot scan python bindings for unsupported data model")
        for target, cflags in scan_targets:
            python_scan_task(bld.path, env, bld, target, cflags)
        python_scan_task_collector(bld.path, env, bld)
        return

    if env['ENABLE_PYTHON_BINDINGS']:
        apidefs = env['PYTHON_BINDINGS_APIDEFS']

        ## 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(os.path.join(curdir, 'apidefs', apidefs)):
            m = re.match(r"^ns3_module_(.+)\.py$", filename)
            if m is None:
                continue
            name = m.group(1)
            if name.endswith("__local"):
                continue
            scanned_modules.append(name)

        debug = ('PYBINDGEN_DEBUG' in os.environ)
        source = [
            'ns3modulegen.py',
            'apidefs/%s/ns3modulegen_generated.py' % (apidefs,),
            'ns3modulegen_core_customizations.py',
            ]
        target = [
            'ns3module.cc',
            'pch/ns3module.h',
            ]
        if not debug:
            target.append('ns3modulegen.log')

        argv = ['NS3_ENABLED_FEATURES=${FEATURES}', '${PYTHON}']
        if debug:
            argv.extend(["-m", "pdb"])
        argv.extend(['${SRC[0]}', '${TGT[0]}', os.path.join(curdir, 'apidefs', apidefs)])
        
        argv.extend(get_modules_and_headers(bld).iterkeys())
        for module in scanned_modules:
            source.append("apidefs/%s/ns3_module_%s.py" % (apidefs, module))
            local = "ns3_module_%s__local.py" % module
            if os.path.exists(os.path.join(curdir, local)):
                source.append(local)

        if not debug:
            argv.extend(['2>', '${TGT[2]}']) # 2> ns3modulegen.log

        for module in scanned_modules:
            target.append("ns3_module_%s.cc" % module)

        features = []
        for (name, caption, was_enabled, reason_not_enabled) in env['NS3_OPTIONAL_FEATURES']:
            if was_enabled:
                features.append(name)

        bindgen = bld.new_task_gen('command', source=source, target=target, command=argv)
        bindgen.env['FEATURES'] = ','.join(features)
        bindgen.dep_vars = ['FEATURES']
        bindgen.before = 'cxx gchx'
        bindgen.after = 'gen_everything_h_task'
        bindgen.name = "pybindgen-command"
 
        features = 'cxx cshlib pyext'
        if env['ENABLE_PYTHON_PCH']:
            features += ' pch'
        pymod = bld.new_task_gen(features=features)
        pymod.source = ['ns3module.cc', 'ns3module_helpers.cc']
        pymod.includes = '. pch'
        if env['ENABLE_PYTHON_PCH']:
            pymod.pch = 'pch/ns3module.h'
        for module in scanned_modules:
            pymod.source.append("ns3_module_%s.cc" % module)
        pymod.target = 'ns3/_ns3'
        pymod.name = 'ns3module'
        pymod.uselib_local = "ns3"
        if pymod.env['ENABLE_STATIC_NS3']:
            if sys.platform == 'darwin':
                pymod.env.append_value('LINKFLAGS', '-Wl,-all_load')
                pymod.env.append_value('LINKFLAGS', '-lns3')
            else:
                pymod.env.append_value('LINKFLAGS', '-Wl,--whole-archive,-Bstatic')
                pymod.env.append_value('LINKFLAGS', '-lns3')
                pymod.env.append_value('LINKFLAGS', '-Wl,-Bdynamic,--no-whole-archive')

        defines = list(pymod.env['CXXDEFINES'])
        defines.extend(['NS_DEPRECATED=', 'NS3_DEPRECATED_H'])
        if Options.platform == 'win32':
            try:
                defines.remove('_DEBUG') # causes undefined symbols on win32
            except ValueError:
                pass
        pymod.env['CXXDEFINES'] = defines

        # 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.path.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)