From c2859211ba26b08f89505924f622f0839ab9853d Mon Sep 17 00:00:00 2001 From: stevenknight Date: Mon, 27 Jan 2003 03:55:51 +0000 Subject: [PATCH] Provide a better error message when a BuildDir() is read-only. git-svn-id: http://scons.tigris.org/svn/scons/trunk@567 fdb21ef1-2011-0410-befe-b5e4ea1792b1 --- src/CHANGES.txt | 3 + src/engine/SCons/Node/FS.py | 6 +- src/engine/SCons/Node/FSTests.py | 22 +++++ src/engine/SCons/Script/__init__.py | 18 +++- src/engine/SCons/Taskmaster.py | 39 ++++++-- src/engine/SCons/TaskmasterTests.py | 55 ++++++++++- test/BuildDir-errors.py | 142 ++++++++++++++++++++++++++++ 7 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 test/BuildDir-errors.py diff --git a/src/CHANGES.txt b/src/CHANGES.txt index 49346b49..ce88f015 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -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 diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index 7b77736c..0978b574 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -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 diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 51dcc1ba..83bdccfd 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -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') diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py index 92572668..a14c7ae4 100644 --- a/src/engine/SCons/Script/__init__.py +++ b/src/engine/SCons/Script/__init__.py @@ -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: diff --git a/src/engine/SCons/Taskmaster.py b/src/engine/SCons/Taskmaster.py index 645c7df2..10f074f5 100644 --- a/src/engine/SCons/Taskmaster.py +++ b/src/engine/SCons/Taskmaster.py @@ -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 diff --git a/src/engine/SCons/TaskmasterTests.py b/src/engine/SCons/TaskmasterTests.py index 577d7831..cc0b437d 100644 --- a/src/engine/SCons/TaskmasterTests.py +++ b/src/engine/SCons/TaskmasterTests.py @@ -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 index 00000000..dcfce09e --- /dev/null +++ b/test/BuildDir-errors.py @@ -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() -- 2.26.2