## -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*- from __future__ import print_function import types import re import os import subprocess import shutil import sys from waflib import Task, Options, Configure, TaskGen, Logs, Build, Utils, Errors from waflib.Errors import WafError # feature = TaskGen.feature # after = TaskGen.after # https://github.com/gjcarneiro/pybindgen REQUIRED_PYBINDGEN_VERSION = '0.20.0' REQUIRED_PYGCCXML_VERSION = (1, 9, 1) REQUIRED_CASTXML_VERSION = '0.2' RUN_ME=-3 # return types of some APIs differ in Python 2/3 (type string vs class bytes) # This method will decode('utf-8') a byte object in Python 3, # and do nothing in Python 2 def maybe_decode(input): if sys.version_info < (3,): return input else: try: return input.decode('utf-8') except: sys.exc_clear() return input 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 options(opt): opt.load('python') opt.add_option('--disable-python', help=("Don't build Python bindings."), action="store_true", default=False, dest='python_disable') opt.add_option('--apiscan', help=("Rescan the API for the indicated module(s), for Python bindings. " "Needs working CastXML / pygccxml environment. " "The metamodule 'all' expands to all available ns-3 modules."), default=None, dest='apiscan', metavar="MODULE[,MODULE...]") 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('--with-python', help=('Path to the Python interpreter to use.'), default=None, dest='with_python', type="string") def split_version(version): if (re.search ('post', version)): # Version format such as '0.17.0.post58+ngcf00cc0' ver = re.split('[.+]', version)[:4] return (int(ver[0]), int(ver[1]), int(ver[2]), int(ver[3].split('post')[1])) else: # Version format such as '0.18.0' ver = re.split('[.]', version)[:3] return (int(ver[0]), int(ver[1]), int(ver[2]), 0) def configure(conf): conf.env['ENABLE_PYTHON_BINDINGS'] = False if Options.options.python_disable: conf.report_optional_feature("python", "Python Bindings", False, "disabled by user request") return # Disable python in static builds (bug #1253) if ((conf.env['ENABLE_STATIC_NS3']) or \ (conf.env['ENABLE_SHARED_AND_STATIC_NS3'])): conf.report_optional_feature("python", "Python Bindings", False, "bindings incompatible with static build") return enabled_modules = list(conf.env['NS3_ENABLED_MODULES']) enabled_modules.sort() available_modules = list(conf.env['NS3_MODULES']) available_modules.sort() all_modules_enabled = (enabled_modules == available_modules) conf.load('misc', tooldir=['waf-tools']) 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 if Options.options.with_python is not None: conf.env.PYTHON = Options.options.with_python try: conf.load('python') except Errors.ConfigurationError as ex: conf.report_optional_feature("python", "Python Bindings", False, "The python interpreter was not found") return try: conf.check_python_version((2,3)) except Errors.ConfigurationError as ex: conf.report_optional_feature("python", "Python Bindings", False, "The python found version is too low (2.3 required)") return try: conf.check_python_headers() except Errors.ConfigurationError as ex: conf.report_optional_feature("python", "Python Bindings", False, "Python library or headers missing") return # stupid Mac OSX Python wants to build extensions as "universal # binaries", i386, x86_64, and ppc, but this way the type # __uint128_t is not available. We need to disable the multiarch # crap by removing the -arch parameters. for flags_var in ["CFLAGS_PYEXT", "CFLAGS_PYEMBED", "CXXFLAGS_PYEMBED", "CXXFLAGS_PYEXT", "LINKFLAGS_PYEMBED", "LINKFLAGS_PYEXT"]: flags = conf.env[flags_var] i = 0 while i < len(flags): if flags[i] == '-arch': del flags[i] del flags[i] continue i += 1 conf.env[flags_var] = flags # -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') if conf.check_compilation_flag('-Wno-array-bounds'): conf.env.append_value('CXXFLAGS_PYEXT', '-Wno-array-bounds') # Check for the location of pybindgen if Options.options.with_pybindgen is not None: if os.path.isdir(Options.options.with_pybindgen): conf.msg("Checking for pybindgen location", ("%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-" + REQUIRED_PYBINDGEN_VERSION pybindgen_release_dir = os.path.join('..', pybindgen_release_str) if os.path.isdir(pybindgen_dir): conf.msg("Checking for pybindgen location", ("%s (guessed)" % pybindgen_dir)) conf.env['WITH_PYBINDGEN'] = os.path.abspath(pybindgen_dir) elif os.path.isdir(pybindgen_release_dir): conf.msg("Checking for pybindgen location", ("%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.msg("Checking for pybindgen location", False) # Check for pybindgen set_pybindgen_pythonpath(conf.env) try: conf.check_python_module('pybindgen') except Errors.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'][0], "-c", "import pybindgen.version; " "print(pybindgen.__version__)"], stdout=subprocess.PIPE).communicate()[0] pybindgen_version = maybe_decode(out.strip()) conf.msg('Checking for pybindgen version', pybindgen_version) if not pybindgen_version: Logs.warn("pybindgen_version is an empty string") conf.report_optional_feature("python", "Python Bindings", False, "PyBindGen version not found") return if not (split_version(pybindgen_version) >= split_version(REQUIRED_PYBINDGEN_VERSION)): Logs.warn("pybindgen (found %r), (need %r)" % (pybindgen_version, 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 #include int main () { std::vector< %(type1)s > t = std::vector< %(type2)s > (); return 0; } ''' % dict(type1=t1, type2=t2) try: ret = conf.check(compiler='cxx', fragment=test_program, features='cxx') except Errors.ConfigurationError: ret = False conf.msg('Checking for types %s and %s equivalence' % (t1, t2), (ret and 'no' or 'yes')) return 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.msg('Checking for 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 cxxabi stuff (which Mac OS X Lion breaks) fragment = r""" # include int main () { const abi::__si_class_type_info *_typeinfo __attribute__((unused)) = NULL; return 0; } """ gcc_rtti_abi = conf.check_nonfatal(fragment=fragment, msg="Checking for internal GCC cxxabi", okmsg="complete", errmsg='incomplete', mandatory=False) conf.env["GCC_RTTI_ABI_COMPLETE"] = str(bool(gcc_rtti_abi)) ## Check for pygccxml try: conf.check_python_module('pygccxml') except Errors.ConfigurationError: conf.report_optional_feature("castxml", "Python API Scanning Support", False, "Missing 'pygccxml' Python module") return try: import pygccxml as pygccxml_imported pygccxml_version_str = pygccxml_imported.__version__ except (ImportError, AttributeError): Logs.warn("pygccxml version cannot be determined") conf.report_optional_feature("castxml", "Python API Scanning Support", False, "pygccxml Python module version is unknown") return # Bug 2013: pygccxml versions > 1.0.0 prepend a 'v' to version number pygccxml_version_str = pygccxml_version_str.lstrip('v') pygccxml_version = tuple([int(x) for x in pygccxml_version_str.split('.')]) conf.msg('Checking for 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("castxml", "Python API Scanning Support", False, "pygccxml Python module too old") return ## Check castxml version try: castxml = conf.find_program('castxml', var='CASTXML') except WafError: castxml = None if not castxml: Logs.warn("castxml missing; automatic scanning of API definitions will not be possible") conf.report_optional_feature("castxml", "Python API Scanning Support", False, "castxml missing") return out = subprocess.Popen([castxml[0], '--version'], stdout=subprocess.PIPE).communicate()[0] castxml_version_line = maybe_decode(out).split('\n', 1)[0].strip() ## Expecting version string such as 'castxml version 0.1-gfab9c47' m = re.match( "^castxml version (\d\.\d)(-)?(\w+)?", castxml_version_line) try: castxml_version = m.group(1) castxml_version_ok = castxml_version >= REQUIRED_CASTXML_VERSION except AttributeError: castxml_version = castxml_version_line castxml_version_ok = False conf.msg('Checking for castxml version', castxml_version) if not castxml_version_ok: Logs.warn("castxml version unknown or too old, need version >= %s; automatic scanning of API definitions will not be possible" % REQUIRED_CASTXML_VERSION) conf.report_optional_feature("castxml", "Python API Scanning Support", False, "castxml too old") return ## If we reached conf.env['ENABLE_PYTHON_SCANNING'] = True conf.report_optional_feature("castxml", "Python API Scanning Support", True, None) # --------------------- def get_headers_map(bld): headers_map = {} # header => module for ns3headers in bld.all_task_gen: if 'ns3header' in getattr(ns3headers, "features", []): if ns3headers.module.endswith('-test'): continue for h in ns3headers.to_list(ns3headers.headers): headers_map[os.path.basename(h)] = ns3headers.module return headers_map def get_module_path(bld, module): for ns3headers in bld.all_task_gen: if 'ns3header' in getattr(ns3headers, "features", []): if ns3headers.module == module: break else: raise ValueError("Module %r not found" % module) return ns3headers.path.abspath() class apiscan_task(Task.Task): """Uses castxml to scan the file 'everything.h' and extract API definitions. """ before = ['cxxprogram', 'cxxshlib', 'cxxstlib', 'command'] after = ['gen_ns3_module_header', 'ns3header'] color = "BLUE" def __init__(self, curdirnode, env, bld, target, cflags, module): self.bld = bld super(apiscan_task, self).__init__(generator=self, env=env) self.curdirnode = curdirnode self.env = env self.target = target self.cflags = cflags self.module = module def display(self): return 'api-scan-%s\n' % (self.target,) def uid(self): try: return self.uid_ except AttributeError: m = Utils.md5() up = m.update up(self.__class__.__name__.encode()) up(self.curdirnode.abspath().encode()) up(self.target.encode()) self.uid_ = m.digest() return self.uid_ def run(self): self.inputs = [self.bld.bldnode.find_resource("ns3/{0}-module.h".format(self.module))] self.outputs = [self.bld.srcnode.find_resource("src/{}/bindings/modulegen__{}.py".format(self.module, self.target))] top_builddir = self.bld.bldnode.abspath() module_path = get_module_path(self.bld, self.module) headers_map = get_headers_map(self.bld) scan_header = os.path.join(top_builddir, "ns3", "%s-module.h" % self.module) if not os.path.exists(scan_header): Logs.error("Cannot apiscan module %r: %s does not exist" % (self.module, scan_header)) return 0 argv = [ self.env['PYTHON'][0], os.path.join(self.curdirnode.abspath(), 'ns3modulescan-modular.py'), # scanning script top_builddir, self.module, repr(get_headers_map(self.bld)), os.path.join(module_path, "bindings", 'modulegen__%s.py' % (self.target)), # output file self.cflags, ] scan = subprocess.Popen(argv, stdin=subprocess.PIPE) retval = scan.wait() if retval >= 0 and "LP64" in self.target: self.lp64_to_ilp32( os.path.join(module_path, "bindings", 'modulegen__%s.py' % (self.target)), os.path.join(module_path, "bindings", 'modulegen__%s.py' % "gcc_ILP32") ) return retval def runnable_status(self): # By default, Waf Task will skip running a task if the signature of # the build has not changed. We want this task to always run if # invoked by the user, particularly since --apiscan=all will require # invoking this task many times, once per module. return RUN_ME def lp64_to_ilp32(self, lp64path, ilp32path): lp64file = open(lp64path, "r") lp64bindings = lp64file.read() lp64file.close() ilp32file = open(ilp32path, "w") ilp32bindings = re.sub("unsigned long(?!( long))", "unsigned long long", lp64bindings) ilp32file.write(ilp32bindings) ilp32file.close() 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 if module.name.endswith('-test'): 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 'ns3header' not in getattr(ns3headers, "features", []): continue if ns3headers.module != module_name: continue for source in ns3headers.to_list(ns3headers.headers): headers.append(os.path.basename(source)) retval[module_name] = (list(module.module_deps), headers) return retval class gen_ns3_compat_pymod_task(Task.Task): """Generates a 'ns3.py' compatibility module.""" before = ['cxxprogram', 'cxxshlib', 'cxxstlib'] color = 'BLUE' def run(self): assert len(self.outputs) == 1 outfile = open(self.outputs[0].abspath(), "w") print("import warnings", file=outfile) print('warnings.warn("the ns3 module is a compatibility layer '\ 'and should not be used in newly written code", DeprecationWarning, stacklevel=2)', file=outfile) print(file=outfile) for module in self.bld.env['PYTHON_MODULES_BUILT']: print("from ns.%s import *" % (module.replace('-', '_')), file=outfile) outfile.close() return 0 def build(bld): if Options.options.python_disable: return env = bld.env set_pybindgen_pythonpath(env) if Options.options.apiscan: if not env['ENABLE_PYTHON_SCANNING']: raise 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.append(('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 WafError("Cannot scan python bindings for unsupported data model") test_module_path = bld.path.find_dir("../../src/test") if Options.options.apiscan == 'all': scan_modules = [] for mod in bld.all_task_gen: if not mod.name.startswith('ns3-'): continue if mod.path.is_child_of(test_module_path): continue if mod.name.endswith('-test'): continue bindings_enabled = (mod.name in env.MODULAR_BINDINGS_MODULES) #print mod.name, bindings_enabled if bindings_enabled: scan_modules.append(mod.name.split('ns3-')[1]) else: scan_modules = Options.options.apiscan.split(',') print("Modules to scan: ", scan_modules) for target, cflags in scan_targets: group = bld.get_group(bld.current_group) for module in scan_modules: group.append(apiscan_task(bld.path, env, bld, target, cflags, module)) return if env['ENABLE_PYTHON_BINDINGS']: task = gen_ns3_compat_pymod_task(env=env.derive()) task.set_outputs(bld.path.find_or_declare("ns3.py")) task.dep_vars = ['PYTHON_MODULES_BUILT'] task.bld = bld grp = bld.get_group(bld.current_group) grp.append(task) bld(features='copy', source="ns__init__.py", target='ns/__init__.py') bld.install_as('${PYTHONARCHDIR}/ns/__init__.py', 'ns__init__.py') # note: the actual build commands for the python bindings are in # src/wscript, not here.