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.
578 lines
21 KiB
Python
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)
|