Add an option to not save the --debug=explain information.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Tue, 15 Jun 2004 12:54:45 +0000 (12:54 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Tue, 15 Jun 2004 12:54:45 +0000 (12:54 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@993 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/CHANGES.txt
src/engine/SCons/Node/FS.py
src/engine/SCons/Node/FSTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/SConsign.py
src/engine/SCons/SConsignTests.py
src/engine/SCons/Script/__init__.py
test/chained-build.py
test/explain.py

index 51c9e4c41a660b21fb1471b152ca398e7806648d..63c6a83eff604d4b01cd877ee33d3e54302b2e11 100644 (file)
@@ -757,6 +757,18 @@ Also suppresses SCons status messages.
 Ignored for compatibility with GNU 
 .BR make .
 
+.TP
+.RI --save-explain-info= 0|1
+By default,
+scons will save additional information about the build
+that is used by the
+.B --debug=explain
+option.
+Setting
+.B --save-explain-info=0
+will tell scons to speed up the build
+by not saving this information.
+
 .TP
 -t, --touch
 Ignored for compatibility with GNU
@@ -3423,7 +3435,9 @@ which corresponds to --implicit-cache;
 .B max_drift
 which corresponds to --max-drift;
 .B num_jobs
-which corresponds to -j and --jobs.
+which corresponds to -j and --jobs;
+.B save_explain_info
+which corresponds to --save-explain-info.
 See the documentation for the
 corresponding command line object for information about each specific
 option. Example:
index 704d7b77c78ea975b85b51ae8ce4245118cb5baa..ee758573e3f40e5cdc5bb74fa93167044dd52f00 100644 (file)
@@ -94,6 +94,11 @@ RELEASE 0.96 - XXX
   - Add a --debug=explain option that reports the reason(s) why SCons
     thinks it must rebuild something.
 
+  - Add --save-explain-info and SetOption('save_explain_info') options
+    to control whether the --debug=explain information is saved in
+    the .sconsign file(s).  Not saving this information can improve
+    performance and save memory usage.
+
   - Add support for functions that return platform-independent Actions
     to Chmod(), Copy(), Delete(), Mkdir(), Move() and Touch() files
     and/or directories.  Like any other Actions, the returned Action
index 23cf8a7a9ab6a722f56ab35b1fbdb37f4337dab1..2f115c9c787fa97121977532fad50d64820efb12 100644 (file)
@@ -1385,7 +1385,34 @@ class File(Base):
             return 0
 
     def store_info(self, obj):
-        self.dir.sconsign().set_entry(self.name, obj)
+        # Merge our build information into the already-stored entry.
+        # This accomodates "chained builds" where a file that's a target
+        # in one build (SConstruct file) is a source in a different build.
+        # See test/chained-build.py for the use case.
+        entry = self.get_stored_info()
+        if not SCons.Node.Save_Explain_Info:
+            # If we're not saving explanation info, wipe out any that
+            # might be in the already-stored entry.
+            #
+            # XXX This is kind of bad that we're naming attributes that
+            # are really controlled in Node/__init__.py.  It would be
+            # good to find a way to move this logic there in some way
+            # that still accounts for the fact that not all Node classes
+            # need or use this information.
+            attributes = [
+                'bsources',     'bsourcesigs',
+                'bdepends',     'bdependsigs',
+                'bimplicit',    'bimplicitsigs',
+                'bact',         'bactsig',
+            ]
+            for attr in attributes:
+                try:
+                     delattr(entry, attr)
+                except AttributeError:
+                    pass
+        for key, val in obj.__dict__.items():
+            entry.__dict__[key] = val
+        sconsign = self.dir.sconsign().set_entry(self.name, entry)
 
     def get_stored_info(self):
         try:
index d9ffd83ba5355653951e06f0ddc13da0ea14b9d9..d4137c1c75bdfa9d1794db2f083fc04e545ea5ae 100644 (file)
@@ -1969,6 +1969,55 @@ class SaveStringsTestCase(unittest.TestCase):
         expect = map(os.path.normpath, ['src/f', 'd1/f', 'd0/b', 'd1/b'])
         assert s == expect, s
 
+class SaveExplainInfoTestCase(unittest.TestCase):
+    def runTest(self):
+        """Test how we store --debug=explain info."""
+        test=TestCmd(workdir='')
+        fs = SCons.Node.FS.FS(test.workpath('fs'))
+        f = fs.File('file')
+
+        class BInfo:
+            pass
+
+        bi = BInfo()
+        bi.bact = 'file bact'
+        bi.arg1 = 'file arg1'
+        f.store_info(bi)
+
+        i = f.get_stored_info()
+        assert i.bact == 'file bact', i.arg1
+        assert i.arg1 == 'file arg1', i.arg1
+        assert not hasattr(i, 'arg2'), i.bact
+        assert not hasattr(i, 'arg3'), i.bact
+
+        save_value = SCons.Node.Save_Explain_Info
+        try:
+            SCons.Node.Save_Explain_Info = 1
+
+            bi = BInfo()
+            bi.arg2 = 'file arg2'
+            f.store_info(bi)
+
+            i = f.get_stored_info()
+            assert i.bact == 'file bact', i.arg1
+            assert i.arg1 == 'file arg1', i.arg1
+            assert i.arg2 == 'file arg2', i.arg2
+            assert not hasattr(i, 'arg3'), i.bact
+
+            SCons.Node.Save_Explain_Info = 0
+
+            bi = BInfo()
+            bi.arg3 = 'file arg3'
+            f.store_info(bi)
+
+            i = f.get_stored_info()
+            assert not hasattr(i, 'bact'), i.bact
+            assert i.arg1 == 'file arg1', i.arg1
+            assert i.arg2 == 'file arg2', i.arg2
+            assert i.arg3 == 'file arg3', i.arg2
+        finally:
+            SCons.Node.Save_Explain_Info = save_value
+
 
 
 if __name__ == "__main__":
@@ -1989,5 +2038,6 @@ if __name__ == "__main__":
     suite.addTest(postprocessTestCase())
     suite.addTest(SpecialAttrTestCase())
     suite.addTest(SaveStringsTestCase())
+    suite.addTest(SaveExplainInfoTestCase())
     if not unittest.TextTestRunner().run(suite).wasSuccessful():
         sys.exit(1)
index 4c7d9c224bb4979c7186e1750d3c9e42ccc3febd..cd7aa18a9ce38a944cb797ba6211b2a03e533b22 100644 (file)
@@ -396,6 +396,7 @@ class NodeTestCase(unittest.TestCase):
                     def collect(self, args):
                         return reduce(lambda x, y: x+y, args, self.val)
                 self.module = M(val)
+
         node = SCons.Node.Node()
         binfo = node.gen_binfo(Calculator(666))
         assert isinstance(binfo, SCons.Node.BuildInfo), binfo
@@ -407,6 +408,19 @@ class NodeTestCase(unittest.TestCase):
         assert hasattr(binfo, 'bimplicitsigs')
         assert binfo.bsig == 666, binfo.bsig
 
+        SCons.Node.Save_Explain_Info = 0
+
+        node = SCons.Node.Node()
+        binfo = node.gen_binfo(Calculator(777))
+        assert isinstance(binfo, SCons.Node.BuildInfo), binfo
+        assert not hasattr(binfo, 'bsources')
+        assert not hasattr(binfo, 'bsourcesigs')
+        assert not hasattr(binfo, 'bdepends')
+        assert not hasattr(binfo, 'bdependsigs')
+        assert not hasattr(binfo, 'bimplicit')
+        assert not hasattr(binfo, 'bimplicitsigs')
+        assert binfo.bsig == 777, binfo.bsig
+
     def test_explain(self):
         """Test explaining why a Node must be rebuilt
         """
index ba4cbca588a1ad2d90f00a691e451518acacde6f..64226a623761c985a889f540a8d2d9cb6c4a63cb 100644 (file)
@@ -76,6 +76,9 @@ implicit_deps_unchanged = 0
 # controls whether the cached implicit deps are ignored:
 implicit_deps_changed = 0
 
+# controls whether --debug=explain info is saved in Nodes:
+Save_Explain_Info = 1
+
 # A variable that can be set to an interface-specific function be called
 # to annotate a Node with information about its creation.
 def do_nothing(node): pass
@@ -542,21 +545,26 @@ class Node:
         dependsigs = map(calc_signature, depends)
         implicitsigs = map(calc_signature, implicit)
 
-        binfo.bsources = map(str, sources)
-        binfo.bdepends = map(str, depends)
-        binfo.bimplicit = map(str, implicit)
-
-        binfo.bsourcesigs = sourcesigs
-        binfo.bdependsigs = dependsigs
-        binfo.bimplicitsigs = implicitsigs
-
         sigs = sourcesigs + dependsigs + implicitsigs
 
-        if self.has_builder():
+        has_builder = self.has_builder()
+        if has_builder:
             executor = self.get_executor()
-            binfo.bact = str(executor)
-            binfo.bactsig = calc.module.signature(executor)
-            sigs.append(binfo.bactsig)
+            bactsig = calc.module.signature(executor)
+            sigs.append(bactsig)
+
+        if Save_Explain_Info:
+            binfo.bsources = map(str, sources)
+            binfo.bdepends = map(str, depends)
+            binfo.bimplicit = map(str, implicit)
+
+            binfo.bsourcesigs = sourcesigs
+            binfo.bdependsigs = dependsigs
+            binfo.bimplicitsigs = implicitsigs
+
+            if has_builder:
+                binfo.bact = str(executor)
+                binfo.bactsig = calc.module.signature(executor)
 
         binfo.bsig = calc.module.collect(filter(None, sigs))
 
index c97f1b67c63d87cb165e8f2e659cf8ccfed2e519..b7b06fe51809b436717c2a6d18c774939e9b4317 100644 (file)
@@ -68,8 +68,7 @@ class Base:
 
     def get_entry(self, filename):
         """
-        Create an entry for the filename and return it, or if one already exists,
-        then return it.
+        Fetch the specified entry attribute.
         """
         return self.entries[filename]
 
@@ -77,13 +76,7 @@ class Base:
         """
         Set the entry.
         """
-        try:
-            entry = self.entries[filename]
-        except KeyError:
-            self.entries[filename] = obj
-        else:
-            for key, val in obj.__dict__.items():
-                entry.__dict__[key] = val
+        self.entries[filename] = obj
         self.dirty = 1
 
 class DB(Base):
index 79f43872b902f9c67d6ab36ea548f6d6a6b812d1..16ef81640e1c19bd0e877a05431668b84c79322f 100644 (file)
@@ -47,8 +47,9 @@ class BaseTestCase(unittest.TestCase):
 
         aaa = BuildInfo('aaa')
         bbb = BuildInfo('bbb')
+        bbb.arg1 = 'bbb arg1'
         ccc = BuildInfo('ccc')
-        ccc.arg = 'ccc arg'
+        ccc.arg2 = 'ccc arg2'
 
         f = SCons.SConsign.Base()
         f.set_entry('aaa', aaa)
@@ -61,12 +62,14 @@ class BaseTestCase(unittest.TestCase):
         e = f.get_entry('bbb')
         assert e == bbb, e
         assert e.name == 'bbb', e.name
-        assert not hasattr(e, 'arg'), e
+        assert e.arg1 == 'bbb arg1', e.arg1
+        assert not hasattr(e, 'arg2'), e
 
         f.set_entry('bbb', ccc)
         e = f.get_entry('bbb')
         assert e.name == 'ccc', e.name
-        assert e.arg == 'ccc arg', e.arg
+        assert not hasattr(e, 'arg1'), e
+        assert e.arg2 == 'ccc arg2', e.arg1
 
         ddd = BuildInfo('ddd')
         eee = BuildInfo('eee')
index d4650d209882282329b4ad97b9b2a6354c84e0ed..835409ebdb026a845fd0a1ee86b825c3d4831d32 100644 (file)
@@ -528,7 +528,6 @@ class OptParser(OptionParser):
 
         def opt_debug(option, opt, value, parser, debug_options=debug_options):
             if value in debug_options:
-                #setattr(parser.values, 'debug', value)
                 parser.values.debug = value
             else:
                 raise OptionValueError("Warning:  %s is not a valid debug type" % value)
@@ -541,7 +540,6 @@ class OptParser(OptionParser):
         def opt_duplicate(option, opt, value, parser):
             if not value in SCons.Node.FS.Valid_Duplicates:
                 raise OptionValueError("`%s' is not a valid duplication style." % value)
-            #setattr(parser.values, 'duplicate', value)
             parser.values.duplicate = value
             # Set the duplicate style right away so it can affect linking
             # of SConscript files.
@@ -584,7 +582,6 @@ class OptParser(OptionParser):
 
         def opt_j(option, opt, value, parser):
             value = int(value)
-            #setattr(parser.values, 'num_jobs', value)
             parser.values.num_jobs = value
         self.add_option('-j', '--jobs', action="callback", type="int",
                         callback=opt_j, metavar="N",
@@ -627,6 +624,10 @@ class OptParser(OptionParser):
         self.add_option('-s', '--silent', '--quiet', action="store_true",
                         default=0, help="Don't print commands.")
 
+        self.add_option('--save-explain-info', type="int", action="store",
+                        dest='save_explain_info', metavar='0|1',
+                        help="(Don't) save --debug=explain information")
+
         self.add_option('-u', '--up', '--search-up', action="store_const",
                         dest="climb_up", default=0, const=1,
                         help="Search up directory tree for SConstruct,       "
@@ -734,7 +735,8 @@ class SConscriptSettableOptions:
                          'max_drift':SCons.Sig.default_max_drift,
                          'implicit_cache':0,
                          'clean':0,
-                         'duplicate':'hard-soft-copy'}
+                         'duplicate':'hard-soft-copy',
+                         'save_explain_info':1}
 
     def get(self, name):
         if not self.settable.has_key(name):
@@ -770,7 +772,7 @@ class SConscriptSettableOptions:
             # Set the duplicate stye right away so it can affect linking
             # of SConscript files.
             SCons.Node.FS.set_duplicate(value)
-            
+
         self.settable[name] = value
     
 
@@ -947,6 +949,7 @@ def _main(args, parser):
     # that are SConscript settable:
     SCons.Node.implicit_cache = ssoptions.get('implicit_cache')
     SCons.Node.FS.set_duplicate(ssoptions.get('duplicate'))
+    SCons.Node.Save_Explain_Info = ssoptions.get('save_explain_info') or print_explanations
 
     lookup_top = None
     if targets:
index e4463e5af09964e2f476c75677d2f028464c04e7..ff14964cb20dc5441b378f7687d0c53144f9ea4d 100644 (file)
@@ -28,40 +28,93 @@ import TestSCons
 
 test = TestSCons.TestSCons()
 
-test.write('SConstruct1', """
+test.subdir('w1', 'w2')
+
+SConstruct1_contents = """\
 def build(env, target, source):
     open(str(target[0]), 'wt').write(open(str(source[0]), 'rt').read())
 
 env=Environment(BUILDERS={'B' : Builder(action=build)})
 env.B('foo.mid', 'foo.in')
-""")
+"""
 
-test.write('SConstruct2', """
+SConstruct2_contents = """\
 def build(env, target, source):
     open(str(target[0]), 'wt').write(open(str(source[0]), 'rt').read())
 
 env=Environment(BUILDERS={'B' : Builder(action=build)})
 env.B('foo.out', 'foo.mid')
-""")
+"""
+
+# Test with the default of saving explanation info.
+test.write(['w1', 'SConstruct1'], SConstruct1_contents)
+test.write(['w1', 'SConstruct2'], SConstruct2_contents)
+test.write(['w1', 'foo.in'], "foo.in 1")
+
+test.run(chdir='w1',
+         arguments="--max-drift=0 -f SConstruct1 foo.mid",
+         stdout = test.wrap_stdout('build("foo.mid", "foo.in")\n'))
+test.run(chdir='w1',
+         arguments="--max-drift=0 -f SConstruct2 foo.out",
+         stdout = test.wrap_stdout('build("foo.out", "foo.mid")\n'))
+
+test.up_to_date(chdir='w1',
+                options="--max-drift=0 -f SConstruct1",
+                arguments="foo.mid")
+test.up_to_date(chdir='w1',
+                options="--max-drift=0 -f SConstruct2",
+                arguments="foo.out")
+
+test.write(['w1', 'foo.in'], "foo.in 2")
+
+test.run(chdir='w1',
+         arguments="--max-drift=0 -f SConstruct1 foo.mid",
+         stdout = test.wrap_stdout('build("foo.mid", "foo.in")\n'))
+test.run(chdir='w1',
+         arguments="--max-drift=0 -f SConstruct2 foo.out",
+         stdout = test.wrap_stdout('build("foo.out", "foo.mid")\n'))
+
+test.up_to_date(chdir='w1',
+                options="--max-drift=0 -f SConstruct1",
+                arguments="foo.mid")
+test.up_to_date(chdir='w1',
+                options="--max-drift=0 -f SConstruct2",
+                arguments="foo.out")
 
-test.write('foo.in', "foo.in")
+# Now test when we're not saving explanation info.
+preamble = "SetOption('save_explain_info', 0)\n"
+test.write(['w2', 'SConstruct1'], preamble + SConstruct1_contents)
+test.write(['w2', 'SConstruct2'], preamble + SConstruct2_contents)
+test.write(['w2', 'foo.in'], "foo.in 1")
 
-test.run(arguments="--max-drift=0 -f SConstruct1 foo.mid",
+test.run(chdir='w2',
+         arguments="--max-drift=0 -f SConstruct1 foo.mid",
          stdout = test.wrap_stdout('build("foo.mid", "foo.in")\n'))
-test.run(arguments="--max-drift=0 -f SConstruct2 foo.out",
+test.run(chdir='w2',
+         arguments="--max-drift=0 -f SConstruct2 foo.out",
          stdout = test.wrap_stdout('build("foo.out", "foo.mid")\n'))
 
-test.up_to_date(options="--max-drift=0 -f SConstruct1", arguments="foo.mid")
-test.up_to_date(options="--max-drift=0 -f SConstruct2", arguments="foo.out")
+test.up_to_date(chdir='w2',
+                options="--max-drift=0 -f SConstruct1",
+                arguments="foo.mid")
+test.up_to_date(chdir='w2',
+                options="--max-drift=0 -f SConstruct2",
+                arguments="foo.out")
 
-test.write('foo.in', "foo.in 2")
+test.write(['w2', 'foo.in'], "foo.in 2")
 
-test.run(arguments="--max-drift=0 -f SConstruct1 foo.mid",
+test.run(chdir='w2',
+         arguments="--max-drift=0 -f SConstruct1 foo.mid",
          stdout = test.wrap_stdout('build("foo.mid", "foo.in")\n'))
-test.run(arguments="--max-drift=0 -f SConstruct2 foo.out",
+test.run(chdir='w2',
+         arguments="--max-drift=0 -f SConstruct2 foo.out",
          stdout = test.wrap_stdout('build("foo.out", "foo.mid")\n'))
 
-test.up_to_date(options="--max-drift=0 -f SConstruct1", arguments="foo.mid")
-test.up_to_date(options="--max-drift=0 -f SConstruct2", arguments="foo.out")
+test.up_to_date(chdir='w2',
+                options="--max-drift=0 -f SConstruct1",
+                arguments="foo.mid")
+test.up_to_date(chdir='w2',
+                options="--max-drift=0 -f SConstruct2",
+                arguments="foo.out")
 
 test.pass_test()
index e3f80e42a30bc903129ae99308f6c40a84995695..4526ca940428716bd357d8348964f9a99136e214 100644 (file)
@@ -38,7 +38,9 @@ python = TestSCons.python
 test = TestSCons.TestSCons()
 
 test.subdir('work1', ['work1', 'src'], ['work1', 'src', 'subdir'],
-            'work2', ['work2', 'src'], ['work2', 'src', 'subdir'])
+            'work2', ['work2', 'src'], ['work2', 'src', 'subdir'],
+            'work3', ['work3', 'src'], ['work3', 'src', 'subdir'],
+            'work4', ['work4', 'src'], ['work4', 'src', 'subdir'])
 
 subdir_file6 = os.path.join('subdir', 'file6')
 subdir_file6_in = os.path.join('subdir', 'file6.in')
@@ -100,7 +102,7 @@ env.InstallAs('../inc/eee', 'eee.in')
 
 args = '--debug=explain .'
 
-#
+#############################################################################
 test.write(['work1', 'src', 'SConstruct'], SConstruct_contents)
 
 test.write(['work1', 'src', 'SConscript'], """\
@@ -323,4 +325,536 @@ test.run(chdir='work1/src', arguments=args)
 
 test.up_to_date(chdir='work1/src',arguments='.')
 
+#############################################################################
+# Now test (in a separate workspace) how things function when
+# we tell SCons to not save the --debug=explain info
+# using SetOption('save_explain_info').
+test.write(['work2', 'src', 'SConstruct'],
+           "SetOption('save_explain_info', 0)\n" + SConstruct_contents)
+
+test.write(['work2', 'src', 'SConscript'], """\
+Import("env")
+env.Cat('file1', 'file1.in')
+env.Cat('file2', 'file2.k')
+env.Cat('file3', ['xxx', 'yyy', 'zzz'])
+env.Command('file4', 'file4.in', r"%s %s $TARGET - $SOURCES")
+env.Cat('file5', 'file5.k')
+env.Cat('subdir/file6', 'subdir/file6.in')
+""" % (python, cat_py))
+
+test.write(['work2', 'src', 'aaa'], "aaa 1\n")
+test.write(['work2', 'src', 'bbb.k'], """\
+bbb.k 1
+include ccc
+include ../inc/ddd
+include ../inc/eee
+""")
+test.write(['work2', 'src', 'ccc'], "ccc 1\n")
+test.write(['work2', 'src', 'ddd'], "ddd 1\n")
+test.write(['work2', 'src', 'eee.in'], "eee.in 1\n")
+
+test.write(['work2', 'src', 'file1.in'], "file1.in 1\n")
+
+test.write(['work2', 'src', 'file2.k'], """\
+file2.k 1 line 1
+include xxx
+include yyy
+file2.k 1 line 4
+""")
+
+test.write(['work2', 'src', 'file4.in'], "file4.in 1\n")
+
+test.write(['work2', 'src', 'xxx'], "xxx 1\n")
+test.write(['work2', 'src', 'yyy'], "yyy 1\n")
+test.write(['work2', 'src', 'zzz'], "zzz 1\n")
+
+test.write(['work2', 'src', 'file5.k'], """\
+file5.k 1 line 1
+include ../inc/aaa
+include ../inc/bbb.k
+file5.k 1 line 4
+""")
+
+test.write(['work2', 'src', 'subdir', 'file6.in'], "subdir/file6.in 1\n")
+
+# First, even without build info, we can tell the user that things
+# are being built because they don't exist.
+test.run(chdir='work2/src', arguments=args, stdout=test.wrap_stdout("""\
+scons: building `file1' because it doesn't exist
+%s %s file1 file1.in
+scons: building `file2' because it doesn't exist
+%s %s file2 file2.k
+scons: building `file3' because it doesn't exist
+%s %s file3 xxx yyy zzz
+scons: building `file4' because it doesn't exist
+%s %s file4 - file4.in
+scons: building `%s' because it doesn't exist
+Install file: "aaa" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "ddd" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "eee.in" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "bbb.k" as "%s"
+scons: building `file5' because it doesn't exist
+%s %s file5 file5.k
+scons: building `%s' because it doesn't exist
+%s %s %s %s
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work2', 'inc', 'aaa'),
+       test.workpath('work2', 'inc', 'aaa'),
+       test.workpath('work2', 'inc', 'ddd'),
+       test.workpath('work2', 'inc', 'ddd'),
+       test.workpath('work2', 'inc', 'eee'),
+       test.workpath('work2', 'inc', 'eee'),
+       test.workpath('work2', 'inc', 'bbb.k'),
+       test.workpath('work2', 'inc', 'bbb.k'),
+       python, cat_py,
+       subdir_file6,
+       python, cat_py, subdir_file6, subdir_file6_in)))
+
+test.must_match(['work2', 'src', 'file1'], "file1.in 1\n")
+test.must_match(['work2', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 1
+file2.k 1 line 4
+""")
+test.must_match(['work2', 'src', 'file3'], "xxx 1\nyyy 1\nzzz 1\n")
+test.must_match(['work2', 'src', 'file4'], "file4.in 1\n")
+test.must_match(['work2', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 1
+ccc 1
+ddd 1
+eee.in 1
+file5.k 1 line 4
+""")
+
+# Using --debug=explain above will have actually saved the build info;
+# run again to clear it out.
+test.write(['work2', 'src', 'file1.in'], "file1.in 2\n")
+test.write(['work2', 'src', 'yyy'], "yyy 2\n")
+test.write(['work2', 'src', 'zzz'], "zzz 2\n")
+test.write(['work2', 'src', 'bbb.k'], "bbb.k 2\ninclude ccc\n")
+
+test.run(chdir='work2/src', arguments='.')
+
+# Now, it should tell us that it can't explain why the files are
+# being rebuilt.  It should also *store* the build info because
+# we're using --debug=explain...
+test.write(['work2', 'src', 'file1.in'], "file1.in 3\n")
+test.write(['work2', 'src', 'yyy'], "yyy 3\n")
+test.write(['work2', 'src', 'zzz'], "zzz 3\n")
+test.write(['work2', 'src', 'bbb.k'], "bbb.k 3\ninclude ccc\n")
+
+test.run(chdir='work2/src', arguments=args, stdout=test.wrap_stdout("""\
+scons: Cannot explain why `file1' is being rebuilt: No previous build information found
+%s %s file1 file1.in
+scons: Cannot explain why `file2' is being rebuilt: No previous build information found
+%s %s file2 file2.k
+scons: Cannot explain why `file3' is being rebuilt: No previous build information found
+%s %s file3 xxx yyy zzz
+scons: Cannot explain why `%s' is being rebuilt: No previous build information found
+Install file: "bbb.k" as "%s"
+scons: Cannot explain why `file5' is being rebuilt: No previous build information found
+%s %s file5 file5.k
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work2', 'inc', 'bbb.k'),
+       test.workpath('work2', 'inc', 'bbb.k'),
+       python, cat_py)))
+
+test.must_match(['work2', 'src', 'file1'], "file1.in 3\n")
+test.must_match(['work2', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 3
+file2.k 1 line 4
+""")
+test.must_match(['work2', 'src', 'file3'], "xxx 1\nyyy 3\nzzz 3\n")
+test.must_match(['work2', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 3
+ccc 1
+file5.k 1 line 4
+""")
+
+# ...so if we now update the files again, it should be able to tell
+# us why the files changed.
+test.write(['work2', 'src', 'file1.in'], "file1.in 4\n")
+test.write(['work2', 'src', 'yyy'], "yyy 4\n")
+test.write(['work2', 'src', 'zzz'], "zzz 4\n")
+test.write(['work2', 'src', 'bbb.k'], "bbb.k 4\ninclude ccc\n")
+
+test.run(chdir='work2/src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file1' because `file1.in' changed
+%s %s file1 file1.in
+scons: rebuilding `file2' because `yyy' changed
+%s %s file2 file2.k
+scons: rebuilding `file3' because:
+           `yyy' changed
+           `zzz' changed
+%s %s file3 xxx yyy zzz
+scons: rebuilding `%s' because `bbb.k' changed
+Install file: "bbb.k" as "%s"
+scons: rebuilding `file5' because `%s' changed
+%s %s file5 file5.k
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work2', 'inc', 'bbb.k'),
+       test.workpath('work2', 'inc', 'bbb.k'),
+       test.workpath('work2', 'inc', 'bbb.k'),
+       python, cat_py)))
+
+test.must_match(['work2', 'src', 'file1'], "file1.in 4\n")
+test.must_match(['work2', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 4
+file2.k 1 line 4
+""")
+test.must_match(['work2', 'src', 'file3'], "xxx 1\nyyy 4\nzzz 4\n")
+test.must_match(['work2', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 4
+ccc 1
+file5.k 1 line 4
+""")
+
+#############################################################################
+# Now test (in a separate workspace) how things function when
+# we tell SCons to not save the --debug=explain info
+# using --save-explain-info=0'.
+test.write(['work3', 'src', 'SConstruct'], SConstruct_contents)
+
+test.write(['work3', 'src', 'SConscript'], """\
+Import("env")
+env.Cat('file1', 'file1.in')
+env.Cat('file2', 'file2.k')
+env.Cat('file3', ['xxx', 'yyy', 'zzz'])
+env.Command('file4', 'file4.in', r"%s %s $TARGET - $SOURCES")
+env.Cat('file5', 'file5.k')
+env.Cat('subdir/file6', 'subdir/file6.in')
+""" % (python, cat_py))
+
+test.write(['work3', 'src', 'aaa'], "aaa 1\n")
+test.write(['work3', 'src', 'bbb.k'], """\
+bbb.k 1
+include ccc
+include ../inc/ddd
+include ../inc/eee
+""")
+test.write(['work3', 'src', 'ccc'], "ccc 1\n")
+test.write(['work3', 'src', 'ddd'], "ddd 1\n")
+test.write(['work3', 'src', 'eee.in'], "eee.in 1\n")
+
+test.write(['work3', 'src', 'file1.in'], "file1.in 1\n")
+
+test.write(['work3', 'src', 'file2.k'], """\
+file2.k 1 line 1
+include xxx
+include yyy
+file2.k 1 line 4
+""")
+
+test.write(['work3', 'src', 'file4.in'], "file4.in 1\n")
+
+test.write(['work3', 'src', 'xxx'], "xxx 1\n")
+test.write(['work3', 'src', 'yyy'], "yyy 1\n")
+test.write(['work3', 'src', 'zzz'], "zzz 1\n")
+
+test.write(['work3', 'src', 'file5.k'], """\
+file5.k 1 line 1
+include ../inc/aaa
+include ../inc/bbb.k
+file5.k 1 line 4
+""")
+
+test.write(['work3', 'src', 'subdir', 'file6.in'], "subdir/file6.in 1\n")
+
+# First, even without build info and regardless of storage option,
+# we can tell the user that things are being built because they don't exist.
+test.run(chdir='work3/src',
+         arguments='--debug=explain --save-explain-info=0 .',
+         stdout=test.wrap_stdout("""\
+scons: building `file1' because it doesn't exist
+%s %s file1 file1.in
+scons: building `file2' because it doesn't exist
+%s %s file2 file2.k
+scons: building `file3' because it doesn't exist
+%s %s file3 xxx yyy zzz
+scons: building `file4' because it doesn't exist
+%s %s file4 - file4.in
+scons: building `%s' because it doesn't exist
+Install file: "aaa" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "ddd" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "eee.in" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "bbb.k" as "%s"
+scons: building `file5' because it doesn't exist
+%s %s file5 file5.k
+scons: building `%s' because it doesn't exist
+%s %s %s %s
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work3', 'inc', 'aaa'),
+       test.workpath('work3', 'inc', 'aaa'),
+       test.workpath('work3', 'inc', 'ddd'),
+       test.workpath('work3', 'inc', 'ddd'),
+       test.workpath('work3', 'inc', 'eee'),
+       test.workpath('work3', 'inc', 'eee'),
+       test.workpath('work3', 'inc', 'bbb.k'),
+       test.workpath('work3', 'inc', 'bbb.k'),
+       python, cat_py,
+       subdir_file6,
+       python, cat_py, subdir_file6, subdir_file6_in)))
+
+test.must_match(['work3', 'src', 'file1'], "file1.in 1\n")
+test.must_match(['work3', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 1
+file2.k 1 line 4
+""")
+test.must_match(['work3', 'src', 'file3'], "xxx 1\nyyy 1\nzzz 1\n")
+test.must_match(['work3', 'src', 'file4'], "file4.in 1\n")
+test.must_match(['work3', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 1
+ccc 1
+ddd 1
+eee.in 1
+file5.k 1 line 4
+""")
+
+# Using --debug=explain above will have actually saved the build info;
+# run again to clear it out.
+test.write(['work3', 'src', 'file1.in'], "file1.in 2\n")
+test.write(['work3', 'src', 'yyy'], "yyy 2\n")
+test.write(['work3', 'src', 'zzz'], "zzz 2\n")
+test.write(['work3', 'src', 'bbb.k'], "bbb.k 2\ninclude ccc\n")
+
+test.run(chdir='work3/src', arguments='--save-explain-info=0 .')
+
+# Now, it should tell us that it can't explain why the files are
+# being rebuilt.  It should also *store* the build info because
+# we're using --debug=explain...
+test.write(['work3', 'src', 'file1.in'], "file1.in 3\n")
+test.write(['work3', 'src', 'yyy'], "yyy 3\n")
+test.write(['work3', 'src', 'zzz'], "zzz 3\n")
+test.write(['work3', 'src', 'bbb.k'], "bbb.k 3\ninclude ccc\n")
+
+test.run(chdir='work3/src',
+         arguments='--debug=explain .',
+         stdout=test.wrap_stdout("""\
+scons: Cannot explain why `file1' is being rebuilt: No previous build information found
+%s %s file1 file1.in
+scons: Cannot explain why `file2' is being rebuilt: No previous build information found
+%s %s file2 file2.k
+scons: Cannot explain why `file3' is being rebuilt: No previous build information found
+%s %s file3 xxx yyy zzz
+scons: Cannot explain why `%s' is being rebuilt: No previous build information found
+Install file: "bbb.k" as "%s"
+scons: Cannot explain why `file5' is being rebuilt: No previous build information found
+%s %s file5 file5.k
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work3', 'inc', 'bbb.k'),
+       test.workpath('work3', 'inc', 'bbb.k'),
+       python, cat_py)))
+
+test.must_match(['work3', 'src', 'file1'], "file1.in 3\n")
+test.must_match(['work3', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 3
+file2.k 1 line 4
+""")
+test.must_match(['work3', 'src', 'file3'], "xxx 1\nyyy 3\nzzz 3\n")
+test.must_match(['work3', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 3
+ccc 1
+file5.k 1 line 4
+""")
+
+# ...so if we now update the files again, it should be able to tell
+# us why the files changed.
+test.write(['work3', 'src', 'file1.in'], "file1.in 4\n")
+test.write(['work3', 'src', 'yyy'], "yyy 4\n")
+test.write(['work3', 'src', 'zzz'], "zzz 4\n")
+test.write(['work3', 'src', 'bbb.k'], "bbb.k 4\ninclude ccc\n")
+
+test.run(chdir='work3/src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file1' because `file1.in' changed
+%s %s file1 file1.in
+scons: rebuilding `file2' because `yyy' changed
+%s %s file2 file2.k
+scons: rebuilding `file3' because:
+           `yyy' changed
+           `zzz' changed
+%s %s file3 xxx yyy zzz
+scons: rebuilding `%s' because `bbb.k' changed
+Install file: "bbb.k" as "%s"
+scons: rebuilding `file5' because `%s' changed
+%s %s file5 file5.k
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work3', 'inc', 'bbb.k'),
+       test.workpath('work3', 'inc', 'bbb.k'),
+       test.workpath('work3', 'inc', 'bbb.k'),
+       python, cat_py)))
+
+test.must_match(['work3', 'src', 'file1'], "file1.in 4\n")
+test.must_match(['work3', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 4
+file2.k 1 line 4
+""")
+test.must_match(['work3', 'src', 'file3'], "xxx 1\nyyy 4\nzzz 4\n")
+test.must_match(['work3', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 4
+ccc 1
+file5.k 1 line 4
+""")
+
+#############################################################################
+# Test that the --debug=explain information gets saved by default.
+test.write(['work4', 'src', 'SConstruct'], SConstruct_contents)
+
+test.write(['work4', 'src', 'SConscript'], """\
+Import("env")
+env.Cat('file1', 'file1.in')
+env.Cat('file2', 'file2.k')
+env.Cat('file3', ['xxx', 'yyy', 'zzz'])
+env.Command('file4', 'file4.in', r"%s %s $TARGET - $SOURCES")
+env.Cat('file5', 'file5.k')
+env.Cat('subdir/file6', 'subdir/file6.in')
+""" % (python, cat_py))
+
+test.write(['work4', 'src', 'aaa'], "aaa 1\n")
+test.write(['work4', 'src', 'bbb.k'], """\
+bbb.k 1
+include ccc
+include ../inc/ddd
+include ../inc/eee
+""")
+test.write(['work4', 'src', 'ccc'], "ccc 1\n")
+test.write(['work4', 'src', 'ddd'], "ddd 1\n")
+test.write(['work4', 'src', 'eee.in'], "eee.in 1\n")
+
+test.write(['work4', 'src', 'file1.in'], "file1.in 1\n")
+
+test.write(['work4', 'src', 'file2.k'], """\
+file2.k 1 line 1
+include xxx
+include yyy
+file2.k 1 line 4
+""")
+
+test.write(['work4', 'src', 'file4.in'], "file4.in 1\n")
+
+test.write(['work4', 'src', 'xxx'], "xxx 1\n")
+test.write(['work4', 'src', 'yyy'], "yyy 1\n")
+test.write(['work4', 'src', 'zzz'], "zzz 1\n")
+
+test.write(['work4', 'src', 'file5.k'], """\
+file5.k 1 line 1
+include ../inc/aaa
+include ../inc/bbb.k
+file5.k 1 line 4
+""")
+
+test.write(['work4', 'src', 'subdir', 'file6.in'], "subdir/file6.in 1\n")
+
+#
+test.run(chdir='work4/src', arguments='.')
+
+test.must_match(['work4', 'src', 'file1'], "file1.in 1\n")
+test.must_match(['work4', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 1
+file2.k 1 line 4
+""")
+test.must_match(['work4', 'src', 'file3'], "xxx 1\nyyy 1\nzzz 1\n")
+test.must_match(['work4', 'src', 'file4'], "file4.in 1\n")
+test.must_match(['work4', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 1
+ccc 1
+ddd 1
+eee.in 1
+file5.k 1 line 4
+""")
+
+#
+test.write(['work4', 'src', 'file1.in'], "file1.in 2\n")
+test.write(['work4', 'src', 'yyy'], "yyy 2\n")
+test.write(['work4', 'src', 'zzz'], "zzz 2\n")
+test.write(['work4', 'src', 'bbb.k'], "bbb.k 2\ninclude ccc\n")
+
+test.run(chdir='work4/src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file1' because `file1.in' changed
+%s %s file1 file1.in
+scons: rebuilding `file2' because `yyy' changed
+%s %s file2 file2.k
+scons: rebuilding `file3' because:
+           `yyy' changed
+           `zzz' changed
+%s %s file3 xxx yyy zzz
+scons: rebuilding `%s' because:
+           `%s' is no longer a dependency
+           `%s' is no longer a dependency
+           `bbb.k' changed
+Install file: "bbb.k" as "%s"
+scons: rebuilding `file5' because `%s' changed
+%s %s file5 file5.k
+""" % (python, cat_py,
+       python, cat_py,
+       python, cat_py,
+       test.workpath('work4', 'inc', 'bbb.k'),
+       test.workpath('work4', 'inc', 'ddd'),
+       test.workpath('work4', 'inc', 'eee'),
+       test.workpath('work4', 'inc', 'bbb.k'),
+       test.workpath('work4', 'inc', 'bbb.k'),
+       python, cat_py)))
+
+test.must_match(['work4', 'src', 'file1'], "file1.in 2\n")
+test.must_match(['work4', 'src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 2
+file2.k 1 line 4
+""")
+test.must_match(['work4', 'src', 'file3'], "xxx 1\nyyy 2\nzzz 2\n")
+test.must_match(['work4', 'src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 2
+ccc 1
+file5.k 1 line 4
+""")
+
 test.pass_test()