diff options
Diffstat (limited to 'src')
32 files changed, 1686 insertions, 344 deletions
diff --git a/src/CHANGES.txt b/src/CHANGES.txt index 8886204..c8dc7d3 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -8,7 +8,106 @@ -RELEASE 0.XX - XXX +RELEASE 0.97.0d20071212 - Wed, 12 Dec 2007 09:29:32 -0600 + + From Benoit Belley: + + - Fix occasional spurious rebuilds and inefficiency when using + --implicit-cache and Builders that produce multiple targets. + + - Allow SCons to not have to know about the builders of generated + files when BuildDir(duplicate=0) is used, potentially allowing some + SConscript files to be ignored for smaller builds. + + From David Cournapeau: + + - Add a CheckTypeSize() call to configure contexts. + + From Ken Deeter: + + - Make the "contents" of Alias Nodes a concatenation of the children's + content signatures (MD5 checksums), not a concatenation of the + children's contents, to avoid using large amounts of memory during + signature calculation. + + From Malte Helmert: + + - Fix a lot of typos in the man page and User's Guide. + + From Geoffrey Irving: + + - Speed up conversion of paths in .sconsign files to File or Dir Nodes. + + From Steven Knight: + + - Add an Options.UnknownOptions() method that returns any settings + (from the command line, or whatever dictionary was passed in) + that aren't known to the Options object. + + - Add a Glob() function. + + - When removing targets with the -c option, use the absolute path (to + avoid problems interpreting BuildDir() when the top-level directory + is the source directory). + + - Fix problems with Install() and InstallAs() when called through a + clone (of a clone, ...) of a cloned construction environment. + + - When executing a file containing Options() settings, add the file's + directory to sys.path (so modules can be imported from there) and + explicity set __name__ to the name of the file so the statement's + in the file can deduce the location if they need to. + + - Fix an O(n^2) performance problem when adding sources to a target + through calls to a multi Builder (including Aliases). + + - Redefine the $WINDOWSPROGMANIFESTSUFFIX and + $WINDOWSSHLIBMANIFESTSUFFIX variables so they pick up changes to + the underlying $SHLIBSUFFIX and $PROGSUFFIX variables. + + - Add a GetBuildFailures() function that can be called from functions + registered with the Python atexit module to print summary information + about any failures encountered while building. + + - Return a NodeList object, not a Python list, when a single_source + Builder like Object() is called with more than one file. + + - When searching for implicit dependency files in the directories + in a $*PATH list, don't create Dir Nodes for directories that + don't actually exist on-disk. + + - Add a Requires() function to allow the specification of order-only + prerequisites, which will be updated before specified "downstream" + targets but which don't actually cause the target to be rebuilt. + + - Restore the FS.{Dir,File,Entry}.rel_path() method. + + - Make the default behavior of {Source,Target}Signatures('timestamp') + be equivalent to 'timestamp-match', not 'timestamp-newer'. + + - Fix use of CacheDir with Decider('timestamp-newer') by updating + the modification time when copying files from the cache. + + - Fix random issues with parallel (-j) builds on Windows when Python + holds open file handles (especially for SCons temporary files, + or targets built by Python function actions) across process creation. + + From Maxim Kartashev: + + - Fix test scripts when run on Solaris. + + From Gary Oberbrunner: + + - Fix Glob() when a pattern is in an explicitly-named subdirectory. + + From Philipp Scholl: + + - Fix setting up targets if multiple Package builders are specified + at once. + + + +RELEASE 0.97.0d20070918 - Tue, 18 Sep 2007 10:51:27 -0500 From Steven Knight: diff --git a/src/RELEASE.txt b/src/RELEASE.txt index 23c5638..ca01607 100644 --- a/src/RELEASE.txt +++ b/src/RELEASE.txt @@ -20,11 +20,39 @@ more effectively, please sign up for the scons-users mailing list at: -RELEASE 0.97.0d200709XX - XXX +RELEASE 0.97.0d20071212 - Wed, 12 Dec 2007 09:29:32 -0600 This is the eighth beta release of SCons. Please consult the CHANGES.txt file for a list of specific changes since last release. + Please note the following important changes since release 0.97.0d20070918: + + -- SCons REDEFINES PYTHON open() AND file() ON Windows TO NOT PASS + ON OPEN FILE HANDLES TO CREATED PROCESSES + + On Windows systems, SCons now redefines the Python open() + and file() functions so that, if the Python Win32 extensions + are available, the file handles for any opened files will *not* + be inherited by subprocesses, such as the spawned compilers and + other tools invoked to build the software. + + This prevents certain race conditions where a file handle for + a file opened by Python (either in a Python function action, + or directly in a SConscript file) could be inherited and help + open by a subprocess, interfering with the ability of other + processes to create or modify the file. + + In general, this should not cause problems for the vast majority + of configurations. The only time this would be a problem would be + in the unlikely event that a process spawned by SCons specifically + *expected* to use an inherited file handle opened by SCons. + + If the Python Win32 extensions are not installed or are an + earlier version that does not have the ability to disable file + handle inheritance, SCons will print a warning message when the + -j option is used. The warning message may be suppressed by + specifying --warn=no-parallel-support. + Please note the following important changes since release 0.97.0d20070809: -- "content" SIGNATURES ARE NOW THE DEFAULT BEHAVIOR @@ -433,9 +461,9 @@ RELEASE 0.97.0d200709XX - XXX You can guarantee that a list will be updated in place regardless of which SConscript file defines it and which adds to it by - using the list append() method as follows: + using the list extend() method as follows: - obj.append(env.Object('foo.c')) + obj.extend(env.Object('foo.c')) Please note the following important changes since release 0.96.1: diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index bdedc99..c2c1158 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -330,7 +330,14 @@ class _ActionAction(ActionBase): os.chdir(chdir) try: stat = self.execute(target, source, env) - stat = exitstatfunc(stat) + if isinstance(stat, SCons.Errors.BuildError): + s = exitstatfunc(stat.status) + if s: + stat.status = s + else: + stat = s + else: + stat = exitstatfunc(stat) finally: if save_cwd: os.chdir(save_cwd) @@ -478,7 +485,11 @@ class CommandAction(_ActionAction): cmd_line = escape_list(cmd_line, escape) result = spawn(shell, escape, cmd_line[0], cmd_line, ENV) if not ignore and result: - return result + msg = "Error %s" % result + return SCons.Errors.BuildError(errstr=msg, + status=result, + action=self, + command=cmd_line) return 0 def get_contents(self, target, source, env): @@ -689,9 +700,19 @@ class FunctionAction(_ActionAction): # target file will appear). try: filename = e.filename except AttributeError: filename = None - raise SCons.Errors.BuildError(node=target, - errstr=e.strerror, - filename=filename) + result = SCons.Errors.BuildError(node=target, + errstr=e.strerror, + status=1, + filename=filename, + action=self, + command=self.strfunction(target, source, env)) + else: + if result: + msg = "Error %s" % result + result = SCons.Errors.BuildError(errstr=msg, + status=result, + action=self, + command=self.strfunction(target, source, env)) return result def get_contents(self, target, source, env): @@ -822,8 +843,9 @@ class ActionCaller: # was called by using this hard-coded value as a special return. if s == '$__env__': return env - else: + elif SCons.Util.is_String(s): return env.subst(s, 0, target, source) + return self.parent.convert(s) def subst_args(self, target, source, env): return map(lambda x, self=self, t=target, s=source, e=env: self.subst(x, t, s, e), @@ -853,9 +875,10 @@ class ActionFactory: called with and give them to the ActionCaller object we create, so it can hang onto them until it needs them. """ - def __init__(self, actfunc, strfunc): + def __init__(self, actfunc, strfunc, convert=lambda x: x): self.actfunc = actfunc self.strfunc = strfunc + self.convert = convert def __call__(self, *args, **kw): ac = ActionCaller(self, args, kw) action = Action(ac, strfunction=ac.strfunction) diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index 01d0992..06030e3 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -430,7 +430,7 @@ class _ActionActionTestCase(unittest.TestCase): sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env) - assert result == 7, result + assert result.status == 7, result s = sio.getvalue() assert s == 'execfunc(["out"], ["in"])\n', s @@ -440,14 +440,14 @@ class _ActionActionTestCase(unittest.TestCase): sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == expect % (repr('xyz'), repr(test.workpath())), s sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env, chdir='sub') - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == expect % (repr('sub'), repr(test.workpath())), s @@ -456,7 +456,7 @@ class _ActionActionTestCase(unittest.TestCase): sio = StringIO.StringIO() sys.stdout = sio result = b("out", "in", env) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'firstfunc(["out"], ["in"])\nexecfunc(["out"], ["in"])\n', s @@ -482,35 +482,35 @@ class _ActionActionTestCase(unittest.TestCase): sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'Building out with action:\n execfunc(target, source, env)\nexecfunc(["out"], ["in"])\n', s sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env, presub=0) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'execfunc(["out"], ["in"])\n', s sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env, presub=1) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'Building out with action:\n execfunc(target, source, env)\nexecfunc(["out"], ["in"])\n', s sio = StringIO.StringIO() sys.stdout = sio result = b(["out"], "in", env, presub=1) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'Building out with action:\n firstfunc(target, source, env)\nfirstfunc(["out"], ["in"])\nBuilding out with action:\n execfunc(target, source, env)\nexecfunc(["out"], ["in"])\n', s sio = StringIO.StringIO() sys.stdout = sio result = b(["out", "list"], "in", env, presub=1) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'Building out and list with action:\n firstfunc(target, source, env)\nfirstfunc(["out", "list"], ["in"])\nBuilding out and list with action:\n execfunc(target, source, env)\nexecfunc(["out", "list"], ["in"])\n', s @@ -519,14 +519,14 @@ class _ActionActionTestCase(unittest.TestCase): sio = StringIO.StringIO() sys.stdout = sio result = a2("out", "in", env) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'Building out with action:\n execfunc(target, source, env)\nexecfunc(["out"], ["in"])\n', s sio = StringIO.StringIO() sys.stdout = sio result = a2("out", "in", env, presub=0) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == 'execfunc(["out"], ["in"])\n', s @@ -542,7 +542,7 @@ class _ActionActionTestCase(unittest.TestCase): sio = StringIO.StringIO() sys.stdout = sio result = a("out", "in", env, presub=0, execute=1, show=0) - assert result == 7, result + assert result.status == 7, result.status s = sio.getvalue() assert s == '', s @@ -558,7 +558,7 @@ class _ActionActionTestCase(unittest.TestCase): assert exitstatfunc_result == [], exitstatfunc_result result = a("out", "in", env, execute=1, exitstatfunc=exitstatfunc) - assert result == 7, result + assert result.status == 7, result.status assert exitstatfunc_result == [7], exitstatfunc_result SCons.Action.execute_actions = 1 @@ -709,7 +709,7 @@ class CommandActionTestCase(unittest.TestCase): m = 'Invalid command display variable' assert string.find(s, m) != -1, 'Unexpected string: %s' % s else: - raise "did not catch expected UserError" + raise Exception, "did not catch expected UserError" def test___str__(self): """Test fetching the pre-substitution string for command Actions @@ -1014,26 +1014,26 @@ class CommandActionTestCase(unittest.TestCase): # Test that a nonexistent command returns 127 act = SCons.Action.CommandAction(python + "_no_such_command_") r = act([], [], env.Clone(out = outfile)) - assert r == expect_nonexistent, "r == %d" % r + assert r.status == expect_nonexistent, r.status # Test that trying to execute a directory returns 126 dir, tail = os.path.split(python) act = SCons.Action.CommandAction(dir) r = act([], [], env.Clone(out = outfile)) - assert r == expect_nonexecutable, "r == %d" % r + assert r.status == expect_nonexecutable, r.status # Test that trying to execute a non-executable file returns 126 act = SCons.Action.CommandAction(outfile) r = act([], [], env.Clone(out = outfile)) - assert r == expect_nonexecutable, "r == %d" % r + assert r.status == expect_nonexecutable, r.status act = SCons.Action.CommandAction('%s %s 1' % (_python_, exit_py)) r = act([], [], env) - assert r == 1, r + assert r.status == 1, r.status act = SCons.Action.CommandAction('@%s %s 1' % (_python_, exit_py)) r = act([], [], env) - assert r == 1, r + assert r.status == 1, r.status act = SCons.Action.CommandAction('@-%s %s 1' % (_python_, exit_py)) r = act([], [], env) @@ -1045,7 +1045,7 @@ class CommandActionTestCase(unittest.TestCase): act = SCons.Action.CommandAction('@ %s %s 1' % (_python_, exit_py)) r = act([], [], env) - assert r == 1, r + assert r.status == 1, r.status act = SCons.Action.CommandAction('@- %s %s 1' % (_python_, exit_py)) r = act([], [], env) @@ -1441,13 +1441,10 @@ class FunctionActionTestCase(unittest.TestCase): return 1 act = SCons.Action.FunctionAction(function1) - r = None - try: - r = act(target = [outfile, outfile2], source=[], env=Environment()) - except SCons.Errors.BuildError: - pass - assert r == 1 - assert count == 1 + r = act(target = [outfile, outfile2], source=[], env=Environment()) + assert r.status == 1, r.status + + assert count == 1, count c = test.read(outfile, 'r') assert c == "function1\n", c c = test.read(outfile2, 'r') @@ -1459,7 +1456,7 @@ class FunctionActionTestCase(unittest.TestCase): act = SCons.Action.FunctionAction(class1a) r = act([], [], Environment(out = outfile)) - assert r.__class__ == class1a + assert isinstance(r.status, class1a), r.status c = test.read(outfile, 'r') assert c == "class1a\n", c @@ -1470,7 +1467,7 @@ class FunctionActionTestCase(unittest.TestCase): act = SCons.Action.FunctionAction(class1b()) r = act([], [], Environment(out = outfile)) - assert r == 2 + assert r.status == 2, r.status c = test.read(outfile, 'r') assert c == "class1b\n", c @@ -1611,7 +1608,7 @@ class ListActionTestCase(unittest.TestCase): open(env['out'], 'a').write("class2b\n") act = SCons.Action.ListAction([cmd2, function2, class2a(), class2b]) r = act([], [], Environment(out = outfile)) - assert r.__class__ == class2b + assert isinstance(r.status, class2b), r.status c = test.read(outfile, 'r') assert c == "act.py: 'syzygy'\nfunction2\nclass2a\nclass2b\n", c diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py index 6164a55..4021f2b 100644 --- a/src/engine/SCons/Builder.py +++ b/src/engine/SCons/Builder.py @@ -554,7 +554,7 @@ class BuilderBase: if not tgt is None: tgt = [tgt] if not src is None: src = [src] result.extend(self._execute(env, tgt, src, overwarn)) - return result + return SCons.Node.NodeList(result) overwarn.warn() diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py index bc4c52d..cf13025 100644 --- a/src/engine/SCons/BuilderTests.py +++ b/src/engine/SCons/BuilderTests.py @@ -697,14 +697,32 @@ class BuilderTestCase(unittest.TestCase): single_source = 1, suffix='.out') env['CNT'] = [0] tgt = builder(env, target=outfiles[0], source=infiles[0])[0] + s = str(tgt) + assert s == test.workpath('0.out'), s tgt.prepare() tgt.build() assert env['CNT'][0] == 1, env['CNT'][0] tgt = builder(env, outfiles[1], infiles[1])[0] + s = str(tgt) + assert s == test.workpath('1.out'), s tgt.prepare() tgt.build() assert env['CNT'][0] == 2 tgts = builder(env, None, infiles[2:4]) + try: + [].extend(UserList.UserList()) + except TypeError: + # Old Python version (1.5.2) that can't handle extending + # a list with list-like objects. That means the return + # value from the builder call is a real list with Nodes, + # and doesn't have a __str__() method that stringifies + # the individual elements. Since we're gong to drop 1.5.2 + # support anyway, don't bother trying to test for it. + pass + else: + s = str(tgts) + expect = str([test.workpath('2.out'), test.workpath('3.out')]) + assert s == expect, s for t in tgts: t.prepare() tgts[0].build() tgts[1].build() diff --git a/src/engine/SCons/CacheDir.py b/src/engine/SCons/CacheDir.py index e3730a4..9b2b4b4 100644 --- a/src/engine/SCons/CacheDir.py +++ b/src/engine/SCons/CacheDir.py @@ -51,7 +51,7 @@ def CacheRetrieveFunc(target, source, env): if fs.islink(cachefile): fs.symlink(fs.readlink(cachefile), t.path) else: - fs.copy2(cachefile, t.path) + env.copy_from_cache(cachefile, t.path) st = fs.stat(cachefile) fs.chmod(t.path, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE) return 0 diff --git a/src/engine/SCons/Conftest.py b/src/engine/SCons/Conftest.py index bb3be56..fcf8c5a 100644 --- a/src/engine/SCons/Conftest.py +++ b/src/engine/SCons/Conftest.py @@ -318,6 +318,102 @@ int main() { return ret +def CheckTypeSize(context, type_name, header = None, language = None, expect = None): + """This check can be used to get the size of a given type, or to check whether + the type is of expected size. + + Arguments: + - type : str + the type to check + - includes : sequence + list of headers to include in the test code before testing the type + - language : str + 'C' or 'C++' + - expect : int + if given, will test wether the type has the given number of bytes. + If not given, will automatically find the size. + + Returns: + status : int + 0 if the check failed, or the found size of the type if the check succeeded.""" + + # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H. + if context.headerfilename: + includetext = '#include "%s"' % context.headerfilename + else: + includetext = '' + + if not header: + header = "" + + lang, suffix, msg = _lang2suffix(language) + if msg: + context.Display("Cannot check for %s type: %s\n" % (type_name, msg)) + return msg + + src = includetext + header + if not expect is None: + # Only check if the given size is the right one + context.Display('Checking %s is %d bytes... ' % (type_name, expect)) + + # test code taken from autoconf: this is a pretty clever hack to find that + # a type is of a given size using only compilation. This speeds things up + # quite a bit compared to straightforward code using TryRun + src = src + r""" +typedef %s scons_check_type; + +int main() +{ + static int test_array[1 - 2 * !(((long int) (sizeof(scons_check_type))) == %d)]; + test_array[0] = 0; + + return 0; +} +""" + + # XXX: Try* vs CompileProg ? + st = context.TryCompile(src % (type_name, expect), suffix) + if st: + _Have(context, "SIZEOF_" + type_name, str(expect)) + context.Display("yes\n") + return expect + else: + context.Display("no\n") + _LogFailed(context, src, st) + return 0 + else: + # Only check if the given size is the right one + context.Message('Checking size of %s ... ' % type_name) + + # We have to be careful with the program we wish to test here since + # compilation will be attempted using the current environment's flags. + # So make sure that the program will compile without any warning. For + # example using: 'int main(int argc, char** argv)' will fail with the + # '-Wall -Werror' flags since the variables argc and argv would not be + # used in the program... + # + src = src + """ +#include <stdlib.h> +#include <stdio.h> +int main() { + printf("%d", (int)sizeof(""" + type_name + """)); + return 0; +} + """ + ret = context.TryRun(src, suffix) + st = ret[0] + try: + size = int(ret[1]) + _Have(context, "SIZEOF_" + type_name, str(size)) + context.Display("%d\n" % size) + except ValueError: + size = 0 + _LogFailed(context, src, st) + context.Display(" Failed !\n") + if st: + return size + else: + return 0 def CheckLib(context, libs, func_name = None, header = None, extra_libs = None, call = None, language = None, autoadd = 1): diff --git a/src/engine/SCons/Defaults.py b/src/engine/SCons/Defaults.py index 9308051..c3d30cb 100644 --- a/src/engine/SCons/Defaults.py +++ b/src/engine/SCons/Defaults.py @@ -87,11 +87,10 @@ def DefaultEnvironment(*args, **kw): if not _default_env: import SCons.Util _default_env = apply(SCons.Environment.Environment, args, kw) - _default_env.TargetSignatures('source') if SCons.Util.md5: - _default_env.SourceSignatures('MD5') + _default_env.Decider('MD5') else: - _default_env.SourceSignatures('timestamp') + _default_env.Decider('timestamp-match') global DefaultEnvironment DefaultEnvironment = _fetch_DefaultEnvironment _default_env._CacheDir = SCons.CacheDir.Null() @@ -158,7 +157,10 @@ LdModuleLinkAction = SCons.Action.Action("$LDMODULECOM", "$LDMODULECOMSTR") # ways by creating ActionFactory instances. ActionFactory = SCons.Action.ActionFactory -Chmod = ActionFactory(os.chmod, +def chmod_func(path, mode): + return os.chmod(str(path), mode) + +Chmod = ActionFactory(chmod_func, lambda dest, mode: 'Chmod("%s", 0%o)' % (dest, mode)) def copy_func(dest, src): @@ -172,9 +174,11 @@ def copy_func(dest, src): return shutil.copytree(src, dest, 1) Copy = ActionFactory(copy_func, - lambda dest, src: 'Copy("%s", "%s")' % (dest, src)) + lambda dest, src: 'Copy("%s", "%s")' % (dest, src), + convert=str) def delete_func(entry, must_exist=0): + entry = str(entry) if not must_exist and not os.path.exists(entry): return None if not os.path.exists(entry) or os.path.isfile(entry): @@ -188,12 +192,15 @@ def delete_strfunc(entry, must_exist=0): Delete = ActionFactory(delete_func, delete_strfunc) Mkdir = ActionFactory(os.makedirs, - lambda dir: 'Mkdir("%s")' % dir) + lambda dir: 'Mkdir("%s")' % dir, + convert=str) Move = ActionFactory(lambda dest, src: os.rename(src, dest), - lambda dest, src: 'Move("%s", "%s")' % (dest, src)) + lambda dest, src: 'Move("%s", "%s")' % (dest, src), + convert=str) def touch_func(file): + file = str(file) mtime = int(time.time()) if os.path.exists(file): atime = os.path.getatime(file) diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index 2f4c34e..cf2d0eb 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -216,6 +216,9 @@ class BuilderWrapper(MethodWrapper): def __repr__(self): return '<BuilderWrapper %s>' % repr(self.name) + def __str__(self): + return self.__repr__() + def __getattr__(self, name): if name == 'env': return self.object @@ -259,6 +262,12 @@ class BuilderDict(UserDict): return self.__class__(self.data, self.env) def __setitem__(self, item, val): + try: + method = getattr(self.env, item).method + except AttributeError: + pass + else: + self.env.RemoveMethod(method) UserDict.__setitem__(self, item, val) BuilderWrapper(self.env, val, item) @@ -773,6 +782,10 @@ def default_decide_target(dependency, target, prev_ni): f = SCons.Defaults.DefaultEnvironment().decide_target return f(dependency, target, prev_ni) +def default_copy_from_cache(src, dst): + f = SCons.Defaults.DefaultEnvironment().copy_from_cache + return f(src, dst) + class Base(SubstitutionEnvironment): """Base class for "real" construction Environments. These are the primary objects used to communicate dependency and construction @@ -836,6 +849,8 @@ class Base(SubstitutionEnvironment): self.decide_target = default_decide_target self.decide_source = default_decide_source + self.copy_from_cache = default_copy_from_cache + self._dict['BUILDERS'] = BuilderDict(self._dict['BUILDERS'], self) if platform is None: @@ -1137,7 +1152,7 @@ class Base(SubstitutionEnvironment): clone.added_methods = [] for mw in self.added_methods: - mw.clone(clone) + clone.added_methods.append(mw.clone(clone)) clone._memo = {} @@ -1185,7 +1200,14 @@ class Base(SubstitutionEnvironment): def _changed_timestamp_match(self, dependency, target, prev_ni): return dependency.changed_timestamp_match(target, prev_ni) + def _copy_from_cache(self, src, dst): + return self.fs.copy(src, dst) + + def _copy2_from_cache(self, src, dst): + return self.fs.copy2(src, dst) + def Decider(self, function): + copy_function = self._copy2_from_cache if function in ('MD5', 'content'): if not SCons.Util.md5: raise UserError, "MD5 signatures are not available in this version of Python." @@ -1194,6 +1216,7 @@ class Base(SubstitutionEnvironment): function = self._changed_timestamp_then_content elif function in ('timestamp-newer', 'make'): function = self._changed_timestamp_newer + copy_function = self._copy_from_cache elif function == 'timestamp-match': function = self._changed_timestamp_match elif not callable(function): @@ -1205,6 +1228,8 @@ class Base(SubstitutionEnvironment): self.decide_target = function self.decide_source = function + self.copy_from_cache = copy_function + def Detect(self, progs): """Return the first available program in progs. """ @@ -1706,7 +1731,11 @@ class Base(SubstitutionEnvironment): """Directly execute an action through an Environment """ action = apply(self.Action, (action,) + args, kw) - return action([], [], self) + result = action([], [], self) + if isinstance(result, SCons.Errors.BuildError): + return result.status + else: + return result def File(self, name, *args, **kw): """ @@ -1728,6 +1757,9 @@ class Base(SubstitutionEnvironment): else: return result[0] + def Glob(self, pattern, ondisk=True, source=False, strings=False): + return self.fs.Glob(self.subst(pattern), ondisk, source, strings) + def Ignore(self, target, dependency): """Ignore a dependency.""" tlist = self.arg2nodes(target, self.fs.Entry) @@ -1763,6 +1795,16 @@ class Base(SubstitutionEnvironment): dirs = self.arg2nodes(list(dirs), self.fs.Dir) apply(self.fs.Repository, dirs, kw) + def Requires(self, target, prerequisite): + """Specify that 'prerequisite' must be built before 'target', + (but 'target' does not actually depend on 'prerequisite' + and need not be rebuilt if it changes).""" + tlist = self.arg2nodes(target, self.fs.Entry) + plist = self.arg2nodes(prerequisite, self.fs.Entry) + for t in tlist: + t.add_prerequisite(plist) + return tlist + def Scanner(self, *args, **kw): nargs = [] for arg in args: @@ -1810,7 +1852,7 @@ class Base(SubstitutionEnvironment): raise UserError, "MD5 signatures are not available in this version of Python." self.decide_source = self._changed_content elif type == 'timestamp': - self.decide_source = self._changed_timestamp_newer + self.decide_source = self._changed_timestamp_match else: raise UserError, "Unknown source signature type '%s'" % type @@ -1840,7 +1882,7 @@ class Base(SubstitutionEnvironment): raise UserError, "MD5 signatures are not available in this version of Python." self.decide_target = self._changed_content elif type == 'timestamp': - self.decide_target = self._changed_timestamp_newer + self.decide_target = self._changed_timestamp_match elif type == 'build': self.decide_target = self._changed_build elif type == 'source': diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py index 630f594..3f64d43 100644 --- a/src/engine/SCons/EnvironmentTests.py +++ b/src/engine/SCons/EnvironmentTests.py @@ -674,6 +674,21 @@ sys.exit(1) r = env.func('-yyy') assert r == 'func2-foo-yyy', r + # Test that clones of clones correctly re-bind added methods. + env1 = Environment(FOO = '1') + env1.AddMethod(func2) + env2 = env1.Clone(FOO = '2') + env3 = env2.Clone(FOO = '3') + env4 = env3.Clone(FOO = '4') + r = env1.func2() + assert r == 'func2-1', r + r = env2.func2() + assert r == 'func2-2', r + r = env3.func2() + assert r == 'func2-3', r + r = env4.func2() + assert r == 'func2-4', r + def test_Override(self): "Test overriding construction variables" env = SubstitutionEnvironment(ONE=1, TWO=2, THREE=3, FOUR=4) diff --git a/src/engine/SCons/Errors.py b/src/engine/SCons/Errors.py index 3ee7ff4..fc55cf4 100644 --- a/src/engine/SCons/Errors.py +++ b/src/engine/SCons/Errors.py @@ -33,10 +33,16 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" class BuildError(Exception): - def __init__(self, node=None, errstr="Unknown error", filename=None, *args): + def __init__(self, node=None, errstr="Unknown error", status=0, + filename=None, executor=None, action=None, command=None, + *args): self.node = node self.errstr = errstr + self.status = status self.filename = filename + self.executor = executor + self.action = action + self.command = command apply(Exception.__init__, (self,) + args) class InternalError(Exception): diff --git a/src/engine/SCons/Executor.py b/src/engine/SCons/Executor.py index 88a46cc..1cb0cf9 100644 --- a/src/engine/SCons/Executor.py +++ b/src/engine/SCons/Executor.py @@ -33,6 +33,7 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import string from SCons.Debug import logInstanceCreation +import SCons.Errors import SCons.Memoize @@ -59,6 +60,7 @@ class Executor: self.overridelist = overridelist self.targets = targets self.sources = sources[:] + self.sources_need_sorting = False self.builder_kw = builder_kw self._memo = {} @@ -110,7 +112,7 @@ class Executor: cwd = self.targets[0].cwd except (IndexError, AttributeError): cwd = None - return scanner.path(env, cwd, self.targets, self.sources) + return scanner.path(env, cwd, self.targets, self.get_sources()) def get_kw(self, kw={}): result = self.builder_kw.copy() @@ -126,9 +128,13 @@ class Executor: kw = self.get_kw(kw) status = 0 for act in self.get_action_list(): - status = apply(act, (self.targets, self.sources, env), kw) - if status: - break + status = apply(act, (self.targets, self.get_sources(), env), kw) + if isinstance(status, SCons.Errors.BuildError): + status.executor = self + raise status + elif status: + msg = "Error %s" % status + raise SCons.Errors.BuildError(errstr=msg, executor=self, action=act) return status # use extra indirection because with new-style objects (Python 2.2 @@ -145,8 +151,14 @@ class Executor: """Add source files to this Executor's list. This is necessary for "multi" Builders that can be called repeatedly to build up a source file list for a given target.""" - slist = filter(lambda x, s=self.sources: x not in s, sources) - self.sources.extend(slist) + self.sources.extend(sources) + self.sources_need_sorting = True + + def get_sources(self): + if self.sources_need_sorting: + self.sources = SCons.Util.uniquer_hashables(self.sources) + self.sources_need_sorting = False + return self.sources def add_pre_action(self, action): self.pre_actions.append(action) @@ -158,7 +170,7 @@ class Executor: def my_str(self): env = self.get_build_env() - get = lambda action, t=self.targets, s=self.sources, e=env: \ + get = lambda action, t=self.targets, s=self.get_sources(), e=env: \ action.genstring(t, s, e) return string.join(map(get, self.get_action_list()), "\n") @@ -183,7 +195,7 @@ class Executor: except KeyError: pass env = self.get_build_env() - get = lambda action, t=self.targets, s=self.sources, e=env: \ + get = lambda action, t=self.targets, s=self.get_sources(), e=env: \ action.get_contents(t, s, e) result = string.join(map(get, self.get_action_list()), "") self._memo['get_contents'] = result @@ -201,7 +213,7 @@ class Executor: def scan_sources(self, scanner): if self.sources: - self.scan(scanner, self.sources) + self.scan(scanner, self.get_sources()) def scan(self, scanner, node_list): """Scan a list of this Executor's files (targets or sources) for @@ -241,7 +253,7 @@ class Executor: def get_missing_sources(self): """ """ - return filter(lambda s: s.missing(), self.sources) + return filter(lambda s: s.missing(), self.get_sources()) def _get_unignored_sources_key(self, ignore=()): return tuple(ignore) @@ -261,9 +273,12 @@ class Executor: except KeyError: pass - sourcelist = self.sources + sourcelist = self.get_sources() if ignore: - sourcelist = filter(lambda s, i=ignore: not s in i, sourcelist) + idict = {} + for i in ignore: + idict[i] = 1 + sourcelist = filter(lambda s, i=idict: not i.has_key(s), sourcelist) memo_dict[ignore] = sourcelist @@ -299,7 +314,7 @@ class Executor: result = [] build_env = self.get_build_env() for act in self.get_action_list(): - result.extend(act.get_implicit_deps(self.targets, self.sources, build_env)) + result.extend(act.get_implicit_deps(self.targets, self.get_sources(), build_env)) return result diff --git a/src/engine/SCons/ExecutorTests.py b/src/engine/SCons/ExecutorTests.py index 59deca5..368e034 100644 --- a/src/engine/SCons/ExecutorTests.py +++ b/src/engine/SCons/ExecutorTests.py @@ -236,7 +236,12 @@ class ExecutorTestCase(unittest.TestCase): x = SCons.Executor.Executor(a, env, [], t, ['s1', 's2']) x.add_pre_action(pre_err) x.add_post_action(post) - x(t) + try: + x(t) + except SCons.Errors.BuildError: + pass + else: + raise Exception, "Did not catch expected BuildError" assert result == ['pre_err'], result del result[:] @@ -265,8 +270,19 @@ class ExecutorTestCase(unittest.TestCase): x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2']) assert x.sources == ['s1', 's2'], x.sources x.add_sources(['s1', 's2']) + assert x.sources == ['s1', 's2', 's1', 's2'], x.sources + x.add_sources(['s3', 's1', 's4']) + assert x.sources == ['s1', 's2', 's1', 's2', 's3', 's1', 's4'], x.sources + + def test_get_sources(self): + """Test getting sources from an Executor""" + x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2']) + assert x.sources == ['s1', 's2'], x.sources + x.add_sources(['s1', 's2']) + x.get_sources() assert x.sources == ['s1', 's2'], x.sources x.add_sources(['s3', 's1', 's4']) + x.get_sources() assert x.sources == ['s1', 's2', 's3', 's4'], x.sources def test_add_pre_action(self): diff --git a/src/engine/SCons/Node/Alias.py b/src/engine/SCons/Node/Alias.py index 15de664..bb23d3f 100644 --- a/src/engine/SCons/Node/Alias.py +++ b/src/engine/SCons/Node/Alias.py @@ -91,9 +91,9 @@ class Alias(SCons.Node.Node): def get_contents(self): """The contents of an alias is the concatenation - of all the contents of its sources""" - contents = map(lambda n: n.get_contents(), self.children()) - return string.join(contents, '') + of the content signatures of all its sources.""" + childsigs = map(lambda n: n.get_csig(), self.children()) + return string.join(childsigs, '') def sconsign(self): """An Alias is not recorded in .sconsign files""" @@ -133,7 +133,7 @@ class Alias(SCons.Node.Node): return self.ninfo.csig except AttributeError: pass - + contents = self.get_contents() csig = SCons.Util.MD5signature(contents) self.get_ninfo().csig = csig diff --git a/src/engine/SCons/Node/AliasTests.py b/src/engine/SCons/Node/AliasTests.py index 755cf75..02488f0 100644 --- a/src/engine/SCons/Node/AliasTests.py +++ b/src/engine/SCons/Node/AliasTests.py @@ -54,6 +54,8 @@ class AliasTestCase(unittest.TestCase): class DummyNode: def __init__(self, contents): self.contents = contents + def get_csig(self): + return self.contents def get_contents(self): return self.contents diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index 964af62..d0843d1 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -35,8 +35,10 @@ that can be used by scripts or modules looking for the canonical default. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +import fnmatch import os import os.path +import re import shutil import stat import string @@ -85,6 +87,42 @@ def save_strings(val): Save_Strings = val # +# Avoid unnecessary function calls by recording a Boolean value that +# tells us whether or not os.path.splitdrive() actually does anything +# on this system, and therefore whether we need to bother calling it +# when looking up path names in various methods below. +# + +do_splitdrive = None + +def initialize_do_splitdrive(): + global do_splitdrive + drive, path = os.path.splitdrive('X:/foo') + do_splitdrive = not not drive + +initialize_do_splitdrive() + +# + +needs_normpath_check = None + +def initialize_normpath_check(): + """ + Initialize the normpath_check regular expression. + + This function is used by the unit tests to re-initialize the pattern + when testing for behavior with different values of os.sep. + """ + global needs_normpath_check + if os.sep == '/': + pattern = r'.*/|\.$|\.\.$' + else: + pattern = r'.*[/%s]|\.$|\.\.$' % re.escape(os.sep) + needs_normpath_check = re.compile(pattern) + +initialize_normpath_check() + +# # SCons.Action objects for interacting with the outside world. # # The Node.FS methods in this module should use these actions to @@ -544,9 +582,29 @@ class Base(SCons.Node.Node): return result def _get_str(self): + global Save_Strings if self.duplicate or self.is_derived(): return self.get_path() - return self.srcnode().get_path() + srcnode = self.srcnode() + if srcnode.stat() is None and not self.stat() is None: + result = self.get_path() + else: + result = srcnode.get_path() + if not Save_Strings: + # We're not at the point where we're saving the string string + # representations of FS Nodes (because we haven't finished + # reading the SConscript files and need to have str() return + # things relative to them). That also means we can't yet + # cache values returned (or not returned) by stat(), since + # Python code in the SConscript files might still create + # or otherwise affect the on-disk file. So get rid of the + # values that the underlying stat() method saved. + try: del self._memo['stat'] + except KeyError: pass + if not self is srcnode: + try: del srcnode._memo['stat'] + except KeyError: pass + return result rstr = __str__ @@ -607,15 +665,11 @@ class Base(SCons.Node.Node): corresponding to its source file. Otherwise, return ourself. """ - dir=self.dir - name=self.name - while dir: - if dir.srcdir: - srcnode = dir.srcdir.Entry(name) - srcnode.must_be_same(self.__class__) - return srcnode - name = dir.name + os.sep + name - dir = dir.up() + srcdir_list = self.dir.srcdir_list() + if srcdir_list: + srcnode = srcdir_list[0].Entry(self.name) + srcnode.must_be_same(self.__class__) + return srcnode return self def get_path(self, dir=None): @@ -673,7 +727,7 @@ class Base(SCons.Node.Node): def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext): """ - Generates a target entry that corresponds to this entry (usually + Generates a target entry that corresponds to this entry (usually a source file) with the specified prefix and suffix. Note that this method can be overridden dynamically for generated @@ -745,6 +799,9 @@ class Base(SCons.Node.Node): self._memo['rentry'] = result return result + def _glob1(self, pattern, ondisk=True, source=False, strings=False): + return [] + class Entry(Base): """This is the class for generic Node.FS entries--that is, things that could be a File or a Dir, but we're just not sure yet. @@ -845,11 +902,11 @@ class Entry(Base): directory.""" return self.disambiguate().exists() -# def rel_path(self, other): -# d = self.disambiguate() -# if d.__class__ == Entry: -# raise "rel_path() could not disambiguate File/Dir" -# return d.rel_path(other) + def rel_path(self, other): + d = self.disambiguate() + if d.__class__ == Entry: + raise "rel_path() could not disambiguate File/Dir" + return d.rel_path(other) def new_ninfo(self): return self.disambiguate().new_ninfo() @@ -857,6 +914,9 @@ class Entry(Base): def changed_since_last_build(self, target, prev_ni): return self.disambiguate().changed_since_last_build(target, prev_ni) + def _glob1(self, pattern, ondisk=True, source=False, strings=False): + return self.disambiguate()._glob1(pattern, ondisk, source, strings) + # This is for later so we can differentiate between Entry the class and Entry # the method of the FS class. _classEntry = Entry @@ -885,6 +945,8 @@ class LocalFS: # return os.chdir(path) def chmod(self, path, mode): return os.chmod(path, mode) + def copy(self, src, dst): + return shutil.copy(src, dst) def copy2(self, src, dst): return shutil.copy2(src, dst) def exists(self, path): @@ -975,8 +1037,8 @@ class FS(LocalFS): self.Top.tpath = '.' self._cwd = self.Top - DirNodeInfo.top = self.Top - FileNodeInfo.top = self.Top + DirNodeInfo.fs = self + FileNodeInfo.fs = self def set_SConstruct_dir(self, dir): self.SConstruct_dir = dir @@ -1028,11 +1090,16 @@ class FS(LocalFS): This translates arbitrary input into a canonical Node.FS object of the specified fsclass. The general approach for strings is - to turn it into a normalized absolute path and then call the - root directory's lookup_abs() method for the heavy lifting. + to turn it into a fully normalized absolute path and then call + the root directory's lookup_abs() method for the heavy lifting. If the path name begins with '#', it is unconditionally - interpreted relative to the top-level directory of this FS. + interpreted relative to the top-level directory of this FS. '#' + is treated as a synonym for the top-level SConstruct directory, + much like '~' is treated as a synonym for the user's home + directory in a UNIX shell. So both '#foo' and '#/foo' refer + to the 'foo' subdirectory underneath the top-level SConstruct + directory. If the path name is relative, then the path is looked up relative to the specified directory, or the current directory (self._cwd, @@ -1046,33 +1113,53 @@ class FS(LocalFS): return p # str(p) in case it's something like a proxy object p = str(p) - drive, p = os.path.splitdrive(p) + + initial_hash = (p[0:1] == '#') + if initial_hash: + # There was an initial '#', so we strip it and override + # whatever directory they may have specified with the + # top-level SConstruct directory. + p = p[1:] + directory = self.Top + + if directory and not isinstance(directory, Dir): + directory = self.Dir(directory) + + if do_splitdrive: + drive, p = os.path.splitdrive(p) + else: + drive = '' if drive and not p: - # A drive letter without a path... + # This causes a naked drive letter to be treated as a synonym + # for the root directory on that drive. p = os.sep - root = self.get_root(drive) - elif os.path.isabs(p): - # An absolute path... + absolute = os.path.isabs(p) + + needs_normpath = needs_normpath_check.match(p) + + if initial_hash or not absolute: + # This is a relative lookup, either to the top-level + # SConstruct directory (because of the initial '#') or to + # the current directory (the path name is not absolute). + # Add the string to the appropriate directory lookup path, + # after which the whole thing gets normalized. + if not directory: + directory = self._cwd + if p: + p = directory.labspath + '/' + p + else: + p = directory.labspath + + if needs_normpath: p = os.path.normpath(p) + + if drive or absolute: root = self.get_root(drive) else: - if p[0:1] == '#': - # A top-relative path... - directory = self.Top - offset = 1 - if p[1:2] in(os.sep, '/'): - offset = 2 - p = p[offset:] - else: - # A relative path... - if not directory: - # ...to the current (SConscript) directory. - directory = self._cwd - elif not isinstance(directory, Dir): - # ...to the specified directory. - directory = self.Dir(directory) - p = os.path.normpath(directory.labspath + '/' + p) + if not directory: + directory = self._cwd root = directory.root + if os.sep != '/': p = string.replace(p, os.sep, '/') return root._lookup_abs(p, fsclass, create) @@ -1098,7 +1185,7 @@ class FS(LocalFS): """ return self._lookup(name, directory, File, create) - def Dir(self, name, directory = None, create = 1): + def Dir(self, name, directory = None, create = True): """Lookup or create a Dir node with the specified name. If the name is a relative path (begins with ./, ../, or a file name), then it is looked up relative to the supplied directory node, @@ -1160,24 +1247,41 @@ class FS(LocalFS): message = fmt % string.join(map(str, targets)) return targets, message + def Glob(self, pathname, ondisk=True, source=True, strings=False, cwd=None): + """ + Globs + + This is mainly a shim layer + """ + if cwd is None: + cwd = self.getcwd() + return cwd.glob(pathname, ondisk, source, strings) + class DirNodeInfo(SCons.Node.NodeInfoBase): # This should get reset by the FS initialization. current_version_id = 1 - top = None + fs = None def str_to_node(self, s): - top = self.top - if os.path.isabs(s): - n = top.fs._lookup(s, top, Entry) - else: + top = self.fs.Top + root = top.root + if do_splitdrive: + drive, s = os.path.splitdrive(s) + if drive: + root = self.fs.get_root(drive) + if not os.path.isabs(s): s = top.labspath + '/' + s - n = top.root._lookup_abs(s, Entry) - return n + return root._lookup_abs(s, Entry) class DirBuildInfo(SCons.Node.BuildInfoBase): current_version_id = 1 +glob_magic_check = re.compile('[*?[]') + +def has_glob_magic(s): + return glob_magic_check.search(s) is not None + class Dir(Base): """A class for directories in a file system. """ @@ -1252,12 +1356,12 @@ class Dir(Base): """ return self.fs.Entry(name, self) - def Dir(self, name): + def Dir(self, name, create=True): """ Looks up or creates a directory node named 'name' relative to this directory. """ - dir = self.fs.Dir(name, self) + dir = self.fs.Dir(name, self, create) return dir def File(self, name): @@ -1313,7 +1417,10 @@ class Dir(Base): while dir: for rep in dir.getRepositories(): result.append(rep.Dir(fname)) - fname = dir.name + os.sep + fname + if fname == '.': + fname = dir.name + else: + fname = dir.name + os.sep + fname dir = dir.up() self._memo['get_all_rdirs'] = result @@ -1329,66 +1436,68 @@ class Dir(Base): def up(self): return self.entries['..'] -# This complicated method, which constructs relative paths between -# arbitrary Node.FS objects, is no longer used. It was introduced to -# store dependency paths in .sconsign files relative to the target, but -# that ended up being significantly inefficient. We're leaving the code -# here, commented out, because it would be too easy for someone to decide -# to re-invent this wheel in the future (if it becomes necessary) because -# they didn't know this code was buried in some source-code change from -# the distant past... -# -# def _rel_path_key(self, other): -# return str(other) -# -# memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key)) -# -# def rel_path(self, other): -# """Return a path to "other" relative to this directory. -# """ -# try: -# memo_dict = self._memo['rel_path'] -# except KeyError: -# memo_dict = {} -# self._memo['rel_path'] = memo_dict -# else: -# try: -# return memo_dict[other] -# except KeyError: -# pass -# -# if self is other: -# -# result = '.' -# -# elif not other in self.path_elements: -# -# try: -# other_dir = other.get_dir() -# except AttributeError: -# result = str(other) -# else: -# if other_dir is None: -# result = other.name -# else: -# dir_rel_path = self.rel_path(other_dir) -# if dir_rel_path == '.': -# result = other.name -# else: -# result = dir_rel_path + os.sep + other.name -# -# else: -# -# i = self.path_elements.index(other) + 1 -# -# path_elems = ['..'] * (len(self.path_elements) - i) \ -# + map(lambda n: n.name, other.path_elements[i:]) -# -# result = string.join(path_elems, os.sep) -# -# memo_dict[other] = result -# -# return result + def _rel_path_key(self, other): + return str(other) + + memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key)) + + def rel_path(self, other): + """Return a path to "other" relative to this directory. + """ + + # This complicated and expensive method, which constructs relative + # paths between arbitrary Node.FS objects, is no longer used + # by SCons itself. It was introduced to store dependency paths + # in .sconsign files relative to the target, but that ended up + # being significantly inefficient. + # + # We're continuing to support the method because some SConstruct + # files out there started using it when it was available, and + # we're all about backwards compatibility.. + + try: + memo_dict = self._memo['rel_path'] + except KeyError: + memo_dict = {} + self._memo['rel_path'] = memo_dict + else: + try: + return memo_dict[other] + except KeyError: + pass + + if self is other: + + result = '.' + + elif not other in self.path_elements: + + try: + other_dir = other.get_dir() + except AttributeError: + result = str(other) + else: + if other_dir is None: + result = other.name + else: + dir_rel_path = self.rel_path(other_dir) + if dir_rel_path == '.': + result = other.name + else: + result = dir_rel_path + os.sep + other.name + + else: + + i = self.path_elements.index(other) + 1 + + path_elems = ['..'] * (len(self.path_elements) - i) \ + + map(lambda n: n.name, other.path_elements[i:]) + + result = string.join(path_elems, os.sep) + + memo_dict[other] = result + + return result def get_env_scanner(self, env, kw={}): import SCons.Defaults @@ -1575,13 +1684,7 @@ class Dir(Base): dir = self while dir: if dir.srcdir: - d = dir.srcdir.Dir(dirname) - if d.is_under(dir): - # Shouldn't source from something in the build path: - # build_dir is probably under src_dir, in which case - # we are reflecting. - break - result.append(d) + result.append(dir.srcdir.Dir(dirname)) dirname = dir.name + os.sep + dirname dir = dir.up() @@ -1591,6 +1694,11 @@ class Dir(Base): def srcdir_duplicate(self, name): for dir in self.srcdir_list(): + if self.is_under(dir): + # We shouldn't source from something in the build path; + # build_dir is probably under src_dir, in which case + # we are reflecting. + break if dir.entry_exists_on_disk(name): srcnode = dir.Entry(name).disambiguate() if self.duplicate: @@ -1693,6 +1801,118 @@ class Dir(Base): for dirname in filter(select_dirs, names): entries[dirname].walk(func, arg) + def glob(self, pathname, ondisk=True, source=False, strings=False): + """ + Returns a list of Nodes (or strings) matching a specified + pathname pattern. + + Pathname patterns follow UNIX shell semantics: * matches + any-length strings of any characters, ? matches any character, + and [] can enclose lists or ranges of characters. Matches do + not span directory separators. + + The matches take into account Repositories, returning local + Nodes if a corresponding entry exists in a Repository (either + an in-memory Node or something on disk). + + By defafult, the glob() function matches entries that exist + on-disk, in addition to in-memory Nodes. Setting the "ondisk" + argument to False (or some other non-true value) causes the glob() + function to only match in-memory Nodes. The default behavior is + to return both the on-disk and in-memory Nodes. + + The "source" argument, when true, specifies that corresponding + source Nodes must be returned if you're globbing in a build + directory (initialized with BuildDir()). The default behavior + is to return Nodes local to the BuildDir(). + + The "strings" argument, when true, returns the matches as strings, + not Nodes. The strings are path names relative to this directory. + + The underlying algorithm is adapted from the glob.glob() function + in the Python library (but heavily modified), and uses fnmatch() + under the covers. + """ + dirname, basename = os.path.split(pathname) + if not dirname: + return self._glob1(basename, ondisk, source, strings) + if has_glob_magic(dirname): + list = self.glob(dirname, ondisk, source, strings=False) + else: + list = [self.Dir(dirname, create=True)] + result = [] + for dir in list: + r = dir._glob1(basename, ondisk, source, strings) + if strings: + r = map(lambda x, d=str(dir): os.path.join(d, x), r) + result.extend(r) + return result + + def _glob1(self, pattern, ondisk=True, source=False, strings=False): + """ + Globs for and returns a list of entry names matching a single + pattern in this directory. + + This searches any repositories and source directories for + corresponding entries and returns a Node (or string) relative + to the current directory if an entry is found anywhere. + + TODO: handle pattern with no wildcard + """ + search_dir_list = self.get_all_rdirs() + for srcdir in self.srcdir_list(): + search_dir_list.extend(srcdir.get_all_rdirs()) + + names = [] + for dir in search_dir_list: + # We use the .name attribute from the Node because the keys of + # the dir.entries dictionary are normalized (that is, all upper + # case) on case-insensitive systems like Windows. + #node_names = [ v.name for k, v in dir.entries.items() if k not in ('.', '..') ] + entry_names = filter(lambda n: n not in ('.', '..'), dir.entries.keys()) + node_names = map(lambda n, e=dir.entries: e[n].name, entry_names) + names.extend(node_names) + if ondisk: + try: + disk_names = os.listdir(dir.abspath) + except os.error: + pass + else: + names.extend(disk_names) + if not strings: + # We're going to return corresponding Nodes in + # the local directory, so we need to make sure + # those Nodes exist. We only want to create + # Nodes for the entries that will match the + # specified pattern, though, which means we + # need to filter the list here, even though + # the overall list will also be filtered later, + # after we exit this loop. + if pattern[0] != '.': + #disk_names = [ d for d in disk_names if d[0] != '.' ] + disk_names = filter(lambda x: x[0] != '.', disk_names) + disk_names = fnmatch.filter(disk_names, pattern) + rep_nodes = map(dir.Entry, disk_names) + #rep_nodes = [ n.disambiguate() for n in rep_nodes ] + rep_nodes = map(lambda n: n.disambiguate(), rep_nodes) + for node, name in zip(rep_nodes, disk_names): + n = self.Entry(name) + if n.__class__ != node.__class__: + n.__class__ = node.__class__ + n._morph() + + names = set(names) + if pattern[0] != '.': + #names = [ n for n in names if n[0] != '.' ] + names = filter(lambda x: x[0] != '.', names) + names = fnmatch.filter(names, pattern) + + if strings: + return names + + #return [ self.entries[_my_normcase(n)] for n in names ] + return map(lambda n, e=self.entries: e[_my_normcase(n)], names) + class RootDir(Dir): """A class for the root directory of a file system. @@ -1817,16 +2037,18 @@ class FileNodeInfo(SCons.Node.NodeInfoBase): field_list = ['csig', 'timestamp', 'size'] # This should get reset by the FS initialization. - top = None + fs = None def str_to_node(self, s): - top = self.top - if os.path.isabs(s): - n = top.fs._lookup(s, top, Entry) - else: + top = self.fs.Top + root = top.root + if do_splitdrive: + drive, s = os.path.splitdrive(s) + if drive: + root = self.fs.get_root(drive) + if not os.path.isabs(s): s = top.labspath + '/' + s - n = top.root._lookup_abs(s, Entry) - return n + return root._lookup_abs(s, Entry) class FileBuildInfo(SCons.Node.BuildInfoBase): current_version_id = 1 @@ -1925,10 +2147,10 @@ class File(Base): the SConscript directory of this file.""" return self.cwd.Entry(name) - def Dir(self, name): + def Dir(self, name, create=True): """Create a directory node named 'name' relative to the SConscript directory of this file.""" - return self.cwd.Dir(name) + return self.cwd.Dir(name, create) def Dirs(self, pathlist): """Create a list of directories relative to the SConscript @@ -2171,8 +2393,8 @@ class File(Base): try: return binfo.bimplicit except AttributeError: return None -# def rel_path(self, other): -# return self.dir.rel_path(other) + def rel_path(self, other): + return self.dir.rel_path(other) def _get_found_includes_key(self, env, scanner, path): return (id(env), id(scanner), path) @@ -2329,7 +2551,9 @@ class File(Base): def _rmv_existing(self): self.clear_memoized_values() - Unlink(self, [], None) + e = Unlink(self, [], None) + if isinstance(e, SCons.Errors.BuildError): + raise e # # Taskmaster interface subsystem @@ -2367,13 +2591,9 @@ class File(Base): def do_duplicate(self, src): self._createDir() - try: - Unlink(self, None, None) - except SCons.Errors.BuildError: - pass - try: - Link(self, src, None) - except SCons.Errors.BuildError, e: + Unlink(self, None, None) + e = Link(self, src, None) + if isinstance(e, SCons.Errors.BuildError): desc = "Cannot duplicate `%s' in `%s': %s." % (src.path, self.dir.path, e.errstr) raise SCons.Errors.StopError, desc self.linked = 1 @@ -2440,7 +2660,7 @@ class File(Base): # which can be the case if they've disabled disk checks, # or if an action with a File target actually happens to # create a same-named directory by mistake. - csig = None + csig = '' else: csig = SCons.Util.MD5signature(contents) @@ -2511,7 +2731,9 @@ class File(Base): # ...and it's even up-to-date... if self._local: # ...and they'd like a local copy. - LocalCopy(self, r, None) + e = LocalCopy(self, r, None) + if isinstance(e, SCons.Errors.BuildError): + raise self.store_info() if T: Trace(' 1\n') return 1 @@ -2610,6 +2832,39 @@ class FileFinder: def __init__(self): self._memo = {} + def filedir_lookup(self, p, fd=None): + """ + A helper method for find_file() that looks up a directory for + a file we're trying to find. This only creates the Dir Node if + it exists on-disk, since if the directory doesn't exist we know + we won't find any files in it... :-) + + It would be more compact to just use this as a nested function + with a default keyword argument (see the commented-out version + below), but that doesn't work unless you have nested scopes, + so we define it here just this works work under Python 1.5.2. + """ + if fd is None: + fd = self.default_filedir + dir, name = os.path.split(fd) + drive, d = os.path.splitdrive(dir) + if d in ('/', os.sep): + return p + if dir: + p = self.filedir_lookup(p, dir) + if not p: + return None + norm_name = _my_normcase(name) + try: + node = p.entries[norm_name] + except KeyError: + return p.dir_on_disk(name) + # Once we move to Python 2.2 we can do: + #if isinstance(node, (Dir, Entry)): + if isinstance(node, Dir) or isinstance(node, Entry): + return node + return None + def _find_file_key(self, filename, paths, verbose=None): return (filename, paths) @@ -2655,14 +2910,35 @@ class FileFinder: filedir, filename = os.path.split(filename) if filedir: - def filedir_lookup(p, fd=filedir): - try: - return p.Dir(fd) - except TypeError: - # We tried to look up a Dir, but it seems there's - # already a File (or something else) there. No big. - return None - paths = filter(None, map(filedir_lookup, paths)) + # More compact code that we can't use until we drop + # support for Python 1.5.2: + # + #def filedir_lookup(p, fd=filedir): + # """ + # A helper function that looks up a directory for a file + # we're trying to find. This only creates the Dir Node + # if it exists on-disk, since if the directory doesn't + # exist we know we won't find any files in it... :-) + # """ + # dir, name = os.path.split(fd) + # if dir: + # p = filedir_lookup(p, dir) + # if not p: + # return None + # norm_name = _my_normcase(name) + # try: + # node = p.entries[norm_name] + # except KeyError: + # return p.dir_on_disk(name) + # # Once we move to Python 2.2 we can do: + # #if isinstance(node, (Dir, Entry)): + # if isinstance(node, Dir) or isinstance(node, Entry): + # return node + # return None + #paths = filter(None, map(filedir_lookup, paths)) + + self.default_filedir = filedir + paths = filter(None, map(self.filedir_lookup, paths)) result = None for dir in paths: diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 225226d..b698e87 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -288,8 +288,9 @@ class BuildDirTestCase(unittest.TestCase): assert not f7.exists() assert f7.rexists() - assert f7.rfile().path == os.path.normpath(test.workpath('rep1/build/var1/test2.out')),\ - f7.rfile().path + r = f7.rfile().path + expect = os.path.normpath(test.workpath('rep1/build/var1/test2.out')) + assert r == expect, (repr(r), repr(expect)) assert not f8.exists() assert f8.rexists() @@ -534,6 +535,8 @@ class BuildDirTestCase(unittest.TestCase): 'work/src/b1/b2', 'work/src/b1/b2/b1', 'work/src/b1/b2/b1/b2', + 'work/src/b1/b2/b1/b2/b1', + 'work/src/b1/b2/b1/b2/b1/b2', ] srcnode_map = { @@ -543,6 +546,10 @@ class BuildDirTestCase(unittest.TestCase): 'work/src/b1/b2/b1/f' : 'work/src/b1/f', 'work/src/b1/b2/b1/b2' : 'work/src/b1/b2', 'work/src/b1/b2/b1/b2/f' : 'work/src/b1/b2/f', + 'work/src/b1/b2/b1/b2/b1' : 'work/src/b1/b2/b1', + 'work/src/b1/b2/b1/b2/b1/f' : 'work/src/b1/b2/b1/f', + 'work/src/b1/b2/b1/b2/b1/b2' : 'work/src/b1/b2/b1/b2', + 'work/src/b1/b2/b1/b2/b1/b2/f' : 'work/src/b1/b2/b1/b2/f', } alter_map = { @@ -910,43 +917,47 @@ class FSTestCase(_tempdirTestCase): drive, path = os.path.splitdrive(os.getcwd()) + def _do_Dir_test(lpath, path_, abspath_, up_path_, sep, fileSys=fs, drive=drive): + dir = fileSys.Dir(string.replace(lpath, '/', sep)) + + if os.sep != '/': + path_ = string.replace(path_, '/', os.sep) + abspath_ = string.replace(abspath_, '/', os.sep) + up_path_ = string.replace(up_path_, '/', os.sep) + + def strip_slash(p, drive=drive): + if p[-1] == os.sep and len(p) > 1: + p = p[:-1] + if p[0] == os.sep: + p = drive + p + return p + path = strip_slash(path_) + abspath = strip_slash(abspath_) + up_path = strip_slash(up_path_) + name = string.split(abspath, os.sep)[-1] + + assert dir.name == name, \ + "dir.name %s != expected name %s" % \ + (dir.name, name) + assert dir.path == path, \ + "dir.path %s != expected path %s" % \ + (dir.path, path) + assert str(dir) == path, \ + "str(dir) %s != expected path %s" % \ + (str(dir), path) + assert dir.get_abspath() == abspath, \ + "dir.abspath %s != expected absolute path %s" % \ + (dir.get_abspath(), abspath) + assert dir.up().path == up_path, \ + "dir.up().path %s != expected parent path %s" % \ + (dir.up().path, up_path) + for sep in seps: - def Dir_test(lpath, path_, abspath_, up_path_, fileSys=fs, s=sep, drive=drive): - dir = fileSys.Dir(string.replace(lpath, '/', s)) - - if os.sep != '/': - path_ = string.replace(path_, '/', os.sep) - abspath_ = string.replace(abspath_, '/', os.sep) - up_path_ = string.replace(up_path_, '/', os.sep) - - def strip_slash(p, drive=drive): - if p[-1] == os.sep and len(p) > 1: - p = p[:-1] - if p[0] == os.sep: - p = drive + p - return p - path = strip_slash(path_) - abspath = strip_slash(abspath_) - up_path = strip_slash(up_path_) - name = string.split(abspath, os.sep)[-1] - - assert dir.name == name, \ - "dir.name %s != expected name %s" % \ - (dir.name, name) - assert dir.path == path, \ - "dir.path %s != expected path %s" % \ - (dir.path, path) - assert str(dir) == path, \ - "str(dir) %s != expected path %s" % \ - (str(dir), path) - assert dir.get_abspath() == abspath, \ - "dir.abspath %s != expected absolute path %s" % \ - (dir.get_abspath(), abspath) - assert dir.up().path == up_path, \ - "dir.up().path %s != expected parent path %s" % \ - (dir.up().path, up_path) + def Dir_test(lpath, path_, abspath_, up_path_, sep=sep, func=_do_Dir_test): + return func(lpath, path_, abspath_, up_path_, sep) + Dir_test('', './', sub_dir, sub) Dir_test('foo', 'foo/', sub_dir_foo, './') Dir_test('foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') Dir_test('/foo', '/foo/', '/foo/', '/') @@ -1374,6 +1385,109 @@ class FSTestCase(_tempdirTestCase): f.get_string(0) assert f.get_string(1) == 'baz', f.get_string(1) + def test_drive_letters(self): + """Test drive-letter look-ups""" + + test = self.test + + test.subdir('sub', ['sub', 'dir']) + + def drive_workpath(drive, dirs, test=test): + x = apply(test.workpath, dirs) + drive, path = os.path.splitdrive(x) + return 'X:' + path + + wp = drive_workpath('X:', ['']) + + if wp[-1] in (os.sep, '/'): + tmp = os.path.split(wp[:-1])[0] + else: + tmp = os.path.split(wp)[0] + + parent_tmp = os.path.split(tmp)[0] + if parent_tmp == 'X:': + parent_tmp = 'X:' + os.sep + + tmp_foo = os.path.join(tmp, 'foo') + + foo = drive_workpath('X:', ['foo']) + foo_bar = drive_workpath('X:', ['foo', 'bar']) + sub = drive_workpath('X:', ['sub', '']) + sub_dir = drive_workpath('X:', ['sub', 'dir', '']) + sub_dir_foo = drive_workpath('X:', ['sub', 'dir', 'foo', '']) + sub_dir_foo_bar = drive_workpath('X:', ['sub', 'dir', 'foo', 'bar', '']) + sub_foo = drive_workpath('X:', ['sub', 'foo', '']) + + fs = SCons.Node.FS.FS() + + seps = [os.sep] + if os.sep != '/': + seps = seps + ['/'] + + def _do_Dir_test(lpath, path_, up_path_, sep, fileSys=fs): + dir = fileSys.Dir(string.replace(lpath, '/', sep)) + + if os.sep != '/': + path_ = string.replace(path_, '/', os.sep) + up_path_ = string.replace(up_path_, '/', os.sep) + + def strip_slash(p): + if p[-1] == os.sep and len(p) > 3: + p = p[:-1] + return p + path = strip_slash(path_) + up_path = strip_slash(up_path_) + name = string.split(path, os.sep)[-1] + + assert dir.name == name, \ + "dir.name %s != expected name %s" % \ + (dir.name, name) + assert dir.path == path, \ + "dir.path %s != expected path %s" % \ + (dir.path, path) + assert str(dir) == path, \ + "str(dir) %s != expected path %s" % \ + (str(dir), path) + assert dir.up().path == up_path, \ + "dir.up().path %s != expected parent path %s" % \ + (dir.up().path, up_path) + + save_os_path = os.path + save_os_sep = os.sep + try: + import ntpath + os.path = ntpath + os.sep = '\\' + SCons.Node.FS.initialize_do_splitdrive() + SCons.Node.FS.initialize_normpath_check() + + for sep in seps: + + def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test): + return func(lpath, path_, up_path_, sep) + + Dir_test('#X:', wp, tmp) + Dir_test('X:foo', foo, wp) + Dir_test('X:foo/bar', foo_bar, foo) + Dir_test('X:/foo', 'X:/foo', 'X:/') + Dir_test('X:/foo/bar', 'X:/foo/bar/', 'X:/foo/') + Dir_test('X:..', tmp, parent_tmp) + Dir_test('X:foo/..', wp, tmp) + Dir_test('X:../foo', tmp_foo, tmp) + Dir_test('X:.', wp, tmp) + Dir_test('X:./.', wp, tmp) + Dir_test('X:foo/./bar', foo_bar, foo) + Dir_test('#X:../foo', tmp_foo, tmp) + Dir_test('#X:/../foo', tmp_foo, tmp) + Dir_test('#X:foo/bar', foo_bar, foo) + Dir_test('#X:/foo/bar', foo_bar, foo) + Dir_test('#X:/', wp, tmp) + finally: + os.path = save_os_path + os.sep = save_os_sep + SCons.Node.FS.initialize_do_splitdrive() + SCons.Node.FS.initialize_normpath_check() + def test_target_from_source(self): """Test the method for generating target nodes from sources""" fs = self.fs @@ -1426,13 +1540,7 @@ class FSTestCase(_tempdirTestCase): above_path = apply(os.path.join, ['..']*len(dirs) + ['above']) above = d2.Dir(above_path) - # Note that the rel_path() method is not used right now, but we're - # leaving it commented out and disabling the unit here because - # it would be a shame to have to recreate the logic (or remember - # that it's buried in a long-past code checkin) if we ever need to - # resurrect it. - - def DO_NOT_test_rel_path(self): + def test_rel_path(self): """Test the rel_path() method""" test = self.test fs = self.fs @@ -1669,10 +1777,10 @@ class DirTestCase(_tempdirTestCase): check(s, ['src/b1']) s = b1_b2_b1_b2.srcdir_list() - check(s, []) + check(s, ['src/b1/b2']) s = b1_b2_b1_b2_sub.srcdir_list() - check(s, []) + check(s, ['src/b1/b2/sub']) def test_srcdir_duplicate(self): """Test the Dir.srcdir_duplicate() method @@ -1978,6 +2086,291 @@ class FileTestCase(_tempdirTestCase): +class GlobTestCase(_tempdirTestCase): + def setUp(self): + _tempdirTestCase.setUp(self) + + fs = SCons.Node.FS.FS() + self.fs = fs + + # Make entries on disk that will not have Nodes, so we can verify + # the behavior of looking for things on disk. + self.test.write('disk-aaa', "disk-aaa\n") + self.test.write('disk-bbb', "disk-bbb\n") + self.test.write('disk-ccc', "disk-ccc\n") + self.test.subdir('disk-sub') + self.test.write(['disk-sub', 'disk-ddd'], "disk-sub/disk-ddd\n") + self.test.write(['disk-sub', 'disk-eee'], "disk-sub/disk-eee\n") + self.test.write(['disk-sub', 'disk-fff'], "disk-sub/disk-fff\n") + + # Make some entries that have both Nodes and on-disk entries, + # so we can verify what we do with + self.test.write('both-aaa', "both-aaa\n") + self.test.write('both-bbb', "both-bbb\n") + self.test.write('both-ccc', "both-ccc\n") + self.test.subdir('both-sub1') + self.test.write(['both-sub1', 'both-ddd'], "both-sub1/both-ddd\n") + self.test.write(['both-sub1', 'both-eee'], "both-sub1/both-eee\n") + self.test.write(['both-sub1', 'both-fff'], "both-sub1/both-fff\n") + self.test.subdir('both-sub2') + self.test.write(['both-sub2', 'both-ddd'], "both-sub2/both-ddd\n") + self.test.write(['both-sub2', 'both-eee'], "both-sub2/both-eee\n") + self.test.write(['both-sub2', 'both-fff'], "both-sub2/both-fff\n") + + self.both_aaa = fs.File('both-aaa') + self.both_bbb = fs.File('both-bbb') + self.both_ccc = fs.File('both-ccc') + self.both_sub1 = fs.Dir('both-sub1') + self.both_sub1_both_ddd = self.both_sub1.File('both-ddd') + self.both_sub1_both_eee = self.both_sub1.File('both-eee') + self.both_sub1_both_fff = self.both_sub1.File('both-fff') + self.both_sub2 = fs.Dir('both-sub2') + self.both_sub2_both_ddd = self.both_sub2.File('both-ddd') + self.both_sub2_both_eee = self.both_sub2.File('both-eee') + self.both_sub2_both_fff = self.both_sub2.File('both-fff') + + # Make various Nodes (that don't have on-disk entries) so we + # can verify how we match them. + self.ggg = fs.File('ggg') + self.hhh = fs.File('hhh') + self.iii = fs.File('iii') + self.subdir1 = fs.Dir('subdir1') + self.subdir1_jjj = self.subdir1.File('jjj') + self.subdir1_kkk = self.subdir1.File('kkk') + self.subdir1_lll = self.subdir1.File('lll') + self.subdir2 = fs.Dir('subdir2') + self.subdir2_jjj = self.subdir2.File('jjj') + self.subdir2_kkk = self.subdir2.File('kkk') + self.subdir2_lll = self.subdir2.File('lll') + self.sub = fs.Dir('sub') + self.sub_dir3 = self.sub.Dir('dir3') + self.sub_dir3_jjj = self.sub_dir3.File('jjj') + self.sub_dir3_kkk = self.sub_dir3.File('kkk') + self.sub_dir3_lll = self.sub_dir3.File('lll') + + + def do_cases(self, cases, **kwargs): + + # First, execute all of the cases with string=True and verify + # that we get the expected strings returned. We do this first + # so the Glob() calls don't add Nodes to the self.fs file system + # hierarchy. + + import copy + strings_kwargs = copy.copy(kwargs) + strings_kwargs['strings'] = True + for input, string_expect, node_expect in cases: + r = apply(self.fs.Glob, (input,), strings_kwargs) + r.sort() + assert r == string_expect, "Glob(%s, strings=True) expected %s, got %s" % (input, string_expect, r) + + # Now execute all of the cases without string=True and look for + # the expected Nodes to be returned. If we don't have a list of + # actual expected Nodes, that means we're expecting a search for + # on-disk-only files to have returned some newly-created nodes. + # Verify those by running the list through str() before comparing + # them with the expected list of strings. + for input, string_expect, node_expect in cases: + r = apply(self.fs.Glob, (input,), kwargs) + if node_expect: + r.sort(lambda a,b: cmp(a.path, b.path)) + result = node_expect + else: + r = map(str, r) + r.sort() + result = string_expect + assert r == result, "Glob(%s) expected %s, got %s" % (input, map(str, result), map(str, r)) + + def test_exact_match(self): + """Test globbing for exact Node matches""" + join = os.path.join + + cases = ( + ('ggg', ['ggg'], [self.ggg]), + + ('subdir1', ['subdir1'], [self.subdir1]), + + ('subdir1/jjj', [join('subdir1', 'jjj')], [self.subdir1_jjj]), + + ('disk-aaa', ['disk-aaa'], None), + + ('disk-sub', ['disk-sub'], None), + + ('both-aaa', ['both-aaa'], []), + ) + + self.do_cases(cases) + + def test_subdir_matches(self): + """Test globbing for exact Node matches in subdirectories""" + join = os.path.join + + cases = ( + ('*/jjj', + [join('subdir1', 'jjj'), join('subdir2', 'jjj')], + [self.subdir1_jjj, self.subdir2_jjj]), + + ('*/disk-ddd', + [join('disk-sub', 'disk-ddd')], + None), + ) + + self.do_cases(cases) + + def test_asterisk(self): + """Test globbing for simple asterisk Node matches""" + cases = ( + ('h*', + ['hhh'], + [self.hhh]), + + ('*', + ['both-aaa', 'both-bbb', 'both-ccc', + 'both-sub1', 'both-sub2', + 'ggg', 'hhh', 'iii', + 'sub', 'subdir1', 'subdir2'], + [self.both_aaa, self.both_bbb, self.both_ccc, + self.both_sub1, self.both_sub2, + self.ggg, self.hhh, self.iii, + self.sub, self.subdir1, self.subdir2]), + ) + + self.do_cases(cases, ondisk=False) + + cases = ( + ('disk-b*', + ['disk-bbb'], + None), + + ('*', + ['both-aaa', 'both-bbb', 'both-ccc', 'both-sub1', 'both-sub2', + 'disk-aaa', 'disk-bbb', 'disk-ccc', 'disk-sub', + 'ggg', 'hhh', 'iii', + 'sub', 'subdir1', 'subdir2'], + None), + ) + + self.do_cases(cases) + + def test_question_mark(self): + """Test globbing for simple question-mark Node matches""" + join = os.path.join + + cases = ( + ('ii?', + ['iii'], + [self.iii]), + + ('both-sub?/both-eee', + [join('both-sub1', 'both-eee'), join('both-sub2', 'both-eee')], + [self.both_sub1_both_eee, self.both_sub2_both_eee]), + + ('subdir?/jjj', + [join('subdir1', 'jjj'), join('subdir2', 'jjj')], + [self.subdir1_jjj, self.subdir2_jjj]), + + ('disk-cc?', + ['disk-ccc'], + None), + ) + + self.do_cases(cases) + + def test_does_not_exist(self): + """Test globbing for things that don't exist""" + + cases = ( + ('does_not_exist', [], []), + ('no_subdir/*', [], []), + ('subdir?/no_file', [], []), + ) + + self.do_cases(cases) + + def test_subdir_asterisk(self): + """Test globbing for asterisk Node matches in subdirectories""" + join = os.path.join + + cases = ( + ('*/k*', + [join('subdir1', 'kkk'), join('subdir2', 'kkk')], + [self.subdir1_kkk, self.subdir2_kkk]), + + ('both-sub?/*', + [join('both-sub1', 'both-ddd'), + join('both-sub1', 'both-eee'), + join('both-sub1', 'both-fff'), + join('both-sub2', 'both-ddd'), + join('both-sub2', 'both-eee'), + join('both-sub2', 'both-fff')], + [self.both_sub1_both_ddd, self.both_sub1_both_eee, self.both_sub1_both_fff, + self.both_sub2_both_ddd, self.both_sub2_both_eee, self.both_sub2_both_fff], + ), + + ('subdir?/*', + [join('subdir1', 'jjj'), + join('subdir1', 'kkk'), + join('subdir1', 'lll'), + join('subdir2', 'jjj'), + join('subdir2', 'kkk'), + join('subdir2', 'lll')], + [self.subdir1_jjj, self.subdir1_kkk, self.subdir1_lll, + self.subdir2_jjj, self.subdir2_kkk, self.subdir2_lll]), + + ('sub/*/*', + [join('sub', 'dir3', 'jjj'), + join('sub', 'dir3', 'kkk'), + join('sub', 'dir3', 'lll')], + [self.sub_dir3_jjj, self.sub_dir3_kkk, self.sub_dir3_lll]), + + ('*/k*', + [join('subdir1', 'kkk'), join('subdir2', 'kkk')], + None), + + ('subdir?/*', + [join('subdir1', 'jjj'), + join('subdir1', 'kkk'), + join('subdir1', 'lll'), + join('subdir2', 'jjj'), + join('subdir2', 'kkk'), + join('subdir2', 'lll')], + None), + + ('sub/*/*', + [join('sub', 'dir3', 'jjj'), + join('sub', 'dir3', 'kkk'), + join('sub', 'dir3', 'lll')], + None), + ) + + self.do_cases(cases) + + def test_subdir_question(self): + """Test globbing for question-mark Node matches in subdirectories""" + join = os.path.join + + cases = ( + ('*/?kk', + [join('subdir1', 'kkk'), join('subdir2', 'kkk')], + [self.subdir1_kkk, self.subdir2_kkk]), + + ('subdir?/l?l', + [join('subdir1', 'lll'), join('subdir2', 'lll')], + [self.subdir1_lll, self.subdir2_lll]), + + ('*/disk-?ff', + [join('disk-sub', 'disk-fff')], + None), + + ('subdir?/l?l', + [join('subdir1', 'lll'), join('subdir2', 'lll')], + None), + ) + + self.do_cases(cases) + + + class RepositoryTestCase(_tempdirTestCase): def setUp(self): @@ -2379,7 +2772,7 @@ class StringDirTestCase(unittest.TestCase): fs = SCons.Node.FS.FS(test.workpath('')) d = fs.Dir('sub', '.') - assert str(d) == 'sub' + assert str(d) == 'sub', str(d) assert d.exists() f = fs.File('file', 'sub') assert str(f) == os.path.join('sub', 'file') @@ -2913,6 +3306,7 @@ if __name__ == "__main__": FileBuildInfoTestCase, FileNodeInfoTestCase, FSTestCase, + GlobTestCase, RepositoryTestCase, ] for tclass in tclasses: diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 7ddca37..f252151 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -207,6 +207,7 @@ class Node: self.depends_dict = {} self.ignore = [] # dependencies to ignore self.ignore_dict = {} + self.prerequisites = SCons.Util.UniqueList() self.implicit = None # implicit (scanned) dependencies (None means not scanned yet) self.waiting_parents = {} self.waiting_s_e = {} @@ -361,11 +362,11 @@ class Node: in built(). """ - executor = self.get_executor() - stat = apply(executor, (self,), kw) - if stat: - msg = "Error %d" % stat - raise SCons.Errors.BuildError(node=self, errstr=msg) + try: + apply(self.get_executor(), (self,), kw) + except SCons.Errors.BuildError, e: + e.node = self + raise def built(self): """Called just after this node is successfully built.""" @@ -614,6 +615,7 @@ class Node: return build_env = self.get_build_env() + executor = self.get_executor() # Here's where we implement --implicit-cache. if implicit_cache and not implicit_deps_changed: @@ -623,7 +625,14 @@ class Node: # stored .sconsign entry to have already been converted # to Nodes for us. (We used to run them through a # source_factory function here.) - self._add_child(self.implicit, self.implicit_dict, implicit) + + # Update all of the targets with them. This + # essentially short-circuits an N*M scan of the + # sources for each individual target, which is a hell + # of a lot more efficient. + for tgt in executor.targets: + tgt.add_to_implicit(implicit) + if implicit_deps_unchanged or self.is_up_to_date(): return # one of this node's sources has changed, @@ -633,8 +642,6 @@ class Node: self._children_reset() self.del_binfo() - executor = self.get_executor() - # Have the executor scan the sources. executor.scan_sources(self.builder.source_scanner) @@ -825,6 +832,11 @@ class Node: s = str(e) raise SCons.Errors.UserError("attempted to add a non-Node dependency to %s:\n\t%s is a %s, not a Node" % (str(self), s, type(e))) + def add_prerequisite(self, prerequisite): + """Adds prerequisites""" + self.prerequisites.extend(prerequisite) + self._children_reset() + def add_ignore(self, depend): """Adds dependencies to ignore.""" try: @@ -1200,19 +1212,18 @@ class Node: lines = ["%s:\n" % preamble] + lines return string.join(lines, ' '*11) -l = [1] -ul = UserList.UserList([2]) try: - l.extend(ul) + [].extend(UserList.UserList([])) except TypeError: + # Python 1.5.2 doesn't allow a list to be extended by list-like + # objects (such as UserList instances), so just punt and use + # real lists. def NodeList(l): return l else: class NodeList(UserList.UserList): def __str__(self): return str(map(str, self.data)) -del l -del ul def get_children(node, parent): return node.children() def ignore_cycle(node, stack): pass diff --git a/src/engine/SCons/Options/OptionsTests.py b/src/engine/SCons/Options/OptionsTests.py index 95bd1cd..5ec9d7a 100644 --- a/src/engine/SCons/Options/OptionsTests.py +++ b/src/engine/SCons/Options/OptionsTests.py @@ -516,9 +516,37 @@ B 42 54 b - alpha test ['B'] assert text == expectAlpha, text - + +class UnknownOptionsTestCase(unittest.TestCase): + + def test_unknown(self): + """Test the UnknownOptions() method""" + opts = SCons.Options.Options() + opts.Add('ANSWER', + 'THE answer to THE question', + "42") + + args = { + 'ANSWER' : 'answer', + 'UNKNOWN' : 'unknown', + } + + env = Environment() + opts.Update(env, args) + + r = opts.UnknownOptions() + assert r == {'UNKNOWN' : 'unknown'}, r + assert env['ANSWER'] == 'answer', env['ANSWER'] + + + if __name__ == "__main__": - suite = unittest.makeSuite(OptionsTestCase, 'test_') + suite = unittest.TestSuite() + tclasses = [ OptionsTestCase, + UnknownOptionsTestCase ] + for tclass in tclasses: + names = unittest.getTestCaseNames(tclass, 'test_') + suite.addTests(map(tclass, names)) if not unittest.TextTestRunner().run(suite).wasSuccessful(): sys.exit(1) diff --git a/src/engine/SCons/Options/__init__.py b/src/engine/SCons/Options/__init__.py index e2ad80f..3dc7772 100644 --- a/src/engine/SCons/Options/__init__.py +++ b/src/engine/SCons/Options/__init__.py @@ -29,8 +29,11 @@ customizable variables to an SCons build. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +import SCons.compat + import os.path import string +import sys import SCons.Errors import SCons.Util @@ -64,6 +67,7 @@ class Options: else: files = [] self.files = files + self.unknown = {} # create the singleton instance if is_global: @@ -158,16 +162,29 @@ class Options: # next set the value specified in the options file for filename in self.files: if os.path.exists(filename): - execfile(filename, values) - - # finally set the values specified on the command line + dir = os.path.split(os.path.abspath(filename))[0] + if dir: + sys.path.insert(0, dir) + try: + values['__name__'] = filename + execfile(filename, {}, values) + finally: + if dir: + del sys.path[0] + del values['__name__'] + + # set the values specified on the command line if args is None: args = self.args for arg, value in args.items(): - for option in self.options: - if arg in option.aliases + [ option.key ]: - values[option.key]=value + added = False + for option in self.options: + if arg in option.aliases + [ option.key ]: + values[option.key] = value + added = True + if not added: + self.unknown[arg] = value # put the variables in the environment: # (don't copy over variables that are not declared as options) @@ -195,6 +212,13 @@ class Options: if option.validator and values.has_key(option.key): option.validator(option.key, env.subst('${%s}'%option.key), env) + def UnknownOptions(self): + """ + Returns any options in the specified arguments lists that + were not known, declared options in this object. + """ + return self.unknown + def Save(self, filename, env): """ Saves all the options in the given file. This file can diff --git a/src/engine/SCons/Platform/win32.py b/src/engine/SCons/Platform/win32.py index 8d35a8d..2fa30cc 100644 --- a/src/engine/SCons/Platform/win32.py +++ b/src/engine/SCons/Platform/win32.py @@ -40,13 +40,53 @@ import tempfile from SCons.Platform.posix import exitvalmap from SCons.Platform import TempFileMunge - -# XXX See note below about why importing SCons.Action should be -# eventually refactored. -import SCons.Action import SCons.Util + +try: + import msvcrt + import win32api + import win32con + + msvcrt.get_osfhandle + win32api.SetHandleInformation + win32con.HANDLE_FLAG_INHERIT +except ImportError: + parallel_msg = \ + "you do not seem to have the pywin32 extensions installed;\n" + \ + "\tparallel (-j) builds may not work reliably with open Python files." +except AttributeError: + parallel_msg = \ + "your pywin32 extensions do not support file handle operations;\n" + \ + "\tparallel (-j) builds may not work reliably with open Python files." +else: + parallel_msg = None + + import __builtin__ + + _builtin_file = __builtin__.file + _builtin_open = __builtin__.open + + def _scons_file(*args, **kw): + fp = apply(_builtin_file, args, kw) + win32api.SetHandleInformation(msvcrt.get_osfhandle(fp.fileno()), + win32con.HANDLE_FLAG_INHERIT, + 0) + return fp + + def _scons_open(*args, **kw): + fp = apply(_builtin_open, args, kw) + win32api.SetHandleInformation(msvcrt.get_osfhandle(fp.fileno()), + win32con.HANDLE_FLAG_INHERIT, + 0) + return fp + + __builtin__.file = _scons_file + __builtin__.open = _scons_open + + + # The upshot of all this is that, if you are using Python 1.5.2, # you had better have cmd or command.com in your PATH when you run # scons. diff --git a/src/engine/SCons/SConf.py b/src/engine/SCons/SConf.py index 47a552a..ae3a77e 100644 --- a/src/engine/SCons/SConf.py +++ b/src/engine/SCons/SConf.py @@ -54,6 +54,14 @@ from SCons.Debug import Trace SCons.Conftest.LogInputFiles = 0 SCons.Conftest.LogErrorMessages = 0 +# Set +build_type = None +build_types = ['clean', 'help'] + +def SetBuildType(type): + global build_type + build_type = type + # to be set, if we are in dry-run mode dryrun = 0 @@ -354,7 +362,7 @@ class SConfBuildTask(SCons.Taskmaster.Task): sconsign.set_entry(t.name, sconsign_entry) sconsign.merge() -class SConf: +class SConfBase: """This is simply a class to represent a configure context. After creating a SConf object, you can call any tests. After finished with your tests, be sure to call the Finish() method, which returns the modified @@ -395,6 +403,7 @@ class SConf: default_tests = { 'CheckFunc' : CheckFunc, 'CheckType' : CheckType, + 'CheckTypeSize' : CheckTypeSize, 'CheckHeader' : CheckHeader, 'CheckCHeader' : CheckCHeader, 'CheckCXXHeader' : CheckCXXHeader, @@ -603,7 +612,7 @@ class SConf: def AddTest(self, test_name, test_instance): """Adds test_class to this SConf instance. It can be called with self.test_name(...)""" - setattr(self, test_name, SConf.TestWrapper(test_instance, self)) + setattr(self, test_name, SConfBase.TestWrapper(test_instance, self)) def AddTests(self, tests): """Adds all the tests given in the tests dictionary to this SConf @@ -815,6 +824,19 @@ class CheckContext: #### End of stuff used by Conftest.py. +def SConf(*args, **kw): + if kw.get(build_type, True): + kw['_depth'] = kw.get('_depth', 0) + 1 + for bt in build_types: + try: + del kw[bt] + except KeyError: + pass + return apply(SConfBase, args, kw) + else: + return SCons.Util.Null() + + def CheckFunc(context, function_name, header = None, language = None): res = SCons.Conftest.CheckFunc(context, function_name, header = header, language = language) context.did_show_result = 1 @@ -826,6 +848,13 @@ def CheckType(context, type_name, includes = "", language = None): context.did_show_result = 1 return not res +def CheckTypeSize(context, type_name, includes = "", language = None, expect = None): + res = SCons.Conftest.CheckTypeSize(context, type_name, + header = includes, language = language, + expect = expect) + context.did_show_result = 1 + return res + def createIncludesFromHeaders(headers, leaveLast, include_quotes = '""'): # used by CheckHeader and CheckLibWithHeader to produce C - #include # statements from the specified header (list) diff --git a/src/engine/SCons/SConfTests.py b/src/engine/SCons/SConfTests.py index 2c35730..601c5eb 100644 --- a/src/engine/SCons/SConfTests.py +++ b/src/engine/SCons/SConfTests.py @@ -95,7 +95,7 @@ class SConfTestCase(unittest.TestCase): # 'TryLink'), so we are aware of reloading modules. def checks(self, sconf, TryFuncString): - TryFunc = self.SConf.SConf.__dict__[TryFuncString] + TryFunc = self.SConf.SConfBase.__dict__[TryFuncString] res1 = TryFunc( sconf, "int main() { return 0; }\n", ".c" ) res2 = TryFunc( sconf, '#include "no_std_header.h"\nint main() {return 0; }\n', @@ -497,6 +497,40 @@ int main() { finally: sconf.Finish() + def test_CheckTypeSize(self): + """Test SConf.CheckTypeSize() + """ + self._resetSConfState() + sconf = self.SConf.SConf(self.scons_env, + conf_dir=self.test.workpath('config.tests'), + log_file=self.test.workpath('config.log')) + try: + # CheckTypeSize() + + # In ANSI C, sizeof(char) == 1. + r = sconf.CheckTypeSize('char', expect = 1) + assert r == 1, "sizeof(char) != 1 ??" + r = sconf.CheckTypeSize('char', expect = 0) + assert r == 0, "sizeof(char) == 0 ??" + r = sconf.CheckTypeSize('char', expect = 2) + assert r == 0, "sizeof(char) == 2 ??" + r = sconf.CheckTypeSize('char') + assert r == 1, "sizeof(char) != 1 ??" + r = sconf.CheckTypeSize('const unsigned char') + assert r == 1, "sizeof(const unsigned char) != 1 ??" + + # Checking C++ + r = sconf.CheckTypeSize('const unsigned char', language = 'C++') + assert r == 1, "sizeof(const unsigned char) != 1 ??" + + # Checking Non-existing type + r = sconf.CheckTypeSize('thistypedefhasnotchancetosexist_scons') + assert r == 0, \ + "Checking size of thistypedefhasnotchancetosexist_scons succeeded ?" + + finally: + sconf.Finish() + def test_(self): """Test SConf.CheckType() """ diff --git a/src/engine/SCons/Script/Main.py b/src/engine/SCons/Script/Main.py index 36dc21e..97e0b19 100644 --- a/src/engine/SCons/Script/Main.py +++ b/src/engine/SCons/Script/Main.py @@ -139,6 +139,12 @@ def Progress(*args, **kw): # Task control. # + +_BuildFailures = [] + +def GetBuildFailures(): + return _BuildFailures + class BuildTask(SCons.Taskmaster.Task): """An SCons build task.""" progress = ProgressObject @@ -174,6 +180,7 @@ class BuildTask(SCons.Taskmaster.Task): display("scons: `%s' is up to date." % str(self.node)) def do_failed(self, status=2): + _BuildFailures.append(self.exception[1]) global exit_status if self.options.ignore_errors: SCons.Taskmaster.Task.executed(self) @@ -276,34 +283,31 @@ class BuildTask(SCons.Taskmaster.Task): class CleanTask(SCons.Taskmaster.Task): """An SCons clean task.""" - def dir_index(self, directory): - dirname = lambda f, d=directory: os.path.join(d, f) - files = map(dirname, os.listdir(directory)) - - # os.listdir() isn't guaranteed to return files in any specific order, - # but some of the test code expects sorted output. - files.sort() - return files - - def fs_delete(self, path, remove=1): + def fs_delete(self, path, pathstr, remove=1): try: if os.path.exists(path): if os.path.isfile(path): if remove: os.unlink(path) - display("Removed " + path) + display("Removed " + pathstr) elif os.path.isdir(path) and not os.path.islink(path): # delete everything in the dir - for p in self.dir_index(path): + entries = os.listdir(path) + # Sort for deterministic output (os.listdir() Can + # return entries in a random order). + entries.sort() + for e in entries: + p = os.path.join(path, e) + s = os.path.join(pathstr, e) if os.path.isfile(p): if remove: os.unlink(p) - display("Removed " + p) + display("Removed " + s) else: - self.fs_delete(p, remove) + self.fs_delete(p, s, remove) # then delete dir itself if remove: os.rmdir(path) - display("Removed directory " + path) + display("Removed directory " + pathstr) except (IOError, OSError), e: - print "scons: Could not remove '%s':" % str(path), e.strerror + print "scons: Could not remove '%s':" % pathstr, e.strerror def show(self): target = self.targets[0] @@ -314,7 +318,7 @@ class CleanTask(SCons.Taskmaster.Task): if SCons.Environment.CleanTargets.has_key(target): files = SCons.Environment.CleanTargets[target] for f in files: - self.fs_delete(str(f), 0) + self.fs_delete(f.abspath, str(f), 0) def remove(self): target = self.targets[0] @@ -335,7 +339,7 @@ class CleanTask(SCons.Taskmaster.Task): if SCons.Environment.CleanTargets.has_key(target): files = SCons.Environment.CleanTargets[target] for f in files: - self.fs_delete(str(f)) + self.fs_delete(f.abspath, str(f)) execute = remove @@ -726,6 +730,7 @@ def version_string(label, module): module.__buildsys__) def _main(parser): + import SCons global exit_status options = parser.values @@ -836,12 +841,10 @@ def _main(parser): CleanTask.execute = CleanTask.show if options.question: SCons.SConf.dryrun = 1 - if options.clean or options.help: - # If they're cleaning targets or have asked for help, replace - # the whole SCons.SConf module with a Null object so that the - # Configure() calls when reading the SConscript files don't - # actually do anything. - SCons.SConf.SConf = SCons.Util.Null + if options.clean: + SCons.SConf.SetBuildType('clean') + if options.help: + SCons.SConf.SetBuildType('help') SCons.SConf.SetCacheMode(options.config) SCons.SConf.SetProgressDisplay(progress_display) @@ -1067,7 +1070,6 @@ def _main(parser): """Leave the order of dependencies alone.""" return dependencies - progress_display("scons: " + opening_message) if options.taskmastertrace_file == '-': tmtrace = sys.stdout elif options.taskmastertrace_file: @@ -1083,15 +1085,22 @@ def _main(parser): global num_jobs num_jobs = options.num_jobs jobs = SCons.Job.Jobs(num_jobs, taskmaster) - if num_jobs > 1 and jobs.num_jobs == 1: - msg = "parallel builds are unsupported by this version of Python;\n" + \ - "\tignoring -j or num_jobs option.\n" - SCons.Warnings.warn(SCons.Warnings.NoParallelSupportWarning, msg) + if num_jobs > 1: + msg = None + if jobs.num_jobs == 1: + msg = "parallel builds are unsupported by this version of Python;\n" + \ + "\tignoring -j or num_jobs option.\n" + elif sys.platform == 'win32': + import SCons.Platform.win32 + msg = SCons.Platform.win32.parallel_msg + if msg: + SCons.Warnings.warn(SCons.Warnings.NoParallelSupportWarning, msg) memory_stats.append('before building targets:') count_stats.append(('pre-', 'build')) try: + progress_display("scons: " + opening_message) jobs.run() finally: jobs.cleanup() diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py index d1a115a..44f01b8 100644 --- a/src/engine/SCons/Script/__init__.py +++ b/src/engine/SCons/Script/__init__.py @@ -111,6 +111,7 @@ AddOption = Main.AddOption GetOption = Main.GetOption SetOption = Main.SetOption Progress = Main.Progress +GetBuildFailures = Main.GetBuildFailures #keep_going_on_error = Main.keep_going_on_error #print_dtree = Main.print_dtree @@ -302,6 +303,7 @@ GlobalDefaultEnvironmentFunctions = [ 'FindSourceFiles', 'Flatten', 'GetBuildPath', + 'Glob', 'Ignore', 'Install', 'InstallAs', @@ -310,6 +312,7 @@ GlobalDefaultEnvironmentFunctions = [ 'ParseDepends', 'Precious', 'Repository', + 'Requires', 'SConsignFile', 'SideEffect', 'SourceCode', diff --git a/src/engine/SCons/Taskmaster.py b/src/engine/SCons/Taskmaster.py index 598566e..3bb4225 100644 --- a/src/engine/SCons/Taskmaster.py +++ b/src/engine/SCons/Taskmaster.py @@ -535,7 +535,7 @@ class Taskmaster: node.set_state(SCons.Node.pending) try: - children = node.children() + children = node.children() + node.prerequisites except SystemExit: exc_value = sys.exc_info()[1] e = SCons.Errors.ExplicitExit(node, exc_value.code) diff --git a/src/engine/SCons/TaskmasterTests.py b/src/engine/SCons/TaskmasterTests.py index c79fb93..9a7969b 100644 --- a/src/engine/SCons/TaskmasterTests.py +++ b/src/engine/SCons/TaskmasterTests.py @@ -46,6 +46,7 @@ class Node: self.scanned = 0 self.scanner = None self.targets = [self] + self.prerequisites = [] class Builder: def targets(self, node): return node.targets diff --git a/src/engine/SCons/Tool/mslink.py b/src/engine/SCons/Tool/mslink.py index c071aa7..25f3564 100644 --- a/src/engine/SCons/Tool/mslink.py +++ b/src/engine/SCons/Tool/mslink.py @@ -186,9 +186,9 @@ def generate(env): env['WINDOWSEXPSUFFIX'] = '${WIN32EXPSUFFIX}' env['WINDOWSSHLIBMANIFESTPREFIX'] = '' - env['WINDOWSSHLIBMANIFESTSUFFIX'] = env['SHLIBSUFFIX'] + '.manifest' + env['WINDOWSSHLIBMANIFESTSUFFIX'] = '${SHLIBSUFFIX}.manifest' env['WINDOWSPROGMANIFESTPREFIX'] = '' - env['WINDOWSPROGMANIFESTSUFFIX'] = env['PROGSUFFIX'] + '.manifest' + env['WINDOWSPROGMANIFESTSUFFIX'] = '${PROGSUFFIX}.manifest' env['REGSVRACTION'] = regServerCheck env['REGSVR'] = os.path.join(SCons.Platform.win32.get_system_root(),'System32','regsvr32') diff --git a/src/engine/SCons/Tool/packaging/__init__.py b/src/engine/SCons/Tool/packaging/__init__.py index 72bbff0..79cd4ab 100644 --- a/src/engine/SCons/Tool/packaging/__init__.py +++ b/src/engine/SCons/Tool/packaging/__init__.py @@ -86,14 +86,17 @@ def Tag(env, target, source, *more_tags, **kw_tags): def Package(env, target=None, source=None, **kw): """ Entry point for the package tool. """ - # first check some arguments + # check if we need to find the source files ourself if not source: source = env.FindInstalledFiles() if len(source)==0: raise UserError, "No source for Package() given" - # has the option for this Tool been set? + # decide which types of packages shall be built. Can be defined through + # four mechanisms: command line argument, keyword argument, + # environment argument and default selection( zip or tar.gz ) in that + # order. try: kw['PACKAGETYPE']=env['PACKAGETYPE'] except KeyError: pass @@ -108,12 +111,12 @@ def Package(env, target=None, source=None, **kw): kw['PACKAGETYPE']='zip' else: raise UserError, "No type for Package() given" + PACKAGETYPE=kw['PACKAGETYPE'] if not is_List(PACKAGETYPE): - #PACKAGETYPE=PACKAGETYPE.split(',') PACKAGETYPE=string.split(PACKAGETYPE, ',') - # now load the needed packagers. + # load the needed packagers. def load_packager(type): try: file,path,desc=imp.find_module(type, __path__) @@ -123,23 +126,24 @@ def Package(env, target=None, source=None, **kw): packagers=map(load_packager, PACKAGETYPE) - # now try to setup the default_target and the default PACKAGEROOT - # arguments. + # set up targets and the PACKAGEROOT try: # fill up the target list with a default target name until the PACKAGETYPE # list is of the same size as the target list. - if target==None or target==[]: - target=["%(NAME)s-%(VERSION)s"%kw] + if not target: target = [] + + size_diff = len(PACKAGETYPE)-len(target) + default_name = "%(NAME)s-%(VERSION)s" - size_diff=len(PACKAGETYPE)-len(target) if size_diff>0: - target.extend([target]*size_diff) + default_target = default_name%kw + target.extend( [default_target]*size_diff ) if not kw.has_key('PACKAGEROOT'): - kw['PACKAGEROOT']="%(NAME)s-%(VERSION)s"%kw + kw['PACKAGEROOT'] = default_name%kw except KeyError, e: - raise SCons.Errors.UserError( "Missing PackageTag '%s'"%e.args[0] ) + raise SCons.Errors.UserError( "Missing Packagetag '%s'"%e.args[0] ) # setup the source files source=env.arg2nodes(source, env.fs.Entry) @@ -148,11 +152,14 @@ def Package(env, target=None, source=None, **kw): targets=[] try: for packager in packagers: - t=apply(packager.package, [env,target,source], kw) + t=[target.pop(0)] + t=apply(packager.package, [env,t,source], kw) targets.extend(t) + assert( len(target) == 0 ) + except KeyError, e: - raise SCons.Errors.UserError( "Missing PackageTag '%s' for %s packager"\ + raise SCons.Errors.UserError( "Missing Packagetag '%s' for %s packager"\ % (e.args[0],packager.__name__) ) except TypeError, e: # this exception means that a needed argument for the packager is @@ -170,10 +177,10 @@ def Package(env, target=None, source=None, **kw): if len(args)==0: raise # must be a different error, so reraise elif len(args)==1: - raise SCons.Errors.UserError( "Missing PackageTag '%s' for %s packager"\ + raise SCons.Errors.UserError( "Missing Packagetag '%s' for %s packager"\ % (args[0],packager.__name__) ) else: - raise SCons.Errors.UserError( "Missing PackageTags '%s' for %s packager"\ + raise SCons.Errors.UserError( "Missing Packagetags '%s' for %s packager"\ % (", ".join(args),packager.__name__) ) target=env.arg2nodes(target, env.fs.Entry) diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py index 04d263b..258de0f 100644 --- a/src/engine/SCons/Util.py +++ b/src/engine/SCons/Util.py @@ -1051,6 +1051,99 @@ class LogicalLines: +class UniqueList(UserList): + def __init__(self, seq = []): + UserList.__init__(self, seq) + self.unique = True + def __make_unique(self): + if not self.unique: + self.data = uniquer_hashables(self.data) + self.unique = True + def __lt__(self, other): + self.__make_unique() + return UserList.__lt__(self, other) + def __le__(self, other): + self.__make_unique() + return UserList.__le__(self, other) + def __eq__(self, other): + self.__make_unique() + return UserList.__eq__(self, other) + def __ne__(self, other): + self.__make_unique() + return UserList.__ne__(self, other) + def __gt__(self, other): + self.__make_unique() + return UserList.__gt__(self, other) + def __ge__(self, other): + self.__make_unique() + return UserList.__ge__(self, other) + def __cmp__(self, other): + self.__make_unique() + return UserList.__cmp__(self, other) + def __len__(self): + self.__make_unique() + return UserList.__len__(self) + def __getitem__(self, i): + self.__make_unique() + return UserList.__getitem__(self, i) + def __setitem__(self, i, item): + UserList.__setitem__(self, i, item) + self.unique = False + def __getslice__(self, i, j): + self.__make_unique() + return UserList.__getslice__(self, i, j) + def __setslice__(self, i, j, other): + UserList.__setslice__(self, i, j, other) + self.unique = False + def __add__(self, other): + result = UserList.__add__(self, other) + result.unique = False + return result + def __radd__(self, other): + result = UserList.__radd__(self, other) + result.unique = False + return result + def __iadd__(self, other): + result = UserList.__iadd__(self, other) + result.unique = False + return result + def __mul__(self, other): + result = UserList.__mul__(self, other) + result.unique = False + return result + def __rmul__(self, other): + result = UserList.__rmul__(self, other) + result.unique = False + return result + def __imul__(self, other): + result = UserList.__imul__(self, other) + result.unique = False + return result + def append(self, item): + UserList.append(self, item) + self.unique = False + def insert(self, i): + UserList.insert(self, i) + self.unique = False + def count(self, item): + self.__make_unique() + return UserList.count(self, item) + def index(self, item): + self.__make_unique() + return UserList.index(self, item) + def reverse(self): + self.__make_unique() + UserList.reverse(self) + def sort(self, *args, **kwds): + self.__make_unique() + #return UserList.sort(self, *args, **kwds) + return apply(UserList.sort, (self,)+args, kwds) + def extend(self, other): + UserList.extend(self, other) + self.unique = False + + + class Unbuffered: """ A proxy class that wraps a file object, flushing after every write, diff --git a/src/engine/SCons/compat/__init__.py b/src/engine/SCons/compat/__init__.py index 5e095d1..47ae3be 100644 --- a/src/engine/SCons/compat/__init__.py +++ b/src/engine/SCons/compat/__init__.py @@ -111,6 +111,35 @@ except NameError: import sets __builtin__.set = sets.Set +import fnmatch +try: + fnmatch.filter +except AttributeError: + # Pre-2.2 Python has no fnmatch.filter() function. + def filter(names, pat): + """Return the subset of the list NAMES that match PAT""" + import os,posixpath + result=[] + pat = os.path.normcase(pat) + if not fnmatch._cache.has_key(pat): + import re + res = fnmatch.translate(pat) + fnmatch._cache[pat] = re.compile(res) + match = fnmatch._cache[pat].match + if os.path is posixpath: + # normcase on posix is NOP. Optimize it away from the loop. + for name in names: + if match(name): + result.append(name) + else: + for name in names: + if match(os.path.normcase(name)): + result.append(name) + return result + fnmatch.filter = filter + del filter + + # If we need the compatibility version of textwrap, it must be imported # before optparse, which uses it. try: |