bindings/python/wscript
author Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
Sat, 04 Jul 2009 08:15:48 +0200
changeset 4654 2eaebe77d66b
parent 4459 d65af64db144
permissions -rw-r--r--
Added tag ns-3.5 for changeset c975274c9707
     1 ## -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
     2 
     3 import re
     4 import os
     5 import pproc as subprocess
     6 import shutil
     7 import sys
     8 
     9 import Task
    10 import Options
    11 import Configure
    12 import TaskGen
    13 import Logs
    14 import Build
    15 import Utils
    16 
    17 ## https://launchpad.net/pybindgen/
    18 REQUIRED_PYBINDGEN_VERSION = (0, 10, 0, 640)
    19 REQUIRED_PYGCCXML_VERSION = (0, 9, 5)
    20 
    21 
    22 def add_to_python_path(path):
    23     if os.environ.get('PYTHONPATH', ''):
    24         os.environ['PYTHONPATH'] = path + os.pathsep + os.environ.get('PYTHONPATH')
    25     else:
    26         os.environ['PYTHONPATH'] = path
    27 
    28 def set_pybindgen_pythonpath(env):
    29     if env['WITH_PYBINDGEN']:
    30         add_to_python_path(env['WITH_PYBINDGEN'])
    31 
    32 
    33 def set_options(opt):
    34     opt.tool_options('python')
    35     opt.add_option('--disable-python',
    36                    help=("Don't build Python bindings."),
    37                    action="store_true", default=False,
    38                    dest='python_disable')
    39     opt.add_option('--python-scan',
    40                    help=("Rescan Python bindings.  Needs working GCCXML / pygccxml environment."),
    41                    action="store_true", default=False,
    42                    dest='python_scan')
    43     opt.add_option('--with-pybindgen',
    44                    help=('Path to an existing pybindgen source tree to use.'),
    45                    default=None,
    46                    dest='with_pybindgen', type="string")
    47 
    48 
    49 def configure(conf):
    50     conf.env['ENABLE_PYTHON_BINDINGS'] = False
    51     if Options.options.python_disable:
    52         conf.report_optional_feature("python", "Python Bindings", False,
    53                                      "disabled by user request")
    54         return
    55 
    56     conf.check_tool('misc')
    57 
    58     if sys.platform == 'cygwin':
    59         conf.report_optional_feature("python", "Python Bindings", False,
    60                                      "unsupported platform 'cygwin'")
    61         Logs.warn("Python is not supported in CygWin environment.  Try MingW instead.")
    62         return
    63 
    64     ## Check for Python
    65     try:
    66         conf.check_tool('python')
    67         conf.check_python_version((2,3))
    68         conf.check_python_headers()
    69     except Configure.ConfigurationError, ex:
    70         conf.report_optional_feature("python", "Python Bindings", False, str(ex))
    71         return
    72 
    73     # -fvisibility=hidden optimization
    74     if (conf.env['CXX_NAME'] == 'gcc' and [int(x) for x in conf.env['CC_VERSION']] >= [4,0,0]
    75         and conf.check_compilation_flag('-fvisibility=hidden')):
    76         conf.env.append_value('CXXFLAGS_PYEXT', '-fvisibility=hidden')
    77         conf.env.append_value('CCFLAGS_PYEXT', '-fvisibility=hidden')
    78 
    79     # Check for the location of pybindgen
    80     if Options.options.with_pybindgen is not None:
    81         if os.path.isdir(Options.options.with_pybindgen):
    82             conf.check_message("pybindgen location", '', True, ("%s (given)" % Options.options.with_pybindgen))
    83             conf.env['WITH_PYBINDGEN'] = os.path.abspath(Options.options.with_pybindgen)
    84     else:
    85         pybindgen_dir = os.path.join('..', "pybindgen")
    86         if os.path.isdir(pybindgen_dir):
    87             conf.check_message("pybindgen location", '', True, ("%s (guessed)" % pybindgen_dir))
    88             conf.env['WITH_PYBINDGEN'] = os.path.abspath(pybindgen_dir)
    89         del pybindgen_dir
    90     if not conf.env['WITH_PYBINDGEN']:
    91         conf.check_message("pybindgen location", '', False)
    92 
    93     # Check for pybindgen
    94 
    95     set_pybindgen_pythonpath(conf.env)
    96 
    97     try:
    98         conf.check_python_module('pybindgen')
    99     except Configure.ConfigurationError:
   100         Logs.warn("pybindgen missing => no python bindings")
   101         conf.report_optional_feature("python", "Python Bindings", False,
   102                                      "PyBindGen missing")
   103         return
   104     else:
   105         out = subprocess.Popen([conf.env['PYTHON'], "-c",
   106                                 "import pybindgen.version; "
   107                                 "print '.'.join([str(x) for x in pybindgen.version.__version__])"],
   108                                 stdout=subprocess.PIPE).communicate()[0]
   109         pybindgen_version_str = out.strip()
   110         pybindgen_version = tuple([int(x) for x in pybindgen_version_str.split('.')])
   111         conf.check_message('pybindgen', 'version',
   112                            (pybindgen_version == REQUIRED_PYBINDGEN_VERSION),
   113                            pybindgen_version_str)
   114         if not (pybindgen_version == REQUIRED_PYBINDGEN_VERSION):
   115             Logs.warn("pybindgen (found %s), (need %s)" %
   116                     (pybindgen_version_str,
   117                      '.'.join([str(x) for x in REQUIRED_PYBINDGEN_VERSION])))
   118             conf.report_optional_feature("python", "Python Bindings", False,
   119                                          "PyBindGen version not correct and newer version could not be retrieved")
   120             return
   121 
   122     ## If all has gone well, we finally enable the Python bindings
   123     conf.env['ENABLE_PYTHON_BINDINGS'] = True
   124     conf.report_optional_feature("python", "Python Bindings", True, None)
   125 
   126     ## Check for pygccxml
   127     try:
   128         conf.check_python_module('pygccxml')
   129     except Configure.ConfigurationError:
   130         conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
   131                                      "Missing 'pygccxml' Python module")
   132         return
   133 
   134     out = subprocess.Popen([conf.env['PYTHON'], "-c",
   135                             "import pygccxml; print pygccxml.__version__"],
   136                             stdout=subprocess.PIPE).communicate()[0]
   137     pygccxml_version_str = out.strip()
   138     pygccxml_version = tuple([int(x) for x in pygccxml_version_str.split('.')])
   139     conf.check_message('pygccxml', 'version',
   140                        (pygccxml_version >= REQUIRED_PYGCCXML_VERSION),
   141                        pygccxml_version_str)
   142     if not (pygccxml_version >= REQUIRED_PYGCCXML_VERSION):
   143         Logs.warn("pygccxml (found %s) is too old (need %s) => "
   144                 "automatic scanning of API definitions will not be possible" %
   145                 (pygccxml_version_str,
   146                  '.'.join([str(x) for x in REQUIRED_PYGCCXML_VERSION])))
   147         conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
   148                                      "pygccxml too old")
   149         return
   150     
   151 
   152     ## Check gccxml version
   153     gccxml = conf.find_program('gccxml', var='GCCXML')
   154     if not gccxml:
   155         Logs.warn("gccxml missing; automatic scanning of API definitions will not be possible")
   156         conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
   157                                      "gccxml missing")
   158         return
   159 
   160     gccxml_version_line = os.popen(gccxml + " --version").readline().strip()
   161     m = re.match( "^GCC-XML version (\d\.\d(\.\d)?)$", gccxml_version_line)
   162     gccxml_version = m.group(1)
   163     gccxml_version_ok = ([int(s) for s in gccxml_version.split('.')] >= [0, 9])
   164     conf.check_message('gccxml', 'version', True, gccxml_version)
   165     if not gccxml_version_ok:
   166         Logs.warn("gccxml too old, need version >= 0.9; automatic scanning of API definitions will not be possible")
   167         conf.report_optional_feature("pygccxml", "Python API Scanning Support", False,
   168                                      "gccxml too old")
   169         return
   170     
   171     ## If we reached
   172     conf.env['ENABLE_PYTHON_SCANNING'] = True
   173     conf.report_optional_feature("pygccxml", "Python API Scanning Support", True, None)
   174 
   175 
   176 prio_headers = {
   177     -2: (
   178         "string.h", # work around http://www.gccxml.org/Bug/view.php?id=6682
   179         ),
   180     -1: (
   181         "propagation-delay-model.h",
   182         "propagation-loss-model.h",
   183         "net-device.h",
   184         )
   185      }
   186 
   187 def get_header_prio(header):
   188     for prio, headers in prio_headers.iteritems():
   189         if header in headers:
   190             return prio
   191     return 1
   192 
   193 
   194 def calc_header_include(path):
   195     (head, tail) = os.path.split (path)
   196     if tail == 'ns3':
   197         return ''
   198     else:
   199         return os.path.join (calc_header_include (head), tail)
   200 
   201 
   202 class gen_everything_h_task(Task.Task):
   203     before = 'cc cxx'
   204     after = 'ns3header_task'
   205     color = 'BLUE'
   206 
   207     def run(self):
   208         assert len(self.outputs) == 1
   209 
   210         header_files = [calc_header_include(node.abspath(self.env)) for node in self.inputs]
   211         outfile = file(self.outputs[0].bldpath(self.env), "w")
   212 
   213         def sort_func(h1, h2):
   214             return cmp((get_header_prio(h1), h1), (get_header_prio(h1), h2))
   215 
   216         header_files.sort(sort_func)
   217 
   218         print >> outfile, """
   219 
   220 /* http://www.nsnam.org/bugzilla/show_bug.cgi?id=413 */
   221 #ifdef ECHO
   222 # undef ECHO
   223 #endif
   224 
   225     """
   226         for header in header_files:
   227             print >> outfile, "#include \"ns3/%s\"" % (header,)
   228 
   229         print >> outfile, """
   230 namespace ns3 {
   231 static inline Ptr<Object>
   232 __dummy_function_to_force_template_instantiation (Ptr<Object> obj, TypeId typeId)
   233 {
   234    return obj->GetObject<Object> (typeId);
   235 }
   236 
   237 
   238 static inline void
   239 __dummy_function_to_force_template_instantiation_v2 ()
   240 {
   241    Time t1, t2, t3;
   242    t1 = t2 + t3;
   243    t1 = t2 - t3;
   244    TimeSquare tsq = t2*t3;
   245    Time tsqdiv = tsq/Seconds(1);
   246    Scalar scal = t2/t3;
   247    TimeInvert inv = scal/t3;
   248    t1 = scal*t1;
   249    t1 = t1/scal;
   250    t1 < t2;
   251    t1 <= t2;
   252    t1 == t2;
   253    t1 != t2;
   254    t1 >= t2;
   255    t1 > t2;
   256 }
   257 
   258 
   259 }
   260 """
   261         outfile.close()
   262         return 0
   263 
   264 
   265 
   266 class all_ns3_headers_taskgen(TaskGen.task_gen):
   267     """Generates a 'everything.h' header file that includes some/all public ns3 headers.
   268     This single header file is to be parsed only once by gccxml, for greater efficiency.
   269     """
   270     def __init__(self, *args, **kwargs):
   271         super(all_ns3_headers_taskgen, self).__init__(*args, **kwargs)
   272         self.install_path = None
   273         #self.inst_dir = 'ns3'
   274 
   275     def apply(self):
   276         ## get all of the ns3 headers
   277         ns3_dir_node = self.bld.path.find_dir("ns3")
   278         all_headers_inputs = []
   279 
   280         for filename in self.to_list(self.source):
   281             src_node = ns3_dir_node.find_or_declare(filename)
   282             if src_node is None:
   283                 raise Utils.WafError("source ns3 header file %s not found" % (filename,))
   284             all_headers_inputs.append(src_node)
   285 
   286         ## if self.source was empty, include all ns3 headers in enabled modules
   287         if not all_headers_inputs:
   288             for ns3headers in self.bld.all_task_gen:
   289                 if type(ns3headers).__name__ == 'ns3header_taskgen': # XXX: find less hackish way to compare
   290                     ## skip headers not part of enabled modules
   291                     if self.env['NS3_ENABLED_MODULES']:
   292                         if ("ns3-%s" % ns3headers.module) not in self.env['NS3_ENABLED_MODULES']:
   293                             continue
   294 
   295                     for source in ns3headers.to_list(ns3headers.source):
   296                         #source = os.path.basename(source)
   297                         node = ns3_dir_node.find_or_declare(source)
   298                         if node is None:
   299                             raise Utils.WafError("missing header file %s" % (source,))
   300                         all_headers_inputs.append(node)
   301         assert all_headers_inputs
   302         all_headers_outputs = [self.path.find_or_declare("everything.h")]
   303         task = self.create_task('gen_everything_h', self.env)
   304         task.set_inputs(all_headers_inputs)
   305         task.set_outputs(all_headers_outputs)
   306 
   307     def install(self):
   308         pass
   309 
   310 
   311 def get_modules_and_headers(bld):
   312     """
   313     Gets a dict of
   314        module_name => ([module_dep1, module_dep2, ...], [module_header1, module_header2, ...])
   315     tuples, one for each module.
   316     """
   317 
   318     retval = {}
   319     for module in bld.all_task_gen:
   320         if not module.name.startswith('ns3-'):
   321             continue
   322         module_name = module.name[4:] # strip the ns3- prefix
   323         ## find the headers object for this module
   324         headers = []
   325         for ns3headers in bld.all_task_gen:
   326             if type(ns3headers).__name__ != 'ns3header_taskgen': # XXX: find less hackish way to compare
   327                 continue
   328             if ns3headers.module != module_name:
   329                 continue
   330             for source in ns3headers.to_list(ns3headers.source):
   331                 headers.append(source)
   332         retval[module_name] = (list(module.module_deps), headers)
   333     return retval
   334 
   335 
   336 
   337 class python_scan_task(Task.TaskBase):
   338     """Uses gccxml to scan the file 'everything.h' and extract API definitions.
   339     """
   340     after = 'gen_everything_h_task'
   341     before = 'cc cxx'
   342     def __init__(self, curdirnode, env, bld):
   343         self.bld = bld
   344         super(python_scan_task, self).__init__(generator=self)
   345         self.curdirnode = curdirnode
   346         self.env = env
   347 
   348     def display(self):
   349         return 'python-scan\n'
   350 
   351     def run(self):
   352         #print "Rescanning the python bindings..."
   353         argv = [
   354             self.env['PYTHON'],
   355             os.path.join(self.curdirnode.abspath(), 'ns3modulescan.py'), # scanning script
   356             self.curdirnode.find_dir('../..').abspath(self.env), # include path (where the ns3 include dir is)
   357             self.curdirnode.find_or_declare('everything.h').abspath(self.env),
   358             os.path.join(self.curdirnode.abspath(), 'ns3modulegen_generated.py'), # output file
   359             ]
   360         scan = subprocess.Popen(argv, stdin=subprocess.PIPE)
   361         scan.stdin.write(repr(get_modules_and_headers(self.bld)))
   362         scan.stdin.close()
   363         retval = scan.wait()
   364         print "Scan finished with exit code", retval
   365         if retval:
   366             return retval
   367         # signal stop (we generated files into the source dir and WAF
   368         # can't cope with it, so we have to force the user to restart
   369         # WAF)
   370         self.bld.generator.stop = 1
   371         return 0
   372 
   373 
   374 def build(bld):
   375     if Options.options.python_disable:
   376         return
   377 
   378     env = bld.env
   379     curdir = bld.path.abspath()
   380 
   381     set_pybindgen_pythonpath(env)
   382 
   383     if env['ENABLE_PYTHON_BINDINGS']:
   384         obj = bld.new_task_gen('all_ns3_headers')
   385 
   386     if Options.options.python_scan:
   387         if not env['ENABLE_PYTHON_SCANNING']:
   388             raise Utils.WafError("Cannot re-scan python bindings: (py)gccxml not available")
   389         python_scan_task(bld.path, env, bld)
   390         return
   391 
   392     ## Get a list of scanned modules; the set of scanned modules
   393     ## may be smaller than the set of all modules, in case a new
   394     ## ns3 module is being developed which wasn't scanned yet.
   395     scanned_modules = []
   396     for filename in os.listdir(curdir):
   397         m = re.match(r"^ns3_module_(.+)\.py$", filename)
   398         if m is None:
   399             continue
   400         name = m.group(1)
   401         if name.endswith("__local"):
   402             continue
   403         scanned_modules.append(name)
   404 
   405     if env['ENABLE_PYTHON_BINDINGS']:
   406         source = [
   407             'ns3modulegen.py',
   408             'ns3modulegen_generated.py',
   409             'ns3modulegen_core_customizations.py',
   410             ]
   411         target = [
   412             'ns3module.cc',
   413             'ns3module.h',
   414             'ns3modulegen.log',
   415             ]
   416         argv = ['NS3_ENABLED_FEATURES=${FEATURES}', '${PYTHON}', '${SRC[0]}', '${TGT[0]}']
   417         argv.extend(get_modules_and_headers(bld).iterkeys())
   418         for module in scanned_modules:
   419             source.append("ns3_module_%s.py" % module)
   420             local = "ns3_module_%s__local.py" % module
   421             if os.path.exists(os.path.join(curdir, local)):
   422                 source.append(local)
   423 
   424         argv.extend(['2>', '${TGT[2]}']) # 2> ns3modulegen.log
   425 
   426         for module in scanned_modules:
   427             target.append("ns3_module_%s.cc" % module)
   428 
   429         features = []
   430         for (name, caption, was_enabled, reason_not_enabled) in env['NS3_OPTIONAL_FEATURES']:
   431             if was_enabled:
   432                 features.append(name)
   433 
   434         bindgen = bld.new_task_gen('command', source=source, target=target, command=argv)
   435         bindgen.env['FEATURES'] = ','.join(features)
   436         bindgen.dep_vars = ['FEATURES']
   437         bindgen.before = 'cxx'
   438         bindgen.after = 'gen_everything_h_task'
   439         bindgen.name = "pybindgen-command"
   440 
   441         pymod = bld.new_task_gen('cxx', 'shlib', 'pyext')
   442         if sys.platform == 'cygwin':
   443             pymod.features.append('implib') # workaround for WAF bug #472
   444         pymod.source = ['ns3module.cc', 'ns3module_helpers.cc']
   445         pymod.includes = '.'
   446         for module in scanned_modules:
   447             pymod.source.append("ns3_module_%s.cc" % module)
   448         pymod.target = 'ns3/_ns3'
   449         pymod.name = 'ns3module'
   450         pymod.uselib_local = "ns3"
   451         if pymod.env['ENABLE_STATIC_NS3']:
   452             if sys.platform == 'darwin':
   453                 pymod.env.append_value('LINKFLAGS', '-Wl,-all_load')
   454                 pymod.env.append_value('LINKFLAGS', '-lns3')
   455             else:
   456                 pymod.env.append_value('LINKFLAGS', '-Wl,--whole-archive,-Bstatic')
   457                 pymod.env.append_value('LINKFLAGS', '-lns3')
   458                 pymod.env.append_value('LINKFLAGS', '-Wl,-Bdynamic,--no-whole-archive')
   459 
   460         defines = list(pymod.env['CXXDEFINES'])
   461         defines.extend(['NS_DEPRECATED=', 'NS3_DEPRECATED_H'])
   462         if Options.platform == 'win32':
   463             try:
   464                 defines.remove('_DEBUG') # causes undefined symbols on win32
   465             except ValueError:
   466                 pass
   467         pymod.env['CXXDEFINES'] = defines
   468 
   469         # copy the __init__.py file to the build dir. waf can't handle
   470         # this, it's against waf's principles to have build dir files
   471         # with the same name as source dir files, apparently.
   472         dirnode = bld.path.find_dir('ns3')
   473         src = os.path.join(dirnode.abspath(), '__init__.py')
   474         dst = os.path.join(dirnode.abspath(env), '__init__.py')
   475         try:
   476             need_copy = os.stat(src).st_mtime > os.stat(dst).st_mtime
   477         except OSError:
   478             need_copy = True
   479         if need_copy:
   480             try:
   481                 os.mkdir(os.path.dirname(dst))
   482             except OSError:
   483                 pass
   484             print "%r -> %r" % (src, dst)
   485             shutil.copy2(src, dst)