Files
unison/bindings/python/wscript
Gustavo J. A. M. Carneiro ecc1df19d5 Realign Python ns3.Object constructors with C++
The Python constructors are now parameter oriented instead of attribute oriented.
The main motivation for this change is that Python bindings become broken with new Object types being introduced that do not have a default constructor (constructor without parameters).  Another reason is to shorten the gap between C++ and Python, API-wise--the more similar they are, the less confusing it becomes.
2010-03-17 11:18:55 +00:00

578 lines
21 KiB
Python

## -*- 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, 14, 0, 755)
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'])
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")
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
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:
pybindgen_dir = os.path.join('..', "pybindgen")
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)
del pybindgen_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'
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(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'
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)
scan.stdin.write(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',
'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'
bindgen.after = 'gen_everything_h_task'
bindgen.name = "pybindgen-command"
pymod = bld.new_task_gen('cxx', 'shlib', 'pyext')
if sys.platform == 'cygwin':
pymod.features.append('implib') # workaround for WAF bug #472
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"
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)