## -*- 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, 8, 0, 516) 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 __dummy_function_to_force_template_instantiation (Ptr obj, TypeId typeId) { return obj->GetObject (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 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') 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") print "Rescanning the python bindings..." curdir = bld.m_curdirnode.abspath() argv = [ env['PYTHON'], os.path.join(curdir, 'ns3modulescan.py'), # scanning script bld.m_curdirnode.find_dir('../..').abspath(env), # include path (where the ns3 include dir is) bld.m_curdirnode.find_build('everything.h').abspath(env), os.path.join(curdir, '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) print "Rescanning the python bindings done." raise SystemExit ## 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)