## -*- 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
import sys
## 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'
## https://launchpad.net/pybindgen/
REQUIRED_PYBINDGEN_VERSION = (0, 9, 0, 605)
REQUIRED_PYGCCXML_VERSION = (0, 9, 5)
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'])
else:
add_to_python_path(LOCAL_PYBINDGEN_PATH)
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('--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')
opt.add_option('--with-pybindgen',
help=('Path to an existing pybindgen source tree to use.'),
default=None,
dest='with_pybindgen', type="string")
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. Hit Ctrl-C to skip."
cmd = [bzr, "pull", rev, PYBINDGEN_BRANCH]
print " => ", ' '.join(cmd)
try:
if subprocess.Popen(cmd, cwd=LOCAL_PYBINDGEN_PATH).wait():
return False
except KeyboardInterrupt:
print "Interrupted; Python bindings will be disabled."
return False
print "Update was successful."
else:
print "Trying to fetch pybindgen; this will fail if no network connection is available. Hit Ctrl-C to skip."
cmd = [bzr, "checkout", rev, PYBINDGEN_BRANCH, LOCAL_PYBINDGEN_PATH]
print " => ", ' '.join(cmd)
try:
if subprocess.Popen(cmd).wait():
return False
except KeyboardInterrupt:
print "Interrupted; Python bindings will be disabled."
shutil.rmtree(LOCAL_PYBINDGEN_PATH, True)
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:
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'")
warning("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
# Fix a bug with WAF and g++ 4.3.2 (it does not include "(GCC") in
# the output of g++ --version, so the WAF python detection fails
# to recognize it is gcc)
gcc_version = os.popen("%s --version" % conf.env['CXX']).readline()
if '(GCC)' in gcc_version or 'g++' in gcc_version:
conf.env.append_value('CXXFLAGS_PYEMBED','-fno-strict-aliasing')
conf.env.append_value('CXXFLAGS_PYEXT','-fno-strict-aliasing')
## Check for pybindgen
no_net = False
if Params.g_options.with_pybindgen:
conf.env['WITH_PYBINDGEN'] = os.path.abspath(Params.g_options.with_pybindgen)
no_net = True
if Params.g_options.pybindgen_checkout:
fetch_pybindgen(conf)
set_pybindgen_pythonpath(conf.env)
try:
conf.check_python_module('pybindgen')
except Configure.ConfigurationError:
warning("pybindgen missing")
if no_net or not fetch_pybindgen(conf):
conf.report_optional_feature("python", "Python Bindings", False,
"PyBindGen missing and could not be retrieved")
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 no_net or not fetch_pybindgen(conf):
conf.report_optional_feature("python", "Python Bindings", False,
"PyBindGen too old and newer version could not be retrieved")
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):
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])))
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:
warning("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:
warning("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",
)
}
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)
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(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)
raise SystemExit(0)
def build(bld):
if Params.g_options.python_disable:
return
env = bld.env_of_name('default')
curdir = bld.m_curdirnode.abspath()
set_pybindgen_pythonpath(env)
#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(curdir):
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)
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 = [
#'-m', 'pdb',
bindgen.input_file("ns3modulegen.py"),
bindgen.output_file("ns3module.cc"),
]
bindgen.argv.extend(get_modules_and_headers().iterkeys())
bindgen.hidden_inputs = ['ns3modulegen_generated.py',
'ns3modulegen_core_customizations.py']
for module in scanned_modules:
bindgen.hidden_inputs.append("ns3_module_%s.py" % module)
local = "ns3_module_%s__local.py" % module
if os.path.exists(os.path.join(curdir, local)):
bindgen.hidden_inputs.append(local)
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)
features = []
for (name, caption, was_enabled, reason_not_enabled) in env['NS3_OPTIONAL_FEATURES']:
if was_enabled:
features.append(name)
bindgen.os_env['NS3_ENABLED_FEATURES'] = ','.join(features)
## 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)