From aaf2cbb74e00fdc89da432d18e9fe40bb7de3b9d Mon Sep 17 00:00:00 2001 From: Steven Knight Date: Tue, 6 May 2003 05:58:31 +0000 Subject: Refactor to use real Nodes for command-line attributes and eliminate PathList. (Charles Crain) --- doc/man/scons.1 | 12 +- src/CHANGES.txt | 3 + src/RELEASE.txt | 4 + src/engine/SCons/Action.py | 36 +--- src/engine/SCons/ActionTests.py | 99 +++++---- src/engine/SCons/Builder.py | 2 +- src/engine/SCons/BuilderTests.py | 18 +- src/engine/SCons/Environment.py | 53 +---- src/engine/SCons/EnvironmentTests.py | 76 ++----- src/engine/SCons/Node/FS.py | 95 +++++++-- src/engine/SCons/Node/FSTests.py | 70 ++++++- src/engine/SCons/Node/NodeTests.py | 40 ++++ src/engine/SCons/Node/__init__.py | 43 +++- src/engine/SCons/Platform/win32.py | 5 +- src/engine/SCons/Script/SConscript.py | 6 +- src/engine/SCons/Tool/Perforce.py | 2 +- src/engine/SCons/Tool/jar.py | 8 +- src/engine/SCons/Tool/javac.py | 82 ++++---- src/engine/SCons/Tool/linkloc.py | 4 +- src/engine/SCons/Tool/mingw.py | 4 +- src/engine/SCons/Tool/mslink.py | 59 +++--- src/engine/SCons/Tool/zip.py | 2 +- src/engine/SCons/Util.py | 366 ++++++++++++++++------------------ src/engine/SCons/UtilTests.py | 254 ++++++++++++++--------- test/CacheDir.py | 2 +- test/scan-once.py | 2 +- 26 files changed, 776 insertions(+), 571 deletions(-) diff --git a/doc/man/scons.1 b/doc/man/scons.1 index 14a2e50..e6f72b9 100644 --- a/doc/man/scons.1 +++ b/doc/man/scons.1 @@ -4205,7 +4205,7 @@ takes four arguments: .I target - a list of target nodes, .I env -- the construction environment. +- the construction environment, .I for_signature - a Boolean value that specifies whether the generator is being called @@ -4559,19 +4559,23 @@ may be a callable Python function associated with a construction variable in the environment. The function should -take three arguments: +take four arguments: .I target - a list of target nodes, .I source - a list of source nodes, .I env -- the construction environment. +- the construction environment, +.I for_signature +- a Boolean value that specifies +whether the function is being called +for generating a build signature. SCons will insert whatever the called function returns into the expanded string: .ES -def foo(target, source, env): +def foo(target, source, env, for_signature): return "bar" # Will expand $BAR to "bar baz" diff --git a/src/CHANGES.txt b/src/CHANGES.txt index 227ef59..6c6bcfb 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -52,6 +52,9 @@ RELEASE 0.14 - XXX - Pass Nodes, not strings, to Builder emitter functions. + - Refactor command-line interpolation and signature calculation + so we can use real Node attributes. + From Steven Knight: - Add support for Java (javac and jar). diff --git a/src/RELEASE.txt b/src/RELEASE.txt index e8d4b37..9cad9f6 100644 --- a/src/RELEASE.txt +++ b/src/RELEASE.txt @@ -51,6 +51,10 @@ RELEASE 0.14 - XXX SetOption('num_jobs', num) GetOption('num_jobs') + - Callable expansions of construction variables in a command line + now take a fourth "for_signature" argument that is set when the + expansion is being called to generate a build signature. + Please note the following important changes since release 0.11: - The default behavior of SCons is now to change to the directory in diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index d7be127..befac1e 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -171,9 +171,6 @@ def _string_from_cmd_list(cmd_list): cl.append(arg) return string.join(cl) -_rm = re.compile(r'\$[()]') -_remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)') - class CommandAction(ActionBase): """Class for command-execution actions.""" def __init__(self, cmd): @@ -183,7 +180,8 @@ class CommandAction(ActionBase): self.cmd_list = cmd def strfunction(self, target, source, env): - cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm, + cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, + SCons.Util.SUBST_CMD, target, source) return map(_string_from_cmd_list, cmd_list) @@ -228,7 +226,8 @@ class CommandAction(ActionBase): else: raise SCons.Errors.UserError('Missing SPAWN construction variable.') - cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm, + cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, + SCons.Util.SUBST_CMD, target, source) for cmd_line in cmd_list: if len(cmd_line): @@ -259,21 +258,12 @@ class CommandAction(ActionBase): def get_raw_contents(self, target, source, env): """Return the complete contents of this action's command line. """ - # We've discusssed using the real target and source names in - # a CommandAction's signature contents. This would have the - # advantage of recompiling when a file's name changes (keeping - # debug info current), but it would currently break repository - # logic that will change the file name based on whether the - # files come from a repository or locally. If we ever move to - # that scheme, though, here's how we'd do it: - #return SCons.Util.scons_subst(string.join(self.cmd_list), - # self.subst_dict(target, source, env), - # {}) cmd = self.cmd_list if not SCons.Util.is_List(cmd): cmd = [ cmd ] return SCons.Util.scons_subst(string.join(map(str, cmd)), - env) + env, SCons.Util.SUBST_RAW, + target, source) def get_contents(self, target, source, env): """Return the signature contents of this action's command line. @@ -281,23 +271,13 @@ class CommandAction(ActionBase): This strips $(-$) and everything in between the string, since those parts don't affect signatures. """ - # We've discusssed using the real target and source names in - # a CommandAction's signature contents. This would have the - # advantage of recompiling when a file's name changes (keeping - # debug info current), but it would currently break repository - # logic that will change the file name based on whether the - # files come from a repository or locally. If we ever move to - # that scheme, though, here's how we'd do it: - #return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)), - # self.subst_dict(target, source, env), - # {}, - # _remove) cmd = self.cmd_list if not SCons.Util.is_List(cmd): cmd = [ cmd ] return SCons.Util.scons_subst(string.join(map(str, cmd)), env, - _remove) + SCons.Util.SUBST_SIG, + target, source) class CommandGeneratorAction(ActionBase): """Class for command-generator actions.""" diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index 4aee748..33bf57e 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -127,6 +127,14 @@ class Environment: d['SOURCE'] = d['SOURCES'][0] return d +class DummyNode: + def __init__(self, name): + self.name = name + def __str__(self): + return self.name + def rfile(self): + return self + if os.name == 'java': python = os.path.join(sys.prefix, 'jython') else: @@ -334,20 +342,23 @@ class CommandActionTestCase(unittest.TestCase): def test_strfunction(self): """Test fetching the string representation of command Actions """ + act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE') s = act.strfunction([], [], Environment()) assert s == ['xyzzy'], s - s = act.strfunction(['target'], ['source'], Environment()) + s = act.strfunction([DummyNode('target')], [DummyNode('source')], Environment()) assert s == ['xyzzy target source'], s - s = act.strfunction(['t1', 't2'], ['s1', 's2'], Environment()) + s = act.strfunction([DummyNode('t1'), DummyNode('t2')], + [DummyNode('s1'), DummyNode('s2')], Environment()) assert s == ['xyzzy t1 s1'], s act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES') s = act.strfunction([], [], Environment()) assert s == ['xyzzy'], s - s = act.strfunction(['target'], ['source'], Environment()) + s = act.strfunction([DummyNode('target')], [DummyNode('source')], Environment()) assert s == ['xyzzy target source'], s - s = act.strfunction(['t1', 't2'], ['s1', 's2'], Environment()) + s = act.strfunction([DummyNode('t1'), DummyNode('t2')], + [DummyNode('s1'), DummyNode('s2')], Environment()) assert s == ['xyzzy t1 t2 s1 s2'], s act = SCons.Action.CommandAction(['xyzzy', @@ -355,9 +366,10 @@ class CommandActionTestCase(unittest.TestCase): '$TARGETS', '$SOURCES']) s = act.strfunction([], [], Environment()) assert s == ['xyzzy'], s - s = act.strfunction(['target'], ['source'], Environment()) + s = act.strfunction([DummyNode('target')], [DummyNode('source')], Environment()) assert s == ['xyzzy target source target source'], s - s = act.strfunction(['t1', 't2'], ['s1', 's2'], Environment()) + s = act.strfunction([DummyNode('t1'), DummyNode('t2')], + [DummyNode('s1'), DummyNode('s2')], Environment()) assert s == ['xyzzy t1 s1 t1 t2 s1 s2'], s def test_execute(self): @@ -396,7 +408,7 @@ class CommandActionTestCase(unittest.TestCase): cmd4 = r'%s %s %s $SOURCES' % (python, act_py, outfile) act = SCons.Action.CommandAction(cmd4) - r = act([], ['one', 'two'], env.Copy()) + r = act([], [DummyNode('one'), DummyNode('two')], env.Copy()) assert r == 0 c = test.read(outfile, 'r') assert c == "act.py: 'one' 'two'\n", c @@ -405,8 +417,10 @@ class CommandActionTestCase(unittest.TestCase): act = SCons.Action.CommandAction(cmd4) r = act([], - source = ['three', 'four', 'five'], - env = env.Copy()) + source = [DummyNode('three'), + DummyNode('four'), + DummyNode('five')], + env = env.Copy()) assert r == 0 c = test.read(outfile, 'r') assert c == "act.py: 'three' 'four'\n", c @@ -426,7 +440,7 @@ class CommandActionTestCase(unittest.TestCase): r = act(target = 'out5', source = [], env = env5) act = SCons.Action.CommandAction(cmd5) - r = act(target = 'out5', + r = act(target = DummyNode('out5'), source = [], env = env.Copy(ENV = {'XYZZY' : 'xyzzy', 'PATH' : PATH})) @@ -439,6 +453,8 @@ class CommandActionTestCase(unittest.TestCase): self._str = str def __str__(self): return self._str + def rfile(self): + return self cmd6 = r'%s %s %s ${TARGETS[1]} $TARGET ${SOURCES[:2]}' % (python, act_py, outfile) @@ -559,8 +575,8 @@ class CommandActionTestCase(unittest.TestCase): def test_get_raw_contents(self): """Test fetching the contents of a command Action """ - def CmdGen(target, source, env): - assert target is None, target + def CmdGen(target, source, env, for_signature): + assert for_signature return "%s %s" % \ (env["foo"], env["bar"]) @@ -581,47 +597,47 @@ class CommandActionTestCase(unittest.TestCase): # that scheme, then all of the '__t1__' and '__s6__' file names # in the asserts below would change to 't1' and 's6' and the # like. - t = ['t1', 't2', 't3', 't4', 't5', 't6'] - s = ['s1', 's2', 's3', 's4', 's5', 's6'] + t = map(DummyNode, ['t1', 't2', 't3', 't4', 't5', 't6']) + s = map(DummyNode, ['s1', 's2', 's3', 's4', 's5', 's6']) env = Environment() a = SCons.Action.CommandAction(["$TARGET"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__t1__", c + assert c == "t1", c a = SCons.Action.CommandAction(["$TARGETS"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__t1__ __t2__ __t3__ __t4__ __t5__ __t6__", c + assert c == "t1 t2 t3 t4 t5 t6", c a = SCons.Action.CommandAction(["${TARGETS[2]}"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__t3__", c + assert c == "t3", c a = SCons.Action.CommandAction(["${TARGETS[3:5]}"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__t4__ __t5__", c + assert c == "t4 t5", c a = SCons.Action.CommandAction(["$SOURCE"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__s1__", c + assert c == "s1", c a = SCons.Action.CommandAction(["$SOURCES"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__s1__ __s2__ __s3__ __s4__ __s5__ __s6__", c + assert c == "s1 s2 s3 s4 s5 s6", c a = SCons.Action.CommandAction(["${SOURCES[2]}"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__s3__", c + assert c == "s3", c a = SCons.Action.CommandAction(["${SOURCES[3:5]}"]) c = a.get_raw_contents(target=t, source=s, env=env) - assert c == "__s4__ __s5__", c + assert c == "s4 s5", c def test_get_contents(self): """Test fetching the contents of a command Action """ - def CmdGen(target, source, env): - assert target is None, target + def CmdGen(target, source, env, for_signature): + assert for_signature return "%s %s" % \ (env["foo"], env["bar"]) @@ -642,41 +658,41 @@ class CommandActionTestCase(unittest.TestCase): # that scheme, then all of the '__t1__' and '__s6__' file names # in the asserts below would change to 't1' and 's6' and the # like. - t = ['t1', 't2', 't3', 't4', 't5', 't6'] - s = ['s1', 's2', 's3', 's4', 's5', 's6'] + t = map(DummyNode, ['t1', 't2', 't3', 't4', 't5', 't6']) + s = map(DummyNode, ['s1', 's2', 's3', 's4', 's5', 's6']) env = Environment() a = SCons.Action.CommandAction(["$TARGET"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__t1__", c + assert c == "t1", c a = SCons.Action.CommandAction(["$TARGETS"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__t1__ __t2__ __t3__ __t4__ __t5__ __t6__", c + assert c == "t1 t2 t3 t4 t5 t6", c a = SCons.Action.CommandAction(["${TARGETS[2]}"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__t3__", c + assert c == "t3", c a = SCons.Action.CommandAction(["${TARGETS[3:5]}"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__t4__ __t5__", c + assert c == "t4 t5", c a = SCons.Action.CommandAction(["$SOURCE"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__s1__", c + assert c == "s1", c a = SCons.Action.CommandAction(["$SOURCES"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__s1__ __s2__ __s3__ __s4__ __s5__ __s6__", c + assert c == "s1 s2 s3 s4 s5 s6", c a = SCons.Action.CommandAction(["${SOURCES[2]}"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__s3__", c + assert c == "s3", c a = SCons.Action.CommandAction(["${SOURCES[3:5]}"]) c = a.get_contents(target=t, source=s, env=env) - assert c == "__s4__ __s5__", c + assert c == "s4 s5", c class CommandGeneratorActionTestCase(unittest.TestCase): @@ -732,6 +748,7 @@ class CommandGeneratorActionTestCase(unittest.TestCase): self.t = t def rfile(self): self.t.rfile_called = 1 + return self def f3(target, source, env, for_signature): return '' c = SCons.Action.CommandGeneratorAction(f3) @@ -745,12 +762,18 @@ class CommandGeneratorActionTestCase(unittest.TestCase): foo = env['foo'] bar = env['bar'] assert for_signature, for_signature - return [["guux", foo, "$(", "ignore", "$)", bar]] + return [["guux", foo, "$(", "$ignore", "$)", bar, + '${test("$( foo $bar $)")}' ]] + + def test(mystr): + assert mystr == "$( foo $bar $)", mystr + return "test" a = SCons.Action.CommandGeneratorAction(f) c = a.get_contents(target=[], source=[], - env=Environment(foo = 'FFF', bar = 'BBB')) - assert c == "guux FFF BBB", c + env=Environment(foo = 'FFF', bar = 'BBB', + ignore = 'foo', test=test)) + assert c == "guux FFF BBB test", c class FunctionActionTestCase(unittest.TestCase): @@ -972,7 +995,7 @@ class LazyActionTestCase(unittest.TestCase): """ a = SCons.Action.Action("${FOO}") c = a.get_contents(target=[], source=[], - env = Environment(FOO = [["This", "is", "$(", "a", "$)", "test"]])) + env = Environment(FOO = [["This", "is", "$(", "$a", "$)", "test"]])) assert c == "This is test", c diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py index 19cbe27..8b0ac85 100644 --- a/src/engine/SCons/Builder.py +++ b/src/engine/SCons/Builder.py @@ -462,7 +462,7 @@ class MultiStepBuilder(BuilderBase): src_suffixes = self.src_suffixes(env) for snode in slist: - path, ext = SCons.Util.splitext(snode.abspath) + path, ext = SCons.Util.splitext(snode.get_abspath()) if sdict.has_key(ext): src_bld = sdict[ext] tgt = apply(src_bld, (env, path, snode), kw) diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py index c71f18f..5f9a18c 100644 --- a/src/engine/SCons/BuilderTests.py +++ b/src/engine/SCons/BuilderTests.py @@ -248,20 +248,30 @@ class BuilderTestCase(unittest.TestCase): """Test returning the signature contents of a Builder """ + class DummyNode: + def __init__(self, name): + self.name = name + def __str__(self): + return self.name + def rfile(self): + return self + + target = map(DummyNode, map(lambda x: "__t%d__" % x, range(1, 7))) + source = map(DummyNode, map(lambda x: "__s%d__" % x, range(1, 7))) b1 = SCons.Builder.Builder(action = "foo ${TARGETS[5]}") - contents = b1.get_contents([],[],Environment()) + contents = b1.get_contents(target,source,Environment()) assert contents == "foo __t6__", contents b1 = SCons.Builder.Builder(action = "bar ${SOURCES[3:5]}") - contents = b1.get_contents([],[],Environment()) + contents = b1.get_contents(target,source,Environment()) assert contents == "bar __s4__ __s5__", contents b2 = SCons.Builder.Builder(action = Func) - contents = b2.get_contents([],[],Environment()) + contents = b2.get_contents(target,source,Environment()) assert contents == "\177\036\000\177\037\000d\000\000S", repr(contents) b3 = SCons.Builder.Builder(action = SCons.Action.ListAction(["foo", Func, "bar"])) - contents = b3.get_contents([],[],Environment()) + contents = b3.get_contents(target,source,Environment()) assert contents == "foo\177\036\000\177\037\000d\000\000Sbar", repr(contents) def test_node_factory(self): diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index e3d29eb..f317556 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -141,34 +141,6 @@ class BuilderDict(UserDict): for i, v in dict.items(): self.__setitem__(i, v) -_rm = re.compile(r'\$[()]') - -class _lister: - """This class is used to implement dummy targets and sources - for signature calculation.""" - def __init__(self, fmt): - self.fmt = fmt - def _format(self, index): - # For some reason, I originally made the fake names of - # the targets and sources 1-based (['__t1__, '__t2__']), - # not 0-based. We preserve this behavior by adding one - # to the returned item names, so everyone's targets - # won't get recompiled if they were using an old - # version. - return self.fmt % (index + 1) - def __getitem__(self, index): - return SCons.Util.PathList([self._format(index)])[0] - def __getslice__(self, i, j): - slice = [] - for x in range(i, j): - slice.append(self._format(x)) - return SCons.Util.PathList(slice) - def __getattr__(self, name): - # If we don't find an attribute in this class, let's - # look in PathList. self[0:2] returns a PathList instance - # via __getslice__ - return getattr(self[0:2], name) - class Environment: """Base class for construction Environments. These are the primary objects used to communicate dependency and @@ -442,20 +414,20 @@ class Environment: trailing characters. """ if raw: - regex_remove = None + mode = SCons.Util.SUBST_RAW else: - regex_remove = _rm - return SCons.Util.scons_subst(string, self, regex_remove, + mode = SCons.Util.SUBST_CMD + return SCons.Util.scons_subst(string, self, mode, target, source) def subst_list(self, string, raw=0, target=None, source=None): """Calls through to SCons.Util.scons_subst_list(). See the documentation for that function.""" if raw: - regex_remove = None + mode = SCons.Util.SUBST_RAW else: - regex_remove = _rm - return SCons.Util.scons_subst_list(string, self, regex_remove, + mode = SCons.Util.SUBST_CMD + return SCons.Util.scons_subst_list(string, self, mode, target, source) def get_scanner(self, skey): @@ -571,16 +543,3 @@ class Environment: if name[-len(old_suffix):] == old_suffix: name = name[:-len(old_suffix)] return os.path.join(dir, new_prefix+name+new_suffix) - - def sig_dict(self): - """Supply a dictionary for use in computing signatures. - - This fills in static TARGET, TARGETS, SOURCE and SOURCES - variables so that signatures stay the same every time. - """ - dict = self._dict.copy() - dict['TARGETS'] = _lister('__t%d__') - dict['TARGET'] = dict['TARGETS'][0] - dict['SOURCES'] = _lister('__s%d__') - dict['SOURCE'] = dict['SOURCES'][0] - return dict diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py index e99ea0d..28ea0e5 100644 --- a/src/engine/SCons/EnvironmentTests.py +++ b/src/engine/SCons/EnvironmentTests.py @@ -551,34 +551,42 @@ class EnvironmentTestCase(unittest.TestCase): of variables into other variables. """ env = Environment(AAA = 'a', BBB = 'b') - str = env.subst("$AAA ${AAA}A $BBBB $BBB") - assert str == "a aA b", str + mystr = env.subst("$AAA ${AAA}A $BBBB $BBB") + assert mystr == "a aA b", str # Changed the tests below to reflect a bug fix in # subst() env = Environment(AAA = '$BBB', BBB = 'b', BBBA = 'foo') - str = env.subst("$AAA ${AAA}A ${AAA}B $BBB") - assert str == "b bA bB b", str + mystr = env.subst("$AAA ${AAA}A ${AAA}B $BBB") + assert mystr == "b bA bB b", str env = Environment(AAA = '$BBB', BBB = '$CCC', CCC = 'c') - str = env.subst("$AAA ${AAA}A ${AAA}B $BBB") - assert str == "c cA cB c", str + mystr = env.subst("$AAA ${AAA}A ${AAA}B $BBB") + assert mystr == "c cA cB c", str env = Environment(AAA = '$BBB', BBB = '$CCC', CCC = [ 'a', 'b\nc' ]) lst = env.subst_list([ "$AAA", "B $CCC" ]) assert lst == [ [ "a", "b" ], [ "c", "B a", "b" ], [ "c" ] ], lst + class DummyNode: + def __init__(self, name): + self.name = name + def __str__(self): + return self.name + def rfile(self): + return self + # Test callables in the Environment - def foo(target, source, env): - assert target == 1, target - assert source == 2, source + def foo(target, source, env, for_signature): + assert str(target) == 't', target + assert str(source) == 's', source return env["FOO"] env = Environment(BAR=foo, FOO='baz') - subst = env.subst('test $BAR', target=1, source=2) + subst = env.subst('test $BAR', target=DummyNode('t'), source=DummyNode('s')) assert subst == 'test baz', subst - lst = env.subst_list('test $BAR', target=1, source=2) + lst = env.subst_list('test $BAR', target=DummyNode('t'), source=DummyNode('s')) assert lst[0][0] == 'test', lst[0][0] assert lst[0][1] == 'baz', lst[0][1] @@ -847,52 +855,6 @@ class EnvironmentTestCase(unittest.TestCase): 'PREFIX', 'SUFFIX', 'LIBPREFIX', 'LIBSUFFIX') - def test_sig_dict(self): - """Test the sig_dict() method""" - d = Environment(XYZZY = 'foo').sig_dict() - - assert d['XYZZY'] == 'foo' - - s = str(d['TARGET']) - assert s == '__t1__', s - s = str(d['TARGET'].dir) - assert s == '', s - s = str(d['TARGETS']) - assert s == '__t1__ __t2__', s - s = str(d['TARGETS'][1]) - assert s == '__t2__', s - s = str(d['TARGETS'][2]) - assert s == '__t3__', s - s = str(d['TARGETS'][87]) - assert s == '__t88__', s - s = str(d['TARGETS'][87].dir) - assert s == '', s - s = map(str, d['TARGETS'][3:5]) - assert s == ['__t4__', '__t5__'], s - s = map(lambda x: os.path.normcase(str(x)), d['TARGETS'].abspath) - assert s == map(os.path.normcase, [ os.path.join(os.getcwd(), '__t1__'), - os.path.join(os.getcwd(), '__t2__') ]) - - s = str(d['SOURCE']) - assert s == '__s1__', s - s = str(d['SOURCE'].dir) - assert s == '', s - s = str(d['SOURCES']) - assert s == '__s1__ __s2__', s - s = str(d['SOURCES'][1]) - assert s == '__s2__', s - s = str(d['SOURCES'][2]) - assert s == '__s3__', s - s = str(d['SOURCES'][87]) - assert s == '__s88__', s - s = str(d['SOURCES'][87].dir) - assert s == '', s - s = map(str, d['SOURCES'][3:5]) - assert s == ['__s4__', '__s5__'], s - s = map(lambda x: os.path.normcase(str(x)), d['SOURCES'].abspath) - assert s == map(os.path.normcase, [ os.path.join(os.getcwd(), '__s1__'), - os.path.join(os.getcwd(), '__s2__') ]) - if __name__ == "__main__": suite = unittest.makeSuite(EnvironmentTestCase, 'test_') diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index 0a016ad..e64aacc 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -41,11 +41,11 @@ import os.path import shutil import stat import string -from UserDict import UserDict import SCons.Action import SCons.Errors import SCons.Node +import SCons.Util import SCons.Warnings # @@ -190,7 +190,7 @@ class ParentOfRoot: This class is an instance of the Null object pattern. """ def __init__(self): - self.abspath = '' + self.abspath_str = '' self.path = '' self.abspath_ = '' self.path_ = '' @@ -251,14 +251,14 @@ class Entry(SCons.Node.Node): assert directory, "A directory must be provided" - self.abspath = directory.abspath_ + name + self.abspath_str = directory.abspath_ + name if directory.path == '.': self.path = name else: self.path = directory.path_ + name self.path_ = self.path - self.abspath_ = self.abspath + self.abspath_ = self.abspath_str self.dir = directory self.cwd = None # will hold the SConscript directory for target nodes self.duplicate = directory.duplicate @@ -293,11 +293,11 @@ class Entry(SCons.Node.Node): Since this should return the real contents from the file system, we check to see into what sort of subclass we should morph this Entry.""" - if os.path.isfile(self.abspath): + if os.path.isfile(self.abspath_str): self.__class__ = File self._morph() return File.get_contents(self) - if os.path.isdir(self.abspath): + if os.path.isdir(self.abspath_str): self.__class__ = Dir self._morph() return Dir.get_contents(self) @@ -307,7 +307,7 @@ class Entry(SCons.Node.Node): try: return self._exists except AttributeError: - self._exists = _existsp(self.abspath) + self._exists = _existsp(self.abspath_str) return self._exists def rexists(self): @@ -403,6 +403,71 @@ class Entry(SCons.Node.Node): self.sbuilder = scb return scb + def get_base_path(self): + """Return the file's directory and file name, with the + suffix stripped.""" + return os.path.splitext(self.get_path())[0] + + def get_suffix(self): + """Return the file's suffix.""" + return os.path.splitext(self.get_path())[1] + + def get_file_name(self): + """Return the file's name without the path.""" + return self.name + + def get_file_base(self): + """Return the file name with path and suffix stripped.""" + return os.path.splitext(self.name)[0] + + def get_posix_path(self): + """Return the path with / as the path separator, regardless + of platform.""" + if os.sep == '/': + return str(self) + else: + return string.replace(self.get_path(), os.sep, '/') + + def get_abspath(self): + """Get the absolute path of the file.""" + return self.abspath_str + + def get_srcdir(self): + """Returns the directory containing the source node linked to this + node via BuildDir(), or the directory of this node if not linked.""" + return self.srcnode().dir + + dictSpecialAttrs = { "file" : get_file_name, + "base" : get_base_path, + "filebase" : get_file_base, + "suffix" : get_suffix, + "posix" : get_posix_path, + "abspath" : get_abspath, + "srcpath" : srcnode, + "srcdir" : get_srcdir } + + def __getattr__(self, name): + # This is how we implement the "special" attributes + # such as base, suffix, basepath, etc. + # + # Note that we enclose values in a SCons.Util.Literal instance, + # so they will retain special characters during Environment variable + # substitution. + try: + attr = self.dictSpecialAttrs[name](self) + except KeyError: + raise AttributeError, '%s has no attribute: %s' % (self.__class__, name) + if SCons.Util.is_String(attr): + return SCons.Util.SpecialAttrWrapper(attr, self.name + + "_%s" % name) + return attr + + def for_signature(self): + # Return just our name. Even an absolute path would not work, + # because that can change thanks to symlinks or remapped network + # paths. + return self.name + # This is for later so we can differentiate between Entry the class and Entry # the method of the FS class. _classEntry = Entry @@ -491,7 +556,7 @@ class FS: raise SCons.Errors.UserError dir = Dir(drive, ParentOfRoot(), self) dir.path = dir.path + os.sep - dir.abspath = dir.abspath + os.sep + dir.abspath_str = dir.abspath_str + os.sep self.Root[drive] = dir directory = dir path_comp = path_comp[1:] @@ -577,7 +642,7 @@ class FS: if not dir is None: self._cwd = dir if change_os_dir: - os.chdir(dir.abspath) + os.chdir(dir.abspath_str) except: self._cwd = curr raise @@ -785,7 +850,7 @@ class Dir(Entry): node) don't use signatures for currency calculation.""" self.path_ = self.path + os.sep - self.abspath_ = self.abspath + os.sep + self.abspath_ = self.abspath_str + os.sep self.repositories = [] self.srcdir = None @@ -886,9 +951,9 @@ class Dir(Entry): keys = filter(lambda k: k != '.' and k != '..', self.entries.keys()) kids = map(lambda x, s=self: s.entries[x], keys) def c(one, two): - if one.abspath < two.abspath: + if one.abspath_str < two.abspath_str: return -1 - if one.abspath > two.abspath: + if one.abspath_str > two.abspath_str: return 1 return 0 kids.sort(c) @@ -1020,11 +1085,11 @@ class File(Entry): def get_contents(self): if not self.rexists(): return '' - return open(self.rfile().abspath, "rb").read() + return open(self.rfile().abspath_str, "rb").read() def get_timestamp(self): if self.rexists(): - return os.path.getmtime(self.rfile().abspath) + return os.path.getmtime(self.rfile().abspath_str) else: return 0 @@ -1263,7 +1328,7 @@ class File(Entry): # Duplicate from source path if we are set up to do this. if self.duplicate and not self.has_builder() and not self.linked: src=self.srcnode().rfile() - if src.exists() and src.abspath != self.abspath: + if src.exists() and src.abspath_str != self.abspath_str: self._createDir() try: Unlink(self, None, None) diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index f2d8dd9..209e80d 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -193,7 +193,7 @@ class BuildDirTestCase(unittest.TestCase): # Build path does not exist assert not f1.exists() # ...but the actual file is not there... - assert not os.path.exists(f1.abspath) + assert not os.path.exists(f1.get_abspath()) # And duplicate=0 should also work just like a Repository assert f1.rexists() # rfile() should point to the source path @@ -614,9 +614,9 @@ class FSTestCase(unittest.TestCase): assert dir.path_ == path_, \ "dir.path_ %s != expected path_ %s" % \ (dir.path_, path_) - assert dir.abspath == abspath, \ + assert dir.get_abspath() == abspath, \ "dir.abspath %s != expected absolute path %s" % \ - (dir.abspath, abspath) + (dir.get_abspath(), abspath) assert dir.abspath_ == abspath_, \ "dir.abspath_ %s != expected absolute path_ %s" % \ (dir.abspath_, abspath_) @@ -1052,6 +1052,12 @@ class FSTestCase(unittest.TestCase): assert exc_caught, "Should have caught an OSError, r = " + str(r) + f = fs.Entry('foo/bar/baz') + assert f.for_signature() == 'baz', f.for_signature() + assert f.get_string(0) == os.path.normpath('foo/bar/baz'), \ + f.get_string(0) + assert f.get_string(1) == 'baz', f.get_string(1) + class RepositoryTestCase(unittest.TestCase): def runTest(self): @@ -1512,7 +1518,64 @@ class clearTestCase(unittest.TestCase): assert not hasattr(f, '_exists') assert not hasattr(f, '_rexists') +class SpecialAttrTestCase(unittest.TestCase): + def runTest(self): + """Test special attributes of file nodes.""" + test=TestCmd(workdir='') + fs = SCons.Node.FS.FS(test.workpath('')) + f=fs.Entry('foo/bar/baz.blat') + assert str(f.dir) == os.path.normpath('foo/bar'), str(f.dir) + assert f.dir.is_literal() + assert f.dir.for_signature() == 'bar', f.dir.for_signature() + + assert str(f.file) == 'baz.blat', str(f.file) + assert f.file.is_literal() + assert f.file.for_signature() == 'baz.blat_file', \ + f.file.for_signature() + + assert str(f.base) == os.path.normpath('foo/bar/baz'), str(f.base) + assert f.base.is_literal() + assert f.base.for_signature() == 'baz.blat_base', \ + f.base.for_signature() + + assert str(f.filebase) == 'baz', str(f.filebase) + assert f.filebase.is_literal() + assert f.filebase.for_signature() == 'baz.blat_filebase', \ + f.filebase.for_signature() + + assert str(f.suffix) == '.blat', str(f.suffix) + assert f.suffix.is_literal() + assert f.suffix.for_signature() == 'baz.blat_suffix', \ + f.suffix.for_signature() + + assert str(f.abspath) == test.workpath('foo', 'bar', 'baz.blat'), str(f.abspath) + assert f.abspath.is_literal() + assert f.abspath.for_signature() == 'baz.blat_abspath', \ + f.abspath.for_signature() + + assert str(f.posix) == 'foo/bar/baz.blat', str(f.posix) + assert f.posix.is_literal() + if f.posix != f: + assert f.posix.for_signature() == 'baz.blat_posix', \ + f.posix.for_signature() + + fs.BuildDir('foo', 'baz') + + assert str(f.srcpath) == os.path.normpath('baz/bar/baz.blat'), str(f.srcpath) + assert f.srcpath.is_literal() + assert isinstance(f.srcpath, SCons.Node.FS.Entry) + + assert str(f.srcdir) == os.path.normpath('baz/bar'), str(f.srcdir) + assert f.srcdir.is_literal() + assert isinstance(f.srcdir, SCons.Node.FS.Dir) + + # And now, combinations!!! + assert str(f.srcpath.base) == os.path.normpath('baz/bar/baz'), str(f.srcpath.base) + assert str(f.srcpath.dir) == str(f.srcdir), str(f.srcpath.dir) + assert str(f.srcpath.posix) == 'baz/bar/baz.blat', str(f.srcpath.posix) + + if __name__ == "__main__": suite = unittest.TestSuite() @@ -1527,5 +1590,6 @@ if __name__ == "__main__": suite.addTest(SConstruct_dirTestCase()) suite.addTest(CacheDirTestCase()) suite.addTest(clearTestCase()) + suite.addTest(SpecialAttrTestCase()) if not unittest.TextTestRunner().run(suite).wasSuccessful(): sys.exit(1) diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py index 45c4af9..b188f81 100644 --- a/src/engine/SCons/Node/NodeTests.py +++ b/src/engine/SCons/Node/NodeTests.py @@ -294,6 +294,22 @@ class NodeTestCase(unittest.TestCase): c = node.builder_sig_adapter().get_contents() assert c == 7, c + class ListBuilder: + def __init__(self, targets): + self.tgt = targets + def targets(self, t): + return self.tgt + def get_contents(self, target, source, env): + assert target == self.tgt + return 8 + + node1 = SCons.Node.Node() + node2 = SCons.Node.Node() + node.builder_set(ListBuilder([node1, node2])) + node.env_set(Environment()) + c = node.builder_sig_adapter().get_contents() + assert c == 8, c + def test_current(self): """Test the default current() method """ @@ -747,6 +763,30 @@ class NodeTestCase(unittest.TestCase): n1 = MyNode("n1") assert n1.rstr() == 'n1', n1.rstr() + def test_abspath(self): + """Test the get_abspath() method.""" + n = MyNode("foo") + assert n.get_abspath() == str(n), n.get_abspath() + + def test_for_signature(self): + """Test the for_signature() method.""" + n = MyNode("foo") + assert n.for_signature() == str(n), n.get_abspath() + + def test_get_string(self): + """Test the get_string() method.""" + class TestNode(MyNode): + def __init__(self, name, sig): + MyNode.__init__(self, name) + self.sig = sig + + def for_signature(self): + return self.sig + + n = TestNode("foo", "bar") + assert n.get_string(0) == "foo", n.get_string(0) + assert n.get_string(1) == "bar", n.get_string(1) + def test_arg2nodes(self): """Test the arg2nodes function.""" dict = {} diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 1c23684..883d757 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -262,7 +262,7 @@ class Node: def __init__(self, node): self.node = node def get_contents(self): - return self.node.builder.get_contents(self.node, self.node.sources, self.node.generate_build_env()) + return self.node.builder.get_contents(self.node.builder.targets(self.node), self.node.sources, self.node.generate_build_env()) def get_timestamp(self): return None return Adapter(self) @@ -597,6 +597,47 @@ class Node: else: return None + def get_abspath(self): + """ + Return an absolute path to the Node. This will return simply + str(Node) by default, but for Node types that have a concept of + relative path, this might return something different. + """ + return str(self) + + def for_signature(self): + """ + Return a string representation of the Node that will always + be the same for this particular Node, no matter what. This + is by contrast to the __str__() method, which might, for + instance, return a relative path for a file Node. The purpose + of this method is to generate a value to be used in signature + calculation for the command line used to build a target, and + we use this method instead of str() to avoid unnecessary + rebuilds. This method does not need to return something that + would actually work in a command line; it can return any kind of + nonsense, so long as it does not change. + """ + return str(self) + + def get_string(self, for_signature): + """This is a convenience function designed primarily to be + used in command generators (i.e., CommandGeneratorActions or + Environment variables that are callable), which are called + with a for_signature argument that is nonzero if the command + generator is being called to generate a signature for the + command line, which determines if we should rebuild or not. + + Such command generators should use this method in preference + to str(Node) when converting a Node to a string, passing + in the for_signature parameter, such that we will call + Node.for_signature() or str(Node) properly, depending on whether + we are calculating a signature or actually constructing a + command line.""" + if for_signature: + return self.for_signature() + return str(self) + def get_children(node, parent): return node.children() def ignore_cycle(node, stack): pass def do_nothing(node, parent): pass diff --git a/src/engine/SCons/Platform/win32.py b/src/engine/SCons/Platform/win32.py index 39c0ba8..b36d611 100644 --- a/src/engine/SCons/Platform/win32.py +++ b/src/engine/SCons/Platform/win32.py @@ -54,9 +54,9 @@ class TempFileMunge: def __init__(self, cmd): self.cmd = cmd - def __call__(self, target, source, env): + def __call__(self, target, source, env, for_signature): cmd = env.subst_list(self.cmd, 0, target, source)[0] - if target is None or \ + if for_signature or \ (reduce(lambda x, y: x + len(y), cmd, 0) + len(cmd)) <= 2048: return self.cmd else: @@ -76,7 +76,6 @@ class TempFileMunge: if env['SHELL'] and env['SHELL'] == 'sh': native_tmp = string.replace(native_tmp, '\\', r'\\\\') - args = map(SCons.Util.quote_spaces, cmd[1:]) open(tmp, 'w').write(string.join(args, " ") + "\n") return [ cmd[0], '@' + native_tmp + '\n' + rm, native_tmp ] diff --git a/src/engine/SCons/Script/SConscript.py b/src/engine/SCons/Script/SConscript.py index af8cc79..5258d90 100644 --- a/src/engine/SCons/Script/SConscript.py +++ b/src/engine/SCons/Script/SConscript.py @@ -266,12 +266,12 @@ def SConscript(*ls, **kw): # interpret the stuff within the SConscript file # relative to where we are logically. default_fs.chdir(ldir, change_os_dir=0) - os.chdir(f.rfile().dir.abspath) + os.chdir(f.rfile().dir.get_abspath()) # Append the SConscript directory to the beginning # of sys.path so Python modules in the SConscript # directory can be easily imported. - sys.path = [ f.dir.abspath ] + sys.path + sys.path = [ f.dir.get_abspath() ] + sys.path # This is the magic line that actually reads up and # executes the stuff in the SConscript file. We @@ -294,7 +294,7 @@ def SConscript(*ls, **kw): # Repository directory. Like above, we do this # directly. default_fs.chdir(frame.prev_dir, change_os_dir=0) - os.chdir(frame.prev_dir.rdir().abspath) + os.chdir(frame.prev_dir.rdir().get_abspath()) results.append(frame.retval) diff --git a/src/engine/SCons/Tool/Perforce.py b/src/engine/SCons/Tool/Perforce.py index 3c574b4..3526952 100644 --- a/src/engine/SCons/Tool/Perforce.py +++ b/src/engine/SCons/Tool/Perforce.py @@ -70,7 +70,7 @@ def generate(env): # calling getcwd() for itself, which is odd. If no PWD variable # is present, p4 WILL call getcwd, but this seems to cause problems # with good ol' Win32's tilde-mangling for long file names. - environ['PWD'] = SCons.Node.FS.default_fs.Dir('#').abspath + environ['PWD'] = SCons.Node.FS.default_fs.Dir('#').get_abspath() for var in _import_env: v = os.environ.get(var) diff --git a/src/engine/SCons/Tool/jar.py b/src/engine/SCons/Tool/jar.py index b1164b5..fa21f3c 100644 --- a/src/engine/SCons/Tool/jar.py +++ b/src/engine/SCons/Tool/jar.py @@ -38,15 +38,15 @@ import os.path import SCons.Builder +JarBuilder = SCons.Builder.Builder(action = '$JARCOM', + source_factory = SCons.Node.FS.default_fs.Entry, + suffix = '$JARSUFFIX') + def generate(env): """Add Builders and construction variables for jar to an Environment.""" try: bld = env['BUILDERS']['Jar'] except KeyError: - JarBuilder = SCons.Builder.Builder(action = '$JARCOM', - source_factory = SCons.Node.FS.default_fs.Entry, - suffix = '$JARSUFFIX') - env['BUILDERS']['Jar'] = JarBuilder env['JAR'] = 'jar' diff --git a/src/engine/SCons/Tool/javac.py b/src/engine/SCons/Tool/javac.py index 331e183..8606c02 100644 --- a/src/engine/SCons/Tool/javac.py +++ b/src/engine/SCons/Tool/javac.py @@ -225,50 +225,50 @@ else: """ return os.path.split(file) +def emit_java_files(target, source, env): + """Create and return lists of source java files + and their corresponding target class files. + """ + env['_JAVACLASSDIR'] = target[0] + env['_JAVASRCDIR'] = source[0].rdir() + java_suffix = env.get('JAVASUFFIX', '.java') + class_suffix = env.get('JAVACLASSSUFFIX', '.class') + + slist = [] + js = _my_normcase(java_suffix) + def visit(arg, dirname, names, js=js, dirnode=source[0].rdir()): + java_files = filter(lambda n, js=js: + _my_normcase(n[-len(js):]) == js, + names) + mydir = dirnode.Dir(dirname) + java_paths = map(lambda f, d=mydir: d.File(f), java_files) + arg.extend(java_paths) + os.path.walk(source[0].rdir().get_abspath(), visit, slist) + + tlist = [] + for file in slist: + pkg_dir, classes = parse_java(file.get_abspath()) + if pkg_dir: + for c in classes: + tlist.append(target[0].Dir(pkg_dir).File(c+class_suffix)) + elif classes: + for c in classes: + tlist.append(target[0].File(c+class_suffix)) + else: + # This is an odd end case: no package and no classes. + # Just do our best based on the source file name. + tlist.append(target[0].File(str(file)[:-len(java_suffix)] + class_suffix)) + + return tlist, slist + +JavaBuilder = SCons.Builder.Builder(action = '$JAVACCOM', + emitter = emit_java_files, + target_factory = SCons.Node.FS.default_fs.Dir, + source_factory = SCons.Node.FS.default_fs.Dir) + def generate(env): """Add Builders and construction variables for javac to an Environment.""" - def emit_java_files(target, source, env): - """Create and return lists of source java files - and their corresponding target class files. - """ - env['_JAVACLASSDIR'] = target[0] - env['_JAVASRCDIR'] = source[0].rdir() - java_suffix = env.get('JAVASUFFIX', '.java') - class_suffix = env.get('JAVACLASSSUFFIX', '.class') - - slist = [] - js = _my_normcase(java_suffix) - def visit(arg, dirname, names, js=js, dirnode=source[0].rdir()): - java_files = filter(lambda n, js=js: - _my_normcase(n[-len(js):]) == js, - names) - mydir = dirnode.Dir(dirname) - java_paths = map(lambda f, d=mydir: d.File(f), java_files) - arg.extend(java_paths) - os.path.walk(source[0].rdir().abspath, visit, slist) - - tlist = [] - for file in slist: - pkg_dir, classes = parse_java(file.abspath) - if pkg_dir: - for c in classes: - tlist.append(target[0].Dir(pkg_dir).File(c+class_suffix)) - elif classes: - for c in classes: - tlist.append(target[0].File(c+class_suffix)) - else: - # This is an odd end case: no package and no classes. - # Just do our best based on the source file name. - tlist.append(target[0].File(str(file)[:-len(java_suffix)] + class_suffix)) - - return tlist, slist - - JavaBuilder = SCons.Builder.Builder(action = '$JAVACCOM', - emitter = emit_java_files, - target_factory = SCons.Node.FS.default_fs.Dir, - source_factory = SCons.Node.FS.default_fs.Dir) - env['BUILDERS']['Java'] = JavaBuilder env['JAVAC'] = 'javac' diff --git a/src/engine/SCons/Tool/linkloc.py b/src/engine/SCons/Tool/linkloc.py index fc315d9..3aafe6c 100644 --- a/src/engine/SCons/Tool/linkloc.py +++ b/src/engine/SCons/Tool/linkloc.py @@ -65,8 +65,8 @@ class LinklocGenerator: def __init__(self, cmdline): self.cmdline = cmdline - def __call__(self, env, target, source): - if target is None: + def __call__(self, env, target, source, for_signature): + if for_signature: # Expand the contents of any linker command files recursively subs = 1 strsub = env.subst(self.cmdline) diff --git a/src/engine/SCons/Tool/mingw.py b/src/engine/SCons/Tool/mingw.py index c89bc21..cd18bb5 100644 --- a/src/engine/SCons/Tool/mingw.py +++ b/src/engine/SCons/Tool/mingw.py @@ -55,10 +55,10 @@ def shlib_generator(target, source, env, for_signature): cmd.extend(['$SOURCES', '$_LIBDIRFLAGS', '$_LIBFLAGS']) implib = env.FindIxes(target, 'LIBPREFIX', 'LIBSUFFIX') - if implib: cmd.append('-Wl,--out-implib,'+str(implib)) + if implib: cmd.append('-Wl,--out-implib,'+implib.get_string(for_signature)) def_target = env.FindIxes(target, 'WIN32DEFPREFIX', 'WIN32DEFSUFFIX') - if def_target: cmd.append('-Wl,--output-def,'+str(def_target)) + if def_target: cmd.append('-Wl,--output-def,'+def_target.get_string(for_signature)) return [cmd] diff --git a/src/engine/SCons/Tool/mslink.py b/src/engine/SCons/Tool/mslink.py index 4bf8f0e..8aa6f09 100644 --- a/src/engine/SCons/Tool/mslink.py +++ b/src/engine/SCons/Tool/mslink.py @@ -44,41 +44,34 @@ import msvc from SCons.Tool.msvc import get_msdev_paths -def pdbGenerator(env, target, source): +def pdbGenerator(env, target, source, for_signature): if target and env.has_key('PDB') and env['PDB']: - return ['/PDB:%s'%target[0].File(env['PDB']), '/DEBUG'] - -def win32ShlinkTargets(target, source, env): - if target: - listCmd = [] - dll = env.FindIxes(target, 'SHLIBPREFIX', 'SHLIBSUFFIX') - if dll: listCmd.append("/out:%s"%dll) - - implib = env.FindIxes(target, 'LIBPREFIX', 'LIBSUFFIX') - if implib: listCmd.append("/implib:%s"%implib) - - return listCmd - else: - # For signature calculation - return '/out:$TARGET' - -def win32ShlinkSources(target, source, env): - if target: - listCmd = [] - - deffile = env.FindIxes(source, "WIN32DEFPREFIX", "WIN32DEFSUFFIX") - for src in source: - if src == deffile: - # Treat this source as a .def file. - listCmd.append("/def:%s" % src) - else: - # Just treat it as a generic source file. - listCmd.append(str(src)) - return listCmd - else: - # For signature calculation - return "$SOURCES" + return ['/PDB:%s'%target[0].File(env['PDB']).get_string(for_signature), + '/DEBUG'] +def win32ShlinkTargets(target, source, env, for_signature): + listCmd = [] + dll = env.FindIxes(target, 'SHLIBPREFIX', 'SHLIBSUFFIX') + if dll: listCmd.append("/out:%s"%dll.get_string(for_signature)) + + implib = env.FindIxes(target, 'LIBPREFIX', 'LIBSUFFIX') + if implib: listCmd.append("/implib:%s"%implib.get_string(for_signature)) + + return listCmd + +def win32ShlinkSources(target, source, env, for_signature): + listCmd = [] + + deffile = env.FindIxes(source, "WIN32DEFPREFIX", "WIN32DEFSUFFIX") + for src in source: + if src == deffile: + # Treat this source as a .def file. + listCmd.append("/def:%s" % src.get_string(for_signature)) + else: + # Just treat it as a generic source file. + listCmd.append(src) + return listCmd + def win32LibEmitter(target, source, env): msvc.validate_vars(env) diff --git a/src/engine/SCons/Tool/zip.py b/src/engine/SCons/Tool/zip.py index 4404247..510aa6e 100644 --- a/src/engine/SCons/Tool/zip.py +++ b/src/engine/SCons/Tool/zip.py @@ -59,7 +59,7 @@ try: internal_zip = 1 except ImportError: - zip = "$ZIP $ZIPFLAGS $( ${TARGET.abspath} $) $SOURCES" + zip = "$ZIP $ZIPFLAGS ${TARGET.abspath} $SOURCES" internal_zip = 0 diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py index 47aa37e..b7aac31 100644 --- a/src/engine/SCons/Util.py +++ b/src/engine/SCons/Util.py @@ -77,6 +77,19 @@ def updrive(path): path = string.upper(drive) + rest return path +if hasattr(types, 'UnicodeType'): + def to_String(s): + if isinstance(s, UserString): + t = type(s.data) + else: + t = type(s) + if t is types.UnicodeType: + return unicode(s) + else: + return str(s) +else: + to_String = str + class Literal: """A wrapper for a string. If you use this object wrapped around a string, then it will be interpreted as literal. @@ -91,144 +104,82 @@ class Literal: def is_literal(self): return 1 -class PathList(UserList.UserList): - """This class emulates the behavior of a list, but also implements - the special "path dissection" attributes we can use to find - suffixes, base names, etc. of the paths in the list. - - One other special attribute of this class is that, by - overriding the __str__ and __repr__ methods, this class - represents itself as a space-concatenated string of - the list elements, as in: - - >>> pl=PathList(["/foo/bar.txt", "/baz/foo.txt"]) - >>> pl - '/foo/bar.txt /baz/foo.txt' - >>> pl.base - 'bar foo' - """ +class SpecialAttrWrapper(Literal): + """This is a wrapper for what we call a 'Node special attribute.' + This is any of the attributes of a Node that we can reference from + Environment variable substitution, such as $TARGET.abspath or + $SOURCES[1].filebase. We inherit from Literal so we can handle + special characters, plus we implement a for_signature method, + such that we can return some canonical string during signatutre + calculation to avoid unnecessary rebuilds.""" + + def __init__(self, lstr, for_signature=None): + """The for_signature parameter, if supplied, will be the + canonical string we return from for_signature(). Else + we will simply return lstr.""" + Literal.__init__(self, lstr) + if for_signature: + self.forsig = for_signature + else: + self.forsig = lstr + + def for_signature(self): + return self.forsig + +class CallableComposite(UserList.UserList): + """A simple composite callable class that, when called, will invoke all + of its contained callables with the same arguments.""" def __init__(self, seq = []): UserList.UserList.__init__(self, seq) - def __getattr__(self, name): - # This is how we implement the "special" attributes - # such as base, suffix, basepath, etc. - try: - return self.dictSpecialAttrs[name](self) - except KeyError: - raise AttributeError, 'PathList has no attribute: %s' % name - - def __splitPath(self, split_func=os.path.split): - """This method calls the supplied split_func on each element - in the contained list. We expect split_func to return a - 2-tuple, usually representing two elements of a split file path, - such as those returned by os.path.split(). - - We return a 2-tuple of lists, each equal in length to the contained - list. The first list represents all the elements from the - first part of the split operation, the second represents - all elements from the second part.""" - list1 = [] - list2 = [] - for strPath in self.data: - first_part, second_part = split_func(strPath) - list1.append(first_part) - list2.append(second_part) - # Note that we return explicit PathList() instances, not - # self.__class__(). This makes sure the right attributes are - # available even if this object is a Lister, not a PathList. - return (PathList(list1), PathList(list2)) - - def __getBasePath(self): - """Return the file's directory and file name, with the - suffix stripped.""" - return self.__splitPath(splitext)[0] - - def __getSuffix(self): - """Return the file's suffix.""" - return self.__splitPath(splitext)[1] - - def __getFileName(self): - """Return the file's name without the path.""" - return self.__splitPath()[1] - - def __getDir(self): - """Return the file's path.""" - return self.__splitPath()[0] - - def __getBase(self): - """Return the file name with path and suffix stripped.""" - return self.__getFileName().__splitPath(splitext)[0] - - def __getAbsPath(self): - """Return the absolute path""" - # Note that we return an explicit PathList() instance, not - # self.__class__(). This makes sure the right attributes are - # available even if this object is a Lister, not a PathList. - return PathList(map(lambda x: updrive(os.path.abspath(x)), self.data)) - - def __getSrcDir(self): - """Return the directory containing the linked - source file, or this file path, if not linked""" - sp = self.__splitPath()[0] - rv = [] - for dir in sp: - dn = SCons.Node.FS.default_fs.Dir(str(dir)) - if (dn == None): - rv = rv + [''] - else: - rv = rv + [str(dn.srcnode())] - return PathList(rv) - - def __getSrcPath(self): - """Return the path to the linked source file, - or this file path, if not linked""" - rv = [] - for dir in self.data: - fn = SCons.Node.FS.default_fs.File(str(dir)) - if (fn == None): - rv = rv + [''] - else: - rv = rv + [str(fn.srcnode())] - return PathList(rv) - - def __posix(self): - if os.sep == '/': - return self - else: - return PathList(map(lambda x: string.replace(x, os.sep, '/'), self.data)) - - dictSpecialAttrs = { "file" : __getFileName, - "base" : __getBasePath, - "filebase" : __getBase, - "dir" : __getDir, - "suffix" : __getSuffix, - "abspath" : __getAbsPath, - "srcpath" : __getSrcPath, - "srcdir" : __getSrcDir, - "posix" : __posix - } + def __call__(self, *args, **kwargs): + retvals = map(lambda x, args=args, kwargs=kwargs: apply(x, + args, + kwargs), + self.data) + if self.data and (len(self.data) == len(filter(callable, retvals))): + return self.__class__(retvals) + return NodeList(retvals) + +class NodeList(UserList.UserList): + """This class is almost exactly like a regular list of Nodes + (actually it can hold any object), with one important difference. + If you try to get an attribute from this list, it will return that + attribute from every item in the list. For example: + + >>> someList = NodeList([ ' foo ', ' bar ' ]) + >>> someList.strip() + [ 'foo', 'bar' ] + """ + def __init__(self, seq = []): + UserList.UserList.__init__(self, seq) - def is_literal(self): - return 1 + def __nonzero__(self): + return len(self.data) != 0 def __str__(self): - return string.join(self.data) + return string.join(map(str, self.data)) - def to_String(self): - # Used by our variable-interpolation to interpolate a string. - # The interpolation doesn't use __str__() for this because then - # it interpolates other lists as "['x', 'y']". - return string.join(self.data) - - def __repr__(self): - return repr(string.join(self.data)) + def __getattr__(self, name): + if not self.data: + # If there is nothing in the list, then we have no attributes to + # pass through, so raise AttributeError for everything. + raise AttributeError, "NodeList has no attribute: %s" % name + + # Return a list of the attribute, gotten from every element + # in the list + attrList = map(lambda x, n=name: getattr(x, n), self.data) + + # Special case. If the attribute is callable, we do not want + # to return a list of callables. Rather, we want to return a + # single callable that, when called, will invoke the function on + # all elements of this list. + if self.data and (len(self.data) == len(filter(callable, attrList))): + return CallableComposite(attrList) + return self.__class__(attrList) - def __getitem__(self, item): - # We must do this to ensure that single items returned - # by index access have the special attributes such as - # suffix and basepath. - return self.__class__([ UserList.UserList.__getitem__(self, item), ]) + def is_literal(self): + return 1 _env_var = re.compile(r'^\$([_a-zA-Z]\w*|{[_a-zA-Z]\w*})$') @@ -278,11 +229,11 @@ def quote_spaces(arg): # so that we do not accidentally smush two variables # together during the recursive interpolation process. -_cv = re.compile(r'\$([_a-zA-Z]\w*|{[^}]*})') +_cv = re.compile(r'\$([_a-zA-Z][\.\w]*|{[^}]*})') _space_sep = re.compile(r'[\t ]+(?![^{]*})') _newline = re.compile(r'[\r\n]+') -def _convertArg(x): +def _convertArg(x, strconv=to_String): """This function converts an individual argument. If the argument is to be interpreted literally, with all special characters escaped, then we insert a special code in front @@ -298,16 +249,16 @@ def _convertArg(x): if not literal: # escape newlines as '\0\2', '\0\1' denotes an argument split # Also escape double-dollar signs to mean the literal dollar sign. - return string.replace(_newline.sub('\0\2', to_String(x)), '$$', '\0\4') + return string.replace(_newline.sub('\0\2', strconv(x)), '$$', '\0\4') else: # Interpret non-string args as literals. # The special \0\3 code will tell us to encase this string # in a Literal instance when we are all done # Also escape out any $ signs because we don't want # to continue interpolating a literal. - return '\0\3' + string.replace(str(x), '$', '\0\4') + return '\0\3' + string.replace(strconv(x), '$', '\0\4') -def _convert(x): +def _convert(x, strconv = to_String): """This function is used to convert construction variable values or the value of strSubst to a string for interpolation. This function follows the rules outlined in the documentaion @@ -316,12 +267,13 @@ def _convert(x): return '' elif is_String(x): # escape newlines as '\0\2', '\0\1' denotes an argument split - return _convertArg(_space_sep.sub('\0\1', x)) + return _convertArg(_space_sep.sub('\0\1', x), strconv) elif is_List(x): # '\0\1' denotes an argument split - return string.join(map(_convertArg, x), '\0\1') + return string.join(map(lambda x, s=strconv: _convertArg(x, s), x), + '\0\1') else: - return _convertArg(x) + return _convertArg(x, strconv) class CmdStringHolder: """This is a special class used to hold strings generated @@ -414,24 +366,47 @@ def subst_dict(target, source, env): if not is_List(target): target = [target] - dict['TARGETS'] = PathList(map(os.path.normpath, map(str, target))) + dict['TARGETS'] = NodeList(target) if dict['TARGETS']: dict['TARGET'] = dict['TARGETS'][0] - def rstr(x): - try: - return x.rstr() - except AttributeError: - return str(x) if not is_List(source): source = [source] - dict['SOURCES'] = PathList(map(os.path.normpath, map(rstr, source))) + dict['SOURCES'] = NodeList(map(lambda x: x.rfile(), source)) if dict['SOURCES']: dict['SOURCE'] = dict['SOURCES'][0] return dict -def scons_subst_list(strSubst, env, remove=None, target=None, +# Constants for the "mode" parameter to scons_subst_list() and +# scons_subst(). SUBST_RAW gives the raw command line. SUBST_CMD +# gives a command line suitable for passing to a shell. SUBST_SIG +# gives a command line appropriate for calculating the signature +# of a command line...if this changes, we should rebuild. +SUBST_RAW = 0 +SUBST_CMD = 1 +SUBST_SIG = 2 + +_rm = re.compile(r'\$[()]') +_remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)') + +def _canonicalize(obj): + """Attempt to call the object's for_signature method, + which is expected to return a string suitable for use in calculating + a command line signature (i.e., it only changes when we should + rebuild the target). For instance, file Nodes will report only + their file name (with no path), so changing Repository settings + will not cause a rebuild.""" + try: + return obj.for_signature() + except AttributeError: + return to_String(obj) + +# Indexed by the SUBST_* constants above. +_regex_remove = [ None, _rm, _remove ] +_strconv = [ to_String, to_String, _canonicalize ] + +def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None): """ This function serves the same purpose as scons_subst(), except @@ -457,39 +432,46 @@ def scons_subst_list(strSubst, env, remove=None, target=None, (e.g. file names) to contain embedded newline characters. """ + remove = _regex_remove[mode] + strconv = _strconv[mode] + if target != None: dict = subst_dict(target, source, env) else: - dict = env.sig_dict() + dict = env.Dictionary() def repl(m, target=target, source=source, env=env, local_vars = dict, - global_vars = { "__env__" : env }): + global_vars = { "__env__" : env }, + strconv=strconv, + sig=(mode != SUBST_CMD)): key = m.group(1) if key[0] == '{': key = key[1:-1] try: e = eval(key, global_vars, local_vars) - if callable(e): - # We wait to evaluate callables until the end of everything - # else. For now, we instert a special escape sequence - # that we will look for later. - return '\0\5' + _convert(e(target=target, - source=source, - env=env)) + '\0\5' - else: - # The \0\5 escape code keeps us from smushing two or more - # variables together during recusrive substitution, i.e. - # foo=$bar bar=baz barbaz=blat => $foo$bar->blat (bad) - return "\0\5" + _convert(e) + "\0\5" except NameError: return '\0\5' + if callable(e): + # We wait to evaluate callables until the end of everything + # else. For now, we instert a special escape sequence + # that we will look for later. + return '\0\5' + _convert(e(target=target, + source=source, + env=env, + for_signature=sig), + strconv) + '\0\5' + else: + # The \0\5 escape code keeps us from smushing two or more + # variables together during recusrive substitution, i.e. + # foo=$bar bar=baz barbaz=blat => $foo$bar->blat (bad) + return "\0\5" + _convert(e, strconv) + "\0\5" # Convert the argument to a string: - strSubst = _convert(strSubst) + strSubst = _convert(strSubst, strconv) # Do the interpolation: n = 1 @@ -510,7 +492,7 @@ def scons_subst_list(strSubst, env, remove=None, target=None, return map(lambda x: map(CmdStringHolder, filter(lambda y:y, string.split(x, '\0\1'))), listLines) -def scons_subst(strSubst, env, remove=None, target=None, +def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None): """Recursively interpolates dictionary variables into the specified string, returning the expanded result. @@ -527,32 +509,48 @@ def scons_subst(strSubst, env, remove=None, target=None, if target != None: dict = subst_dict(target, source, env) else: - dict = env.sig_dict() + dict = env.Dictionary() + + remove = _regex_remove[mode] + strconv = _strconv[mode] def repl(m, target=target, source=source, env=env, local_vars = dict, - global_vars = { '__env__' : env }): + global_vars = { '__env__' : env }, + strconv=strconv, + sig=(mode != SUBST_CMD)): key = m.group(1) if key[0] == '{': key = key[1:-1] try: e = eval(key, global_vars, local_vars) - if callable(e): - e = e(target=target, source=source, env=env) - if e is None: - s = '' - elif is_List(e): - try: - s = e.to_String() - except AttributeError: - s = string.join(map(to_String, e), ' ') - else: - s = to_String(e) except NameError: + return '\0\5' + if callable(e): + e = e(target=target, source=source, env=env, for_signature=sig) + + def conv(arg, strconv=strconv): + literal = 0 + try: + if arg.is_literal(): + literal = 1 + except AttributeError: + pass + ret = strconv(arg) + if literal: + # Escape dollar signs to prevent further + # substitution on literals. + ret = string.replace(ret, '$', '\0\4') + return ret + if e is None: s = '' + elif is_List(e): + s = string.join(map(conv, e), ' ') + else: + s = conv(e) # Insert placeholders to avoid accidentally smushing # separate variables together. return "\0\5" + s + "\0\5" @@ -563,7 +561,8 @@ def scons_subst(strSubst, env, remove=None, target=None, # escape double dollar signs strSubst = string.replace(strSubst, '$$', '\0\4') strSubst,n = _cv.subn(repl, strSubst) - # and then remove remove + + # remove the remove regex if remove: strSubst = remove.sub('', strSubst) @@ -615,19 +614,6 @@ def is_Dict(e): def is_List(e): return type(e) is types.ListType or isinstance(e, UserList.UserList) -if hasattr(types, 'UnicodeType'): - def to_String(s): - if isinstance(s, UserString): - t = type(s.data) - else: - t = type(s) - if t is types.UnicodeType: - return unicode(s) - else: - return str(s) -else: - to_String = str - def argmunge(arg): return Split(arg) @@ -678,7 +664,7 @@ def mapPaths(paths, dir, env=None): return str(dir) if os.path.isabs(path) or path[0] == '#': return path - return dir.path_ + path + return str(dir) + os.sep + path return path if not is_List(paths): diff --git a/src/engine/SCons/UtilTests.py b/src/engine/SCons/UtilTests.py index b7aa478..e0543d9 100644 --- a/src/engine/SCons/UtilTests.py +++ b/src/engine/SCons/UtilTests.py @@ -25,17 +25,13 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import os import os.path -import re import string import sys import types import unittest -import SCons.Node -import SCons.Node.FS from SCons.Util import * import TestCmd - class OutBuffer: def __init__(self): self.buffer = "" @@ -58,33 +54,55 @@ class DummyEnv: dict["SOURCES"] = 'ssig' return dict -def CmdGen1(target, source, env): +def CmdGen1(target, source, env, for_signature): # Nifty trick...since Environment references are interpolated, # instantiate an instance of a callable class with this one, # which will then get evaluated. - assert target == 't', target - assert source == 's', source - return "${CMDGEN2('foo')}" + assert str(target) == 't', target + assert str(source) == 's', source + return "${CMDGEN2('foo', %d)}" % for_signature class CmdGen2: - def __init__(self, mystr): + def __init__(self, mystr, forsig): self.mystr = mystr + self.expect_for_signature = forsig - def __call__(self, target, source, env): - assert target == 't', target - assert source == 's', source + def __call__(self, target, source, env, for_signature): + assert str(target) == 't', target + assert str(source) == 's', source + assert for_signature == self.expect_for_signature, for_signature return [ self.mystr, env.Dictionary('BAR') ] class UtilTestCase(unittest.TestCase): def test_subst(self): """Test the subst function""" loc = {} - target = [ "./foo/bar.exe", - "/bar/baz.obj", - "../foo/baz.obj" ] - source = [ "./foo/blah.cpp", - "/bar/ack.cpp", - "../foo/ack.c" ] + + class N: + """Simple node work-alike with some extra stuff for testing.""" + def __init__(self, data): + self.data = os.path.normpath(data) + + def __str__(self): + return self.data + + def is_literal(self): + return 1 + + def get_stuff(self, extra): + return self.data + extra + + def rfile(self): + return self + + foo = 1 + + target = [ N("./foo/bar.exe"), + N("/bar/baz.obj"), + N("../foo/baz.obj") ] + source = [ N("./foo/blah.cpp"), + N("/bar/ack.cpp"), + N("../foo/ack.c") ] loc['xxx'] = None loc['zero'] = 0 loc['one'] = 1 @@ -122,58 +140,33 @@ class UtilTestCase(unittest.TestCase): target=target, source=source) assert newcom == cvt("test foo/bar.exe[0]") - newcom = scons_subst("test ${TARGET.file}", env, - target=target, source=source) - assert newcom == cvt("test bar.exe") - - newcom = scons_subst("test ${TARGET.filebase}", env, + newcom = scons_subst("test $TARGETS.foo", env, target=target, source=source) - assert newcom == cvt("test bar") + assert newcom == "test 1 1 1", newcom - newcom = scons_subst("test ${TARGET.suffix}", env, + newcom = scons_subst("test ${SOURCES[0:2].foo}", env, target=target, source=source) - assert newcom == cvt("test .exe") + assert newcom == "test 1 1", newcom - newcom = scons_subst("test ${TARGET.base}", env, + newcom = scons_subst("test $SOURCE.foo", env, target=target, source=source) - assert newcom == cvt("test foo/bar") + assert newcom == "test 1", newcom - newcom = scons_subst("test ${TARGET.dir}", env, + newcom = scons_subst("test ${TARGET.get_stuff('blah')}", env, target=target, source=source) - assert newcom == cvt("test foo") + assert newcom == cvt("test foo/bar.exeblah"), newcom - newcom = scons_subst("test ${TARGET.abspath}", env, + newcom = scons_subst("test ${SOURCES.get_stuff('blah')}", env, target=target, source=source) - assert newcom == cvt("test %s/foo/bar.exe"%SCons.Util.updrive(os.getcwd())), newcom + assert newcom == cvt("test foo/blah.cppblah /bar/ack.cppblah ../foo/ack.cblah"), newcom - newcom = scons_subst("test ${SOURCES.abspath}", env, + newcom = scons_subst("test ${SOURCES[0:2].get_stuff('blah')}", env, target=target, source=source) - assert newcom == cvt("test %s/foo/blah.cpp %s %s/foo/ack.c"%(SCons.Util.updrive(os.getcwd()), - SCons.Util.updrive(os.path.abspath(os.path.normpath("/bar/ack.cpp"))), - SCons.Util.updrive(os.path.normpath(os.getcwd()+"/..")))), newcom + assert newcom == cvt("test foo/blah.cppblah /bar/ack.cppblah"), newcom - newcom = scons_subst("test ${SOURCE.abspath}", env, + newcom = scons_subst("test ${SOURCES[0:2].get_stuff('blah')}", env, target=target, source=source) - assert newcom == cvt("test %s/foo/blah.cpp"%SCons.Util.updrive(os.getcwd())), newcom - - # Note that we don't use the cvt() helper function here, - # because we're testing that the .posix attribute does its own - # conversion of the path name backslashes to slashes. - newcom = scons_subst("test ${TARGET.posix} ${SOURCE.posix}", env, - target=target, source=source) - assert newcom == "test foo/bar.exe foo/blah.cpp", newcom - - SCons.Node.FS.default_fs.BuildDir("#baz","#foo") - - newcom = scons_subst("test ${SOURCE.srcdir}", env, - target=target, source=['baz/bar.c']) - - assert newcom == cvt("test foo"), newcom - - newcom = scons_subst("test ${SOURCE.srcpath}", env, - target=target, source=['baz/bar.c']) - - assert newcom == cvt("test foo/bar.c"), newcom + assert newcom == cvt("test foo/blah.cppblah /bar/ack.cppblah"), newcom newcom = scons_subst("test $xxx", env) assert newcom == cvt("test"), newcom @@ -184,10 +177,10 @@ class UtilTestCase(unittest.TestCase): newcom = scons_subst("test $( $xxx $)", env) assert newcom == cvt("test $( $)"), newcom - newcom = scons_subst("test $($xxx$)", env, re.compile('\$[()]')) + newcom = scons_subst("test $($xxx$)", env, mode=SUBST_SIG) assert newcom == cvt("test"), newcom - newcom = scons_subst("test $( $xxx $)", env, re.compile('\$[()]')) + newcom = scons_subst("test $( $xxx $)", env, mode=SUBST_SIG) assert newcom == cvt("test"), newcom newcom = scons_subst("test $zero", env) @@ -196,11 +189,8 @@ class UtilTestCase(unittest.TestCase): newcom = scons_subst("test $one", env) assert newcom == cvt("test 1"), newcom - newcom = scons_subst("test aXbXcXd", env, re.compile('X')) - assert newcom == cvt("test abcd"), newcom - newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS", - env, target='t', source='s') + env, target=N('t'), source=N('s')) assert newcom == cvt("test foo baz s t"), newcom # Test against a former bug in scons_subst_list() @@ -217,6 +207,23 @@ class UtilTestCase(unittest.TestCase): newcom = scons_subst("$$FOO$BAZ", DummyEnv(glob)) assert newcom == "$FOOBLAT", newcom + class TestLiteral: + def __init__(self, literal): + self.literal = literal + + def __str__(self): + return self.literal + + def is_literal(self): + return 1 + + # Test that a literal will stop dollar-sign substitution + glob = { "FOO" : "BAR", + "BAZ" : TestLiteral("$FOO"), + "BAR" : "$FOO" } + newcom = scons_subst("$FOO $BAZ $BAR", DummyEnv(glob)) + assert newcom == "BAR $FOO BAR", newcom + def test_splitext(self): assert splitext('foo') == ('foo','') assert splitext('foo.bar') == ('foo','.bar') @@ -227,19 +234,21 @@ class UtilTestCase(unittest.TestCase): class Node: def __init__(self, name): - self.name = name + self.name = os.path.normpath(name) def __str__(self): return self.name def is_literal(self): return 1 + def rfile(self): + return self loc = {} - target = [ "./foo/bar.exe", - "/bar/baz with spaces.obj", - "../foo/baz.obj" ] - source = [ "./foo/blah with spaces.cpp", - "/bar/ack.cpp", - "../foo/ack.c" ] + target = [ Node("./foo/bar.exe"), + Node("/bar/baz with spaces.obj"), + Node("../foo/baz.obj") ] + source = [ Node("./foo/blah with spaces.cpp"), + Node("/bar/ack.cpp"), + Node("../foo/ack.c") ] loc['xxx'] = None loc['NEWLINE'] = 'before\nafter' @@ -307,7 +316,7 @@ class UtilTestCase(unittest.TestCase): # Test interpolating a callable. cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES", env, - target='t', source='s') + target=Node('t'), source=Node('s')) assert len(cmd_list) == 1, len(cmd_list) assert cmd_list[0][0] == 'testing', cmd_list[0][0] assert cmd_list[0][1] == 'foo', cmd_list[0][1] @@ -335,8 +344,8 @@ class UtilTestCase(unittest.TestCase): return '**' + foo + '**' def quote_func(foo): return foo - glob = { "FOO" : PathList([ 'foo\nwith\nnewlines', - 'bar\nwith\nnewlines' ]) } + glob = { "FOO" : [ Literal('foo\nwith\nnewlines'), + Literal('bar\nwith\nnewlines') ] } cmd_list = scons_subst_list("$FOO", DummyEnv(glob)) assert cmd_list[0][0] == 'foo\nwith\nnewlines', cmd_list[0][0] cmd_list[0][0].escape(escape_func) @@ -595,11 +604,41 @@ class UtilTestCase(unittest.TestCase): assert cmd_list[0] == 'BAZ', cmd_list[0] assert cmd_list[1] == '**$BAR**', cmd_list[1] + def test_SpecialAttrWrapper(self): + """Test the SpecialAttrWrapper() function.""" + input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ] + + def escape_func(cmd): + return '**' + cmd + '**' + + + cmd_list = scons_subst_list(input_list, + DummyEnv({ 'FOO' : 'BAZ', + 'BAR' : 'BLAT' })) + map(lambda x, e=escape_func: x.escape(e), cmd_list[0]) + cmd_list = map(str, cmd_list[0]) + assert cmd_list[0] == 'BAZ', cmd_list[0] + assert cmd_list[1] == '**$BAR**', cmd_list[1] + + cmd_list = scons_subst_list(input_list, + DummyEnv({ 'FOO' : 'BAZ', + 'BAR' : 'BLAT' }), + mode=SUBST_SIG) + map(lambda x, e=escape_func: x.escape(e), cmd_list[0]) + cmd_list = map(str, cmd_list[0]) + assert cmd_list[0] == 'BAZ', cmd_list[0] + assert cmd_list[1] == '**BLEH**', cmd_list[1] + def test_mapPaths(self): """Test the mapPaths function""" - fs = SCons.Node.FS.FS() - dir=fs.Dir('foo') - file=fs.File('bar/file') + class MyFileNode: + def __init__(self, path): + self.path = path + def __str__(self): + return self.path + + dir=MyFileNode('foo') + file=MyFileNode('bar/file') class DummyEnv: def subst(self, arg): @@ -618,11 +657,11 @@ class UtilTestCase(unittest.TestCase): def test_display(self): old_stdout = sys.stdout sys.stdout = OutBuffer() - SCons.Util.display("line1") + display("line1") display.set_mode(0) - SCons.Util.display("line2") + display("line2") display.set_mode(1) - SCons.Util.display("line3") + display("line3") assert sys.stdout.buffer == "line1\nline3\n" sys.stdout = old_stdout @@ -648,12 +687,12 @@ class UtilTestCase(unittest.TestCase): "Removed " + os.path.join(base, xxx) + '\n' + \ "Removed directory " + base + '\n' - SCons.Util.fs_delete(base, remove=0) + fs_delete(base, remove=0) assert sys.stdout.buffer == exp, sys.stdout.buffer assert os.path.exists(sub1_yyy) sys.stdout.buffer = "" - SCons.Util.fs_delete(base, remove=1) + fs_delete(base, remove=1) assert sys.stdout.buffer == exp assert not os.path.exists(base) @@ -666,7 +705,7 @@ class UtilTestCase(unittest.TestCase): filename = tempfile.mktemp() str = '1234567890 ' + filename open(filename, 'w').write(str) - assert open(SCons.Util.get_native_path(filename)).read() == str + assert open(get_native_path(filename)).read() == str def test_subst_dict(self): """Test substituting dictionary values in an Action @@ -675,14 +714,24 @@ class UtilTestCase(unittest.TestCase): assert d['a'] == 'A', d assert d['b'] == 'B', d - d = subst_dict(target = 't', source = 's', env=DummyEnv()) - assert str(d['TARGETS']) == 't', d['TARGETS'] + class SimpleNode: + def __init__(self, data): + self.data = data + def __str__(self): + return self.data + def rfile(self): + return self + def is_literal(self): + return 1 + + d = subst_dict(target = SimpleNode('t'), source = SimpleNode('s'), env=DummyEnv()) + assert str(d['TARGETS'][0]) == 't', d['TARGETS'] assert str(d['TARGET']) == 't', d['TARGET'] - assert str(d['SOURCES']) == 's', d['SOURCES'] + assert str(d['SOURCES'][0]) == 's', d['SOURCES'] assert str(d['SOURCE']) == 's', d['SOURCE'] - d = subst_dict(target = ['t1', 't2'], - source = ['s1', 's2'], + d = subst_dict(target = [SimpleNode('t1'), SimpleNode('t2')], + source = [SimpleNode('s1'), SimpleNode('s2')], env = DummyEnv()) TARGETS = map(lambda x: str(x), d['TARGETS']) TARGETS.sort() @@ -698,11 +747,11 @@ class UtilTestCase(unittest.TestCase): self.name = name def __str__(self): return self.name - def rstr(self): - return 'rstr-' + self.name + def rfile(self): + return self.__class__('rstr-' + self.name) - d = subst_dict(target = [N('t3'), 't4'], - source = ['s3', N('s4')], + d = subst_dict(target = [N('t3'), SimpleNode('t4')], + source = [SimpleNode('s3'), N('s4')], env = DummyEnv()) TARGETS = map(lambda x: str(x), d['TARGETS']) TARGETS.sort() @@ -711,6 +760,29 @@ class UtilTestCase(unittest.TestCase): SOURCES.sort() assert SOURCES == ['rstr-s4', 's3'], d['SOURCES'] + def test_NodeList(self): + """Test NodeList class""" + class TestClass: + def __init__(self, name, child=None): + self.child = child + self.bar = name + def foo(self): + return self.bar + "foo" + def getself(self): + return self + + t1 = TestClass('t1', TestClass('t1child')) + t2 = TestClass('t2', TestClass('t2child')) + t3 = TestClass('t3') + + nl = NodeList([t1, t2, t3]) + assert nl.foo() == [ 't1foo', 't2foo', 't3foo' ], nl.foo() + assert nl.bar == [ 't1', 't2', 't3' ], nl.bar + assert nl.getself().bar == [ 't1', 't2', 't3' ], nl.getself().bar + assert nl[0:2].child.foo() == [ 't1childfoo', 't2childfoo' ], \ + nl[0:2].child.foo() + assert nl[0:2].child.bar == [ 't1child', 't2child' ], \ + nl[0:2].child.bar if __name__ == "__main__": suite = unittest.makeSuite(UtilTestCase, 'test_') diff --git a/test/CacheDir.py b/test/CacheDir.py index 24c7e60..de78e7a 100644 --- a/test/CacheDir.py +++ b/test/CacheDir.py @@ -239,7 +239,7 @@ CacheDir(r'%s') def docopy(target,source,env): data = source[0].get_contents() - f = open(target[0].rfile().abspath, "wb") + f = open(target[0].rfile().get_abspath(), "wb") f.write(data) f.close() diff --git a/test/scan-once.py b/test/scan-once.py index ba147cf..f629db6 100644 --- a/test/scan-once.py +++ b/test/scan-once.py @@ -332,7 +332,7 @@ Mylib.ExportLib(env, lib_fullname) #cmd_justlib = "cd %s ; make" % Dir(".") cmd_generated = "%s $SOURCE" % (sys.executable,) -cmd_justlib = "%s %s -C ${SOURCE[0].dir}" % (sys.executable, sys.argv[0]) +cmd_justlib = "%s %s -C ${SOURCES[0].dir}" % (sys.executable, sys.argv[0]) ##### Deps appear correct ... but wacky scanning? # Why? -- cgit v0.12