Provide a better error message when a BuildDir() is read-only.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Mon, 27 Jan 2003 03:55:51 +0000 (03:55 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Mon, 27 Jan 2003 03:55:51 +0000 (03:55 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@567 fdb21ef1-2011-0410-befe-b5e4ea1792b1

src/CHANGES.txt
src/engine/SCons/Node/FS.py
src/engine/SCons/Node/FSTests.py
src/engine/SCons/Script/__init__.py
src/engine/SCons/Taskmaster.py
src/engine/SCons/TaskmasterTests.py
test/BuildDir-errors.py [new file with mode: 0644]

index 49346b492136d75cf1adb5cbc08d5a4f99a1d77a..ce88f015588b9a01f89928a34d92910113799d35 100644 (file)
@@ -42,6 +42,9 @@ RELEASE 0.11 - XXX
   - Fix adding a prefix to a file when the target isn't specified.
     (Bug reported by Esa Ilari Vuokko.)
 
+  - Clean up error messages from problems duplicating into read-only
+    BuildDir directories or into read-only files.
+
   From Steve Leblanc:
 
   - Fix the output of -c -n when directories are involved, so it
index 7b77736c408741250b131f38c9a35238f07558da..0978b574a9f263a62cb50d95b570aa8cde79c953 100644 (file)
@@ -971,7 +971,11 @@ class File(Entry):
                     Unlink(self, None, None)
                 except OSError:
                     pass
-                Link(self, src, None)
+                try:
+                    Link(self, src, None)
+                except IOError, e:
+                    desc = "Cannot duplicate `%s' in `%s': %s." % (src, self.dir, e.strerror)
+                    raise SCons.Errors.StopError, desc
                 self.linked = 1
                 # The Link() action may or may not have actually
                 # created the file, depending on whether the -n
index 51dcc1bac31ee725f290b4ca39586940133bfcdc..83bdccfde361c0ed6112129183e384445c0fef6e 100644 (file)
@@ -288,6 +288,28 @@ class BuildDirTestCase(unittest.TestCase):
         finally:
             SCons.Node.FS.Mkdir = save_Mkdir
             SCons.Node.FS.Link = save_Link
+
+        # Test that an IOError trying to Link a src file
+        # into a BuildDir ends up throwing a StopError.
+        fIO = fs.File("build/var2/IOError")
+
+        save_Link = SCons.Node.FS.Link
+        def Link_IOError(target, source, env):
+            raise IOError, "Link_IOError"
+        SCons.Node.FS.Link = Link_IOError
+
+        test.write(['work', 'src', 'IOError'], "work/src/IOError\n")
+
+        try:
+            exc_caught = 0
+            try:
+                fIO.exists()
+            except SCons.Errors.StopError:
+                exc_caught = 1
+            assert exc_caught, "Should have caught a StopError"
+
+        finally:
+            SCons.Node.FS.Link = save_Link
         
         # Test to see if Link() works...
         test.subdir('src','build')
index 92572668b096b95a5070fcb5ef021504ca62c206..a14c7ae4be07eb3a7a4979a6193b6b2f83ea09d8 100644 (file)
@@ -65,7 +65,6 @@ import SCons.Taskmaster
 from SCons.Util import display
 import SCons.Warnings
 
-
 #
 # Task control.
 #
@@ -642,8 +641,6 @@ class OptParser(OptionParser):
 
 
 def _main():
-    import SCons.Node
-
     targets = []
 
     # Enable deprecated warnings by default.
@@ -753,8 +750,19 @@ def _main():
     display("scons: Reading SConscript files ...")
     try:
         start_time = time.time()
-        for script in scripts:
-            SCons.Script.SConscript.SConscript(script)
+        try:
+            for script in scripts:
+                SCons.Script.SConscript.SConscript(script)
+        except SCons.Errors.StopError, e:
+            # We had problems reading an SConscript file, such as it
+            # couldn't be copied in to the BuildDir.  Since we're just
+            # reading SConscript files and haven't started building
+            # things yet, stop regardless of whether they used -i or -k
+            # or anything else, but don't say "Stop." on the message.
+            global exit_status
+            sys.stderr.write("scons: *** %s\n" % e)
+            exit_status = 2
+            sys.exit(exit_status)
         global sconscript_time
         sconscript_time = time.time() - start_time
     except PrintHelp, text:
index 645c7df2a3c92abcaef687dde9bf4f41681a20bd..10f074f581d23efc8726f3c70e1a8143efbdbcb0 100644 (file)
@@ -73,6 +73,13 @@ class Task:
         This method is called from multiple threads in a parallel build,
         so only do thread safe stuff here.  Do thread unsafe stuff in
         prepare(), executed() or failed()."""
+        try:
+            # We recorded an exception while getting this Task ready
+            # for execution.  Raise it now.
+            raise self.node.exc_type, self.node.exc_value
+        except AttributeError:
+            # The normal case:  no exception to raise.
+            pass
         try:
             self.targets[0].build()
         except KeyboardInterrupt:
@@ -198,7 +205,18 @@ class Taskmaster:
             # keep track of which nodes are in the execution stack:
             node.set_state(SCons.Node.stack)
 
-            children = node.children()
+            try:
+                children = node.children()
+            except:
+                # We had a problem just trying to figure out the
+                # children (like a child couldn't be linked in to a
+                # BuildDir).  Arrange to raise the exception when the
+                # Task is "executed."
+                node.exc_type = sys.exc_type
+                node.exc_value = sys.exc_value
+                self.candidates.pop()
+                self.ready = node
+                break
 
             # detect dependency cycles:
             def in_stack(node): return node.get_state() == SCons.Node.stack
@@ -236,10 +254,11 @@ class Taskmaster:
                 node.set_state(SCons.Node.pending)
                 self.candidates.pop()
                 continue
-            else:
-                self.candidates.pop()
-                self.ready = node
-                break
+
+            # The default when we've gotten through all of the checks above.
+            self.candidates.pop()
+            self.ready = node
+            break
 
     def next_task(self):
         """Return the next task to be executed."""
@@ -259,7 +278,15 @@ class Taskmaster:
         self.executing.extend(node.side_effects)
         
         task = self.tasker(self, tlist, node in self.targets, node)
-        task.make_ready()
+        try:
+            task.make_ready()
+        except:
+            # We had a problem just trying to get this task ready (like
+            # a child couldn't be linked in to a BuildDir when deciding
+            # whether this node is current).  Arrange to raise the
+            # exception when the Task is "executed."
+            node.exc_type = sys.exc_type
+            node.exc_value = sys.exc_value
         self.ready = None
 
         return task
index 577d78312f76ae3b6d279bccdeb6b4b400374521..cc0b437d813fc51e554ebe85eea83c63f267ea98 100644 (file)
@@ -121,6 +121,9 @@ class Node:
 class OtherError(Exception):
     pass
 
+class MyException(Exception):
+    pass
+
 
 class TaskmasterTestCase(unittest.TestCase):
 
@@ -344,6 +347,32 @@ class TaskmasterTestCase(unittest.TestCase):
         assert not tm.next_task()
         t.executed()
 
+    def test_make_ready_exception(self):
+        """Test handling exceptions from Task.make_ready()
+        """
+        class MyTask(SCons.Taskmaster.Task):
+            def make_ready(self):
+                raise MyException, "from make_ready()"
+
+        n1 = Node("n1")
+        tm = SCons.Taskmaster.Taskmaster(targets = [n1], tasker = MyTask)
+        t = tm.next_task()
+        assert n1.exc_type == MyException, n1.exc_type
+        assert str(n1.exc_value) == "from make_ready()", n1.exc_value
+
+
+    def test_children_errors(self):
+        """Test errors when fetching the children of a node.
+        """
+        class MyNode(Node):
+            def children(self):
+                raise SCons.Errors.StopError, "stop!"
+        n1 = MyNode("n1")
+        tm = SCons.Taskmaster.Taskmaster([n1])
+        t = tm.next_task()
+        assert n1.exc_type == SCons.Errors.StopError, "Did not record StopError on node"
+        assert str(n1.exc_value) == "stop!", "Unexpected exc_value `%s'" % n1.exc_value
+
     def test_cycle_detection(self):
         """Test detecting dependency cycles
 
@@ -479,6 +508,9 @@ class TaskmasterTestCase(unittest.TestCase):
         else:
             raise TestFailed, "did not catch expected BuildError"
 
+        # On a generic (non-BuildError) exception from a Builder,
+        # the target should throw a BuildError exception with the
+        # args set to the exception value, instance, and traceback.
         def raise_OtherError():
             raise OtherError
         n4 = Node("n4")
@@ -488,9 +520,6 @@ class TaskmasterTestCase(unittest.TestCase):
         try:
             t.execute()
         except SCons.Errors.BuildError, e:
-            # On a generic (non-BuildError) exception from a Builder,
-            # the target should throw a BuildError exception with the
-            # args set to the exception value, instance, and traceback.
             assert e.node == n4, e.node
             assert e.errstr == "Exception", e.errstr
             assert len(e.args) == 3, `e.args`
@@ -500,6 +529,26 @@ class TaskmasterTestCase(unittest.TestCase):
         else:
             raise TestFailed, "did not catch expected BuildError"
 
+        # If the Node has had an exception recorded (during
+        # preparation), then execute() should raise that exception,
+        # not build the Node.
+        class MyException(Exception):
+            pass
+
+        built_text = None
+        n5 = Node("n5")
+        n5.exc_type = MyException
+        n5.exc_value = "exception value"
+        tm = SCons.Taskmaster.Taskmaster([n5])
+        t = tm.next_task()
+        exc_caught = None
+        try:
+            t.execute()
+        except MyException, v:
+            assert str(v) == "exception value", v
+            exc_caught = 1
+        assert exc_caught, "did not catch expected MyException"
+        assert built_text is None, built_text
 
 
 
diff --git a/test/BuildDir-errors.py b/test/BuildDir-errors.py
new file mode 100644 (file)
index 0000000..dcfce09
--- /dev/null
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+"""
+Validate successful handling of errors when duplicating things in
+BuildDirs.  This is generally when the BuildDir, or something in it,
+is read-only.
+"""
+
+import os
+import os.path
+import stat
+import sys
+import TestSCons
+
+test = TestSCons.TestSCons()
+
+for dir in ['normal', 'ro-dir', 'ro-SConscript', 'ro-src']:
+    test.subdir(dir, [dir, 'src'])
+
+    test.write([dir, 'SConstruct'], """\
+import os.path
+BuildDir('build', 'src')
+SConscript(os.path.join('build', 'SConscript'))
+""") 
+
+    test.write([dir, 'src', 'SConscript'], """\
+def fake_scan(node, env, target):
+    # We fetch the contents here, even though we don't examine
+    # them, because get_contents() will cause the engine to
+    # try to link the source file into the build directory,
+    # potentially triggering a different failure case.
+    contents = node.get_contents()
+    return []
+
+def cat(env, source, target):
+    target = str(target[0])
+    source = map(str, source)
+    f = open(target, "wb")
+    for src in source:
+        f.write(open(src, "rb").read())
+    f.close()
+
+env = Environment(BUILDERS={'Build':Builder(action=cat)},
+                  SCANNERS=[Scanner(fake_scan, skeys = ['.in'])])
+env.Build('file.out', 'file.in')
+""")
+
+    test.write([dir, 'src', 'file.in'], dir + "/src/file.in\n")
+
+# Just verify that the normal case works fine.
+test.run(chdir = 'normal', arguments = ".")
+
+test.fail_test(test.read(['normal', 'build', 'file.out']) != "normal/src/file.in\n")
+
+# Verify the error when the BuildDir itself is read-only.
+dir = os.path.join('ro-dir', 'build')
+test.subdir(dir)
+os.chmod(dir, os.stat(dir)[stat.ST_MODE] & ~stat.S_IWUSR)
+
+test.run(chdir = 'ro-dir',
+         arguments = ".",
+         status = 2,
+         stderr = "scons: *** Cannot duplicate `%s' in `build': Permission denied.\n" % os.path.join('src', 'SConscript'))
+
+# Verify the error when the SConscript file within the BuildDir is
+# read-only.  Note that we have to make the directory read-only too,
+# because otherwise our duplication logic will be able to unlink
+# the read-only SConscript and duplicate the new one.
+dir = os.path.join('ro-SConscript', 'build')
+test.subdir(dir)
+SConscript = test.workpath(dir, 'SConscript')
+test.write(SConscript, '')
+os.chmod(SConscript, os.stat(SConscript)[stat.ST_MODE] & ~stat.S_IWUSR)
+f = open(SConscript, 'r')
+os.chmod(dir, os.stat(dir)[stat.ST_MODE] & ~stat.S_IWUSR)
+
+test.run(chdir = 'ro-SConscript',
+         arguments = ".",
+         status = 2,
+         stderr = "scons: *** Cannot duplicate `%s' in `build': Permission denied.\n" % os.path.join('src', 'SConscript'))
+
+os.chmod('ro-SConscript', os.stat('ro-SConscript')[stat.ST_MODE] | stat.S_IWUSR)
+f.close()
+
+test.run(chdir = 'ro-SConscript',
+         arguments = ".",
+         status = 2,
+         stderr = "scons: *** Cannot duplicate `%s' in `build': Permission denied.\n" % os.path.join('src', 'SConscript'))
+
+# Verify the error when the source file within the BuildDir is
+# read-only.  Note that we have to make the directory read-only too,
+# because otherwise our duplication logic will be able to unlink the
+# read-only source file and duplicate the new one.  But because we've
+# made the BuildDir read-only, we must also create a writable SConscript
+# file there so it can be duplicated from the source directory.
+dir = os.path.join('ro-src', 'build')
+test.subdir(dir)
+test.write([dir, 'SConscript'], '')
+file_in = test.workpath(dir, 'file.in')
+test.write(file_in, '')
+os.chmod(file_in, os.stat(file_in)[stat.ST_MODE] & ~stat.S_IWUSR)
+f = open(file_in, 'r')
+os.chmod(dir, os.stat(dir)[stat.ST_MODE] & ~stat.S_IWUSR)
+
+test.run(chdir = 'ro-src',
+         arguments = ".",
+         status = 2,
+         stderr = "scons: *** Cannot duplicate `%s' in `build': Permission denied.  Stop.\n" % os.path.join('src', 'file.in'))
+
+test.run(chdir = 'ro-src',
+         arguments = "-k .",
+         status = 2,
+         stderr = "scons: *** Cannot duplicate `%s' in `build': Permission denied.\n" % os.path.join('src', 'file.in'))
+
+f.close()
+
+#
+test.pass_test()