diff options
| author | Steven Knight <knight@baldmt.com> | 2001-09-17 04:57:00 (GMT) |
|---|---|---|
| committer | Steven Knight <knight@baldmt.com> | 2001-09-17 04:57:00 (GMT) |
| commit | 3c81bb2bd0e009c0ee81570e17b0f87ad8d034ab (patch) | |
| tree | 1a08c189644909cdadc489cce0eaa487e2c6f578 /src/engine | |
| parent | e2faf9c21bc7712fcdc547b7df0f12a6b2177601 (diff) | |
| download | SCons-3c81bb2bd0e009c0ee81570e17b0f87ad8d034ab.zip SCons-3c81bb2bd0e009c0ee81570e17b0f87ad8d034ab.tar.gz SCons-3c81bb2bd0e009c0ee81570e17b0f87ad8d034ab.tar.bz2 | |
Run setup.py on the unpacked .tar.gz for testing.
Diffstat (limited to 'src/engine')
31 files changed, 2096 insertions, 0 deletions
diff --git a/src/engine/MANIFEST b/src/engine/MANIFEST new file mode 100644 index 0000000..51c6822 --- /dev/null +++ b/src/engine/MANIFEST @@ -0,0 +1,16 @@ +MANIFEST +SCons/__init__.py +SCons/Builder.py +SCons/Defaults.py +SCons/Environment.py +SCons/Errors.py +SCons/Job.py +SCons/exitfuncs.py +SCons/Node/__init__.py +SCons/Node/FS.py +SCons/Scanner/__init__.py +SCons/Scanner/C.py +SCons/Sig/__init__.py +SCons/Sig/MD5.py +SCons/Sig/TimeStamp.py +setup.py diff --git a/src/engine/SCons/.aeignore b/src/engine/SCons/.aeignore new file mode 100644 index 0000000..43fe851 --- /dev/null +++ b/src/engine/SCons/.aeignore @@ -0,0 +1,4 @@ +*,D +*.pyc +.*.swp +.consign diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py new file mode 100644 index 0000000..f1ab100 --- /dev/null +++ b/src/engine/SCons/Builder.py @@ -0,0 +1,100 @@ +"""SCons.Builder + +XXX + +""" + +__revision__ = "Builder.py __REVISION__ __DATE__ __DEVELOPER__" + + + +import os +import SCons.Node.FS +import types + + + +class Builder: + """Base class for Builders, objects that create output + nodes (files) from input nodes (files). + """ + + def __init__(self, name = None, + action = None, + input_suffix = None, + output_suffix = None, + node_class = SCons.Node.FS.File): + self.name = name + self.action = Action(action) + self.insuffix = input_suffix + self.outsuffix = output_suffix + self.node_class = node_class + if not self.insuffix is None and self.insuffix[0] != '.': + self.insuffix = '.' + self.insuffix + if not self.outsuffix is None and self.outsuffix[0] != '.': + self.outsuffix = '.' + self.outsuffix + + def __cmp__(self, other): + return cmp(self.__dict__, other.__dict__) + + def __call__(self, env, target = None, source = None): + node = SCons.Node.FS.lookup(self.node_class, target) + node.builder_set(self) + node.env_set(self) + node.sources = source # XXX REACHING INTO ANOTHER OBJECT + return node + + def execute(self, **kw): + """Execute a builder's action to create an output object. + """ + apply(self.action.execute, (), kw) + + + +print_actions = 1; +execute_actions = 1; + + + +def Action(act): + """A factory for action objects.""" + if type(act) == types.FunctionType: + return FunctionAction(act) + elif type(act) == types.StringType: + return CommandAction(act) + else: + return None + +class ActionBase: + """Base class for actions that create output objects. + + We currently expect Actions will only be accessible through + Builder objects, so they don't yet merit their own module.""" + def __cmp__(self, other): + return cmp(self.__dict__, other.__dict__) + + def show(self, string): + print string + +class CommandAction(ActionBase): + """Class for command-execution actions.""" + def __init__(self, string): + self.command = string + + def execute(self, **kw): + cmd = self.command % kw + if print_actions: + self.show(cmd) + if execute_actions: + os.system(cmd) + +class FunctionAction(ActionBase): + """Class for Python function actions.""" + def __init__(self, function): + self.function = function + + def execute(self, **kw): + # if print_actions: + # XXX: WHAT SHOULD WE PRINT HERE? + if execute_actions: + self.function(kw) diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py new file mode 100644 index 0000000..67435bc --- /dev/null +++ b/src/engine/SCons/BuilderTests.py @@ -0,0 +1,113 @@ +__revision__ = "BuilderTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import sys +import unittest + +import TestCmd +import SCons.Builder + + +# Initial setup of the common environment for all tests, +# a temporary working directory containing a +# script for writing arguments to an output file. +# +# We don't do this as a setUp() method because it's +# unnecessary to create a separate directory and script +# for each test, they can just use the one. +test = TestCmd.TestCmd(workdir = '') + +test.write('act.py', """import os, string, sys +f = open(sys.argv[1], 'w') +f.write("act.py: " + string.join(sys.argv[2:]) + "\\n") +f.close() +sys.exit(0) +""") + +act_py = test.workpath('act.py') +outfile = test.workpath('outfile') + + +class BuilderTestCase(unittest.TestCase): + + def test_action(self): + """Test Builder creation + + Verify that we can retrieve the supplied action attribute. + """ + builder = SCons.Builder.Builder(action = "foo") + assert builder.action.command == "foo" + + def test_cmp(self): + """Test simple comparisons of Builder objects + """ + b1 = SCons.Builder.Builder(input_suffix = '.o') + b2 = SCons.Builder.Builder(input_suffix = '.o') + assert b1 == b2 + b3 = SCons.Builder.Builder(input_suffix = '.x') + assert b1 != b3 + assert b2 != b3 + + def test_execute(self): + """Test execution of simple Builder objects + + One Builder is a string that executes an external command, + and one is an internal Python function. + """ + cmd = "python %s %s xyzzy" % (act_py, outfile) + builder = SCons.Builder.Builder(action = cmd) + builder.execute() + assert test.read(outfile, 'r') == "act.py: xyzzy\n" + + def function(kw): + import os, string, sys + f = open(kw['out'], 'w') + f.write("function\n") + f.close() + return not None + + builder = SCons.Builder.Builder(action = function) + builder.execute(out = outfile) + assert test.read(outfile, 'r') == "function\n" + + def test_insuffix(self): + """Test Builder creation with a specified input suffix + + Make sure that the '.' separator is appended to the + beginning if it isn't already present. + """ + builder = SCons.Builder.Builder(input_suffix = '.c') + assert builder.insuffix == '.c' + builder = SCons.Builder.Builder(input_suffix = 'c') + assert builder.insuffix == '.c' + + def test_name(self): + """Test Builder creation with a specified name + """ + builder = SCons.Builder.Builder(name = 'foo') + assert builder.name == 'foo' + + def test_node_class(self): + """Test a Builder that creates nodes of a specified class + """ + class Foo: + pass + builder = SCons.Builder.Builder(node_class = Foo) + assert builder.node_class is Foo + + def test_outsuffix(self): + """Test Builder creation with a specified output suffix + + Make sure that the '.' separator is appended to the + beginning if it isn't already present. + """ + builder = SCons.Builder.Builder(input_suffix = '.o') + assert builder.insuffix == '.o' + builder = SCons.Builder.Builder(input_suffix = 'o') + assert builder.insuffix == '.o' + + + +if __name__ == "__main__": + suite = unittest.makeSuite(BuilderTestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Defaults.py b/src/engine/SCons/Defaults.py new file mode 100644 index 0000000..56a15f7 --- /dev/null +++ b/src/engine/SCons/Defaults.py @@ -0,0 +1,22 @@ +"""SCons.Defaults + +Builders and other things for the local site. Here's where we'll +duplicate the functionality of autoconf until we move it into the +installation procedure or use something like qmconf. + +""" + +__revision__ = "local.py __REVISION__ __DATE__ __DEVELOPER__" + + + +import SCons.Builder + + + +Object = SCons.Builder.Builder(name = 'Object', + action = 'cc -c -o %(target)s %(source)s') +Program = SCons.Builder.Builder(name = 'Program', + action = 'cc -o %(target)s %(source)s') + +Builders = [Object, Program] diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py new file mode 100644 index 0000000..a3fe27a --- /dev/null +++ b/src/engine/SCons/Environment.py @@ -0,0 +1,133 @@ +"""SCons.Environment + +XXX + +""" + +__revision__ = "Environment.py __REVISION__ __DATE__ __DEVELOPER__" + + + +import copy +import re +import types + + + +def Command(): + pass # XXX + +def Install(): + pass # XXX + +def InstallAs(): + pass # XXX + + + +_cv = re.compile(r'%([_a-zA-Z]\w*|{[_a-zA-Z]\w*})') +_self = None + + + +def _deepcopy_atomic(x, memo): + return x +copy._deepcopy_dispatch[types.ModuleType] = _deepcopy_atomic +copy._deepcopy_dispatch[types.ClassType] = _deepcopy_atomic +copy._deepcopy_dispatch[types.FunctionType] = _deepcopy_atomic +copy._deepcopy_dispatch[types.MethodType] = _deepcopy_atomic +copy._deepcopy_dispatch[types.TracebackType] = _deepcopy_atomic +copy._deepcopy_dispatch[types.FrameType] = _deepcopy_atomic +copy._deepcopy_dispatch[types.FileType] = _deepcopy_atomic + + + +class Environment: + """Base class for construction Environments. These are + the primary objects used to communicate dependency and + construction information to the build engine. + + Keyword arguments supplied when the construction Environment + is created are construction variables used to initialize the + Environment. + """ + + def __init__(self, **kw): + self.Dictionary = {} + if kw.has_key('BUILDERS'): + builders = kw['BUILDERS'] + if not type(builders) is types.ListType: + kw['BUILDERS'] = [builders] + else: + import SCons.Defaults + kw['BUILDERS'] = SCons.Defaults.Builders[:] + self.Dictionary.update(copy.deepcopy(kw)) + + class BuilderWrapper: + """Wrapper class that allows an environment to + be associated with a Builder at instantiation. + """ + def __init__(self, env, builder): + self.env = env + self.builder = builder + + def __call__(self, target = None, source = None): + return self.builder(self.env, target, source) + + def execute(self, **kw): + apply(self.builder.execute, (), kw) + + for b in kw['BUILDERS']: + setattr(self, b.name, BuilderWrapper(self, b)) + + + + def __cmp__(self, other): + return cmp(self.Dictionary, other.Dictionary) + + def Builders(self): + pass # XXX + + def Copy(self, **kw): + """Return a copy of a construction Environment. The + copy is like a Python "deep copy"--that is, independent + copies are made recursively of each objects--except that + a reference is copied when an object is not deep-copyable + (like a function). There are no references to any mutable + objects in the original Environment. + """ + clone = copy.deepcopy(self) + apply(clone.Update, (), kw) + return clone + + def Scanners(self): + pass # XXX + + def Update(self, **kw): + """Update an existing construction Environment with new + construction variables and/or values. + """ + self.Dictionary.update(copy.deepcopy(kw)) + + def subst(self, string): + """Recursively interpolates construction variables from the + Environment into the specified string, returning the expanded + result. Construction variables are specified by a % prefix + in the string and begin with an initial underscore or + alphabetic character followed by any number of underscores + or alphanumeric characters. The construction variable names + may be surrounded by curly braces to separate the name from + trailing characters. + """ + global _self + _self = self # XXX NOT THREAD SAFE, BUT HOW ELSE DO WE DO THIS? + def repl(m): + key = m.group(1) + if key[:1] == '{' and key[-1:] == '}': + key = key[1:-1] + if _self.Dictionary.has_key(key): return _self.Dictionary[key] + else: return '' + n = 1 + while n != 0: + string, n = _cv.subn(repl, string) + return string diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py new file mode 100644 index 0000000..6ac4cb7 --- /dev/null +++ b/src/engine/SCons/EnvironmentTests.py @@ -0,0 +1,144 @@ +__revision__ = "EnvironmentTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import sys +import unittest + +from SCons.Environment import * + + + +built_it = {} + +class Builder: + """A dummy Builder class for testing purposes. "Building" + a target is simply setting a value in the dictionary. + """ + def __init__(self, name = None): + self.name = name + + def execute(self, target = None, source = None): + built_it[target] = 1 + + + +class EnvironmentTestCase(unittest.TestCase): + + def test_Builders(self): + """Test Builder execution through different environments + + One environment is initialized with a single + Builder object, one with a list of a single Builder + object, and one with a list of two Builder objects. + """ + global built_it + + b1 = Builder(name = 'builder1') + b2 = Builder(name = 'builder2') + + built_it = {} + env1 = Environment(BUILDERS = b1) + env1.builder1.execute(target = 'out1') + assert built_it['out1'] + + built_it = {} + env2 = Environment(BUILDERS = [b1]) + env1.builder1.execute(target = 'out1') + assert built_it['out1'] + + built_it = {} + env3 = Environment(BUILDERS = [b1, b2]) + env3.builder1.execute(target = 'out1') + env3.builder2.execute(target = 'out2') + env3.builder1.execute(target = 'out3') + assert built_it['out1'] + assert built_it['out2'] + assert built_it['out3'] + + def test_Command(self): + pass # XXX + + def test_Copy(self): + """Test construction Environment copying + + Update the copy independently afterwards and check that + the original remains intact (that is, no dangling + references point to objects in the copied environment). + Copy the original with some construction variable + updates and check that the original remains intact + and the copy has the updated values. + """ + env1 = Environment(XXX = 'x', YYY = 'y') + env2 = env1.Copy() + env1copy = env1.Copy() + env2.Update(YYY = 'yyy') + assert env1 != env2 + assert env1 == env1copy + + env3 = env1.Copy(XXX = 'x3', ZZZ = 'z3') + assert env3.Dictionary['XXX'] == 'x3' + assert env3.Dictionary['YYY'] == 'y' + assert env3.Dictionary['ZZZ'] == 'z3' + assert env1 == env1copy + + def test_Dictionary(self): + """Test retrieval of known construction variables + + Fetch them from the Dictionary and check for well-known + defaults that get inserted. + """ + env = Environment(XXX = 'x', YYY = 'y') + assert env.Dictionary['XXX'] == 'x' + assert env.Dictionary['YYY'] == 'y' + assert env.Dictionary.has_key('BUILDERS') + + def test_Environment(self): + """Test construction Environments creation + + Create two with identical arguments and check that + they compare the same. + """ + env1 = Environment(XXX = 'x', YYY = 'y') + env2 = Environment(XXX = 'x', YYY = 'y') + assert env1 == env2 + + def test_Install(self): + pass # XXX + + def test_InstallAs(self): + pass # XXX + + def test_Scanners(self): + pass # XXX + + def test_Update(self): + """Test updating an Environment with new construction variables + + After creation of the Environment, of course. + """ + env1 = Environment(AAA = 'a', BBB = 'b') + env1.Update(BBB = 'bbb', CCC = 'ccc') + env2 = Environment(AAA = 'a', BBB = 'bbb', CCC = 'c') + assert env1 != env2 + + def test_subst(self): + """Test substituting construction variables within strings + + Check various combinations, including recursive expansion + of variables into other variables. + """ + env = Environment(AAA = 'a', BBB = 'b') + str = env.subst("%AAA %{AAA}A %BBBB %BBB") + assert str == "a aA b", str + env = Environment(AAA = '%BBB', BBB = 'b', BBBA = 'foo') + str = env.subst("%AAA %{AAA}A %{AAA}B %BBB") + assert str == "b foo b", str + env = Environment(AAA = '%BBB', BBB = '%CCC', CCC = 'c') + str = env.subst("%AAA %{AAA}A %{AAA}B %BBB") + assert str == "c c", str + + + +if __name__ == "__main__": + suite = unittest.makeSuite(EnvironmentTestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Errors.py b/src/engine/SCons/Errors.py new file mode 100644 index 0000000..52facf7 --- /dev/null +++ b/src/engine/SCons/Errors.py @@ -0,0 +1,18 @@ +"""SCons.Errors + +This file contains the exception classes used to handle internal +and user errors in SCons. + +""" + +__revision__ = "Errors.py __REVISION__ __DATE__ __DEVELOPER__" + + + +class InternalError(Exception): + def __init__(self, args=None): + self.args = args + +class UserError(Exception): + def __init__(self, args=None): + self.args = args diff --git a/src/engine/SCons/ErrorsTests.py b/src/engine/SCons/ErrorsTests.py new file mode 100644 index 0000000..911b11b --- /dev/null +++ b/src/engine/SCons/ErrorsTests.py @@ -0,0 +1,28 @@ +__revision__ = "ErrorsTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import sys +import unittest +import SCons.Errors + + +class ErrorsTestCase(unittest.TestCase): + def test_InternalError(self): + """Test the InternalError exception.""" + try: + raise SCons.Errors.InternalError, "test internal error" + except SCons.Errors.InternalError, e: + assert e.args == "test internal error" + + def test_UserError(self): + """Test the UserError exception.""" + try: + raise SCons.Errors.UserError, "test user error" + except SCons.Errors.UserError, e: + assert e.args == "test user error" + + + +if __name__ == "__main__": + suite = unittest.makeSuite(ErrorsTestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Job.py b/src/engine/SCons/Job.py new file mode 100644 index 0000000..aa9d92a --- /dev/null +++ b/src/engine/SCons/Job.py @@ -0,0 +1,239 @@ +"""SCons.Job + +This module defines the Serial and Parallel classes that execute tasks to +complete a build. The Jobs class provides a higher level interface to start, +stop, and wait on jobs. + +""" + +__revision__ = "Job.py __REVISION__ __DATE__ __DEVELOPER__" + +class Jobs: + """An instance of this class initializes N jobs, and provides + methods for starting, stopping, and waiting on all N jobs. + """ + + def __init__(self, num, taskmaster): + """ + create 'num' jobs using the given taskmaster. + + If 'num' is equal to 0, then a serial job will be used, + otherwise 'num' parallel jobs will be used. + """ + + if num > 1: + self.jobs = [] + for i in range(num): + self.jobs.append(Parallel(taskmaster, self)) + else: + self.jobs = [Serial(taskmaster)] + + def start(self): + """start the jobs""" + + for job in self.jobs: + job.start() + + def wait(self): + """ wait for the jobs started with start() to finish""" + + for job in self.jobs: + job.wait() + + def stop(self): + """ + stop the jobs started with start() + + This function does not wait for the jobs to finish. + """ + + for job in self.jobs: + job.stop() + +class Serial: + """This class is used to execute tasks in series, and is more efficient + than Parallel, but is only appropriate for non-parallel builds. Only + one instance of this class should be in existence at a time. + + This class is not thread safe. + """ + + def __init__(self, taskmaster): + """Create a new serial job given a taskmaster. + + The taskmaster's next_task() method should return the next task + that needs to be executed, or None if there are no more tasks. The + taskmaster's executed() method will be called for each task when it + is successfully executed or failed() will be called if it failed to + execute (e.g. execute() raised an exception). The taskmaster's + is_blocked() method will not be called. """ + + self.taskmaster = taskmaster + + def start(self): + + """Start the job. This will begin pulling tasks from the taskmaster + and executing them, and return when there are no more tasks. If a task + fails to execute (i.e. execute() raises an exception), then the job will + stop.""" + + while 1: + task = self.taskmaster.next_task() + + if task is None: + break + + try: + task.execute() + except: + self.taskmaster.failed(task) + return + else: + self.taskmaster.executed(task) + + def stop(self): + """Serial jobs are always finished when start() returns, so there + is nothing to do here""" + + pass + + def wait(self): + """Serial jobs are always finished when start() returns, so there + is nothing to do here""" + pass + + +# The will hold a condition variable once the first parallel task +# is created. +cv = None + +class Parallel: + """This class is used to execute tasks in parallel, and is less + efficient than Serial, but is appropriate for parallel builds. Create + an instance of this class for each job or thread you want. + + This class is thread safe. + """ + + + def __init__(self, taskmaster, jobs): + """Create a new parallel job given a taskmaster, and a Jobs instance. + Multiple jobs will be using the taskmaster in parallel, but all + method calls to taskmaster methods are serialized by the jobs + themselves. + + The taskmaster's next_task() method should return the next task + that needs to be executed, or None if there are no more tasks. The + taskmaster's executed() method will be called for each task when it + is successfully executed or failed() will be called if the task + failed to execute (i.e. execute() raised an exception). The + taskmaster's is_blocked() method should return true iff there are + more tasks, but they can't be executed until one or more other + tasks have been executed. next_task() will be called iff + is_blocked() returned false. + + Note: calls to taskmaster are serialized, but calls to execute() on + distinct tasks are not serialized, because that is the whole point + of parallel jobs: they can execute multiple tasks + simultaneously. """ + + global cv + + # import threading here so that everything in the Job module + # but the Parallel class will work if the interpreter doesn't + # support threads + import threading + + self.taskmaster = taskmaster + self.jobs = jobs + self.thread = threading.Thread(None, self.__run) + self.stop_running = 0 + + if cv is None: + cv = threading.Condition() + + def start(self): + """Start the job. This will spawn a thread that will begin pulling + tasks from the task master and executing them. This method returns + immediately and doesn't wait for the jobs to be executed. + + If a task fails to execute (i.e. execute() raises an exception), + all jobs will be stopped. + + To stop the job, call stop(). + To wait for the job to finish, call wait(). + """ + self.thread.start() + + def stop(self): + """Stop the job. This will cause the job to finish after the + currently executing task is done. A job that has been stopped can + not be restarted. + + To wait for the job to finish, call wait(). + """ + + cv.acquire() + self.stop_running = 1 + # wake up the sleeping jobs so this job will end as soon as possible: + cv.notifyAll() + cv.release() + + def wait(self): + """Wait for the job to finish. A job is finished when either there + are no more tasks or the job has been stopped and it is no longer + executing a task. + + This method should only be called after start() has been called. + + To stop the job, call stop(). + """ + self.thread.join() + + def __run(self): + """private method that actually executes the tasks""" + + cv.acquire() + + try: + + while 1: + while self.taskmaster.is_blocked() and not self.stop_running: + cv.wait(None) + + # check this before calling next_task(), because + # this job may have been stopped because of a build + # failure: + if self.stop_running: + break + + task = self.taskmaster.next_task() + + if task == None: + break + + cv.release() + try: + try: + task.execute() + finally: + cv.acquire() + except: + self.taskmaster.failed(task) + # stop all jobs since there was a failure: + # (this will wake up any waiting jobs, so + # it isn't necessary to explicitly wake them + # here) + self.jobs.stop() + else: + self.taskmaster.executed(task) + + if not self.taskmaster.is_blocked(): + cv.notifyAll() + + finally: + cv.release() + + + + diff --git a/src/engine/SCons/JobTests.py b/src/engine/SCons/JobTests.py new file mode 100644 index 0000000..ec4c46d --- /dev/null +++ b/src/engine/SCons/JobTests.py @@ -0,0 +1,228 @@ +__revision__ = "JobTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import unittest +import random +import math +import SCons.Job +import sys + +# a large number +num_sines = 10000 + +# how many parallel jobs to perform for the test +num_jobs = 11 + +# how many tasks to perform for the test +num_tasks = num_jobs*5 + +class DummyLock: + "fake lock class to use if threads are not supported" + def acquire(self): + pass + + def release(self): + pass + +class NoThreadsException: + "raised by the ParallelTestCase if threads are not supported" + + def __str__(self): + return "the interpreter doesn't support threads" + +class Task: + """A dummy task class for testing purposes.""" + + def __init__(self, i, taskmaster): + self.i = i + self.taskmaster = taskmaster + self.was_executed = 0 + + def execute(self): + self.taskmaster.guard.acquire() + self.taskmaster.begin_list.append(self.i) + self.taskmaster.guard.release() + + # do something that will take some random amount of time: + for i in range(random.randrange(0, num_sines, 1)): + x = math.sin(i) + + self.was_executed = 1 + + self.taskmaster.guard.acquire() + self.taskmaster.end_list.append(self.i) + self.taskmaster.guard.release() + +class ExceptionTask: + """A dummy task class for testing purposes.""" + + def __init__(self, i, taskmaster): + pass + + def execute(self): + raise "exception" + +class Taskmaster: + """A dummy taskmaster class for testing the job classes.""" + + def __init__(self, n, test_case, Task): + """n is the number of dummy tasks to perform.""" + + self.test_case = test_case + self.num_tasks = n + self.num_iterated = 0 + self.num_executed = 0 + self.num_failed = 0 + self.Task = Task + # 'guard' guards 'task_begin_list' and 'task_end_list' + try: + import threading + self.guard = threading.Lock() + except: + self.guard = DummyLock() + + # keep track of the order tasks are begun in + self.begin_list = [] + + # keep track of the order tasks are completed in + self.end_list = [] + + + def next_task(self): + if self.all_tasks_are_iterated(): + return None + else: + self.num_iterated = self.num_iterated + 1 + return self.Task(self.num_iterated, self) + + def all_tasks_are_executed(self): + return self.num_executed == self.num_tasks + + def all_tasks_are_iterated(self): + return self.num_iterated == self.num_tasks + + def executed(self, task): + self.num_executed = self.num_executed + 1 + + self.test_case.failUnless(task.was_executed, + "the task wasn't really executed") + self.test_case.failUnless(task.__class__ is Task, + "the task wasn't really a Task instance") + + def failed(self, task): + self.num_failed = self.num_failed + 1 + + def is_blocked(self): + # simulate blocking tasks + return self.num_iterated - self.num_executed >= max(num_jobs/2, 2) + + def tasks_were_serial(self): + "analyze the task order to see if they were serial" + serial = 1 # assume the tasks were serial + for i in range(num_tasks): + serial = serial and (self.begin_list[i] + == self.end_list[i] + == (i + 1)) + return serial + +class ParallelTestCase(unittest.TestCase): + def runTest(self): + "test parallel jobs" + + try: + import threading + except: + raise NoThreadsException() + + taskmaster = Taskmaster(num_tasks, self, Task) + jobs = SCons.Job.Jobs(num_jobs, taskmaster) + jobs.start() + jobs.wait() + + self.failUnless(not taskmaster.tasks_were_serial(), + "the tasks were not executed in parallel") + self.failUnless(taskmaster.all_tasks_are_executed(), + "all the tests were not executed") + self.failUnless(taskmaster.all_tasks_are_iterated(), + "all the tests were not iterated over") + self.failIf(taskmaster.num_failed, + "some task(s) failed to execute") + +class SerialTestCase(unittest.TestCase): + def runTest(self): + "test a serial job" + + taskmaster = Taskmaster(num_tasks, self, Task) + jobs = SCons.Job.Jobs(1, taskmaster) + jobs.start() + jobs.wait() + + self.failUnless(taskmaster.tasks_were_serial(), + "the tasks were not executed in series") + self.failUnless(taskmaster.all_tasks_are_executed(), + "all the tests were not executed") + self.failUnless(taskmaster.all_tasks_are_iterated(), + "all the tests were not iterated over") + self.failIf(taskmaster.num_failed, + "some task(s) failed to execute") + +class SerialExceptionTestCase(unittest.TestCase): + def runTest(self): + "test a serial job with tasks that raise exceptions" + + taskmaster = Taskmaster(num_tasks, self, ExceptionTask) + jobs = SCons.Job.Jobs(1, taskmaster) + jobs.start() + jobs.wait() + + self.failIf(taskmaster.num_executed, + "a task was executed") + self.failUnless(taskmaster.num_iterated == 1, + "exactly one task should have been iterated") + self.failUnless(taskmaster.num_failed == 1, + "exactly one task should have failed") + +class ParallelExceptionTestCase(unittest.TestCase): + def runTest(self): + "test parallel jobs with tasks that raise exceptions" + + taskmaster = Taskmaster(num_tasks, self, ExceptionTask) + jobs = SCons.Job.Jobs(num_jobs, taskmaster) + jobs.start() + jobs.wait() + + self.failIf(taskmaster.num_executed, + "a task was executed") + self.failUnless(taskmaster.num_iterated >= 1, + "one or more task should have been iterated") + self.failUnless(taskmaster.num_failed >= 1, + "one or more tasks should have failed") + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(ParallelTestCase()) + suite.addTest(SerialTestCase()) + suite.addTest(SerialExceptionTestCase()) + suite.addTest(ParallelExceptionTestCase()) + return suite + +if __name__ == "__main__": + runner = unittest.TextTestRunner() + result = runner.run(suite()) + if (len(result.failures) == 0 + and len(result.errors) == 1 + and type(result.errors[0][0]) == SerialTestCase + and type(result.errors[0][1][0]) == NoThreadsException): + sys.exit(2) + elif not result.wasSuccessful(): + sys.exit(1) + + + + + + + + + + diff --git a/src/engine/SCons/Node/.aeignore b/src/engine/SCons/Node/.aeignore new file mode 100644 index 0000000..43fe851 --- /dev/null +++ b/src/engine/SCons/Node/.aeignore @@ -0,0 +1,4 @@ +*,D +*.pyc +.*.swp +.consign diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py new file mode 100644 index 0000000..e7956c2 --- /dev/null +++ b/src/engine/SCons/Node/FS.py @@ -0,0 +1,139 @@ +"""SCons.Node.FS + +File system nodes. + +""" + +__revision__ = "Node/FS.py __REVISION__ __DATE__ __DEVELOPER__" + + + +import os +import os.path +import SCons.Node + + + +Top = None +Root = {} + + + +def init(path = None): + """Initialize the Node.FS subsystem. + + The supplied path is the top of the source tree, where we + expect to find the top-level build file. If no path is + supplied, the current directory is the default. + """ + global Top + if path == None: + path = os.getcwd() + Top = lookup(Dir, path, directory = None) + Top.path = '.' + +def lookup(fsclass, name, directory = Top): + """Look up a file system node for a path name. If the path + name is relative, it will be looked up relative to the + specified directory node, or to the top-level directory + if no node was specified. An initial '#' specifies that + the name will be looked up relative to the top-level directory, + regardless of the specified directory argument. Returns the + existing or newly-created node for the specified path name. + The node returned will be of the specified fsclass (Dir or + File). + """ + global Top + head, tail = os.path.split(name) + if not tail: + drive, path = os.path.splitdrive(head) + if not Root.has_key(drive): + Root[drive] = Dir(head, None) + Root[drive].abspath = head + Root[drive].path = head + return Root[drive] + if tail[0] == '#': + directory = Top + tail = tail[1:] + elif directory is None: + directory = Top + if head: + directory = lookup(Dir, head, directory) + try: + self = directory.entries[tail] + except AttributeError: + # There was no "entries" attribute on the directory, + # which essentially implies that it was a file. + # Return it as a more descriptive exception. + raise TypeError, directory + except KeyError: + # There was to entry for "tail," so create the new + # node and link it in to the existing structure. + self = fsclass(tail, directory) + self.name = tail + if self.path[0:2] == "./": + self.path = self.path[2:] + directory.entries[tail] = self + except: + raise + if self.__class__.__name__ != fsclass.__name__: + # Here, we found an existing node for this path, + # but it was the wrong type (a File when we were + # looking for a Dir, or vice versa). + raise TypeError, self + return self + + + +# XXX TODO? +# Annotate with the creator +# is_under +# rel_path +# srcpath / srcdir +# link / is_linked +# linked_targets +# is_accessible + +class Dir(SCons.Node.Node): + """A class for directories in a file system. + """ + + def __init__(self, name, directory): + self.entries = {} + self.entries['.'] = self + self.entries['..'] = directory + if not directory is None: + self.abspath = os.path.join(directory.abspath, name, '') + self.path = os.path.join(directory.path, name, '') + + def up(self): + return self.entries['..'] + + +# XXX TODO? +# rfile +# precious +# no_rfile +# rpath +# rsrcpath +# source_exists +# derived_exists +# is_on_rpath +# local +# base_suf +# suffix +# addsuffix +# accessible +# ignore +# build +# bind +# is_under +# relpath + +class File(SCons.Node.Node): + """A class for files in a file system. + """ + + def __init__(self, name, directory): + self.abspath = os.path.join(directory.abspath, name) + self.path = os.path.join(directory.path, name) diff --git a/src/engine/SCons/Node/FS/.aeignore b/src/engine/SCons/Node/FS/.aeignore new file mode 100644 index 0000000..43fe851 --- /dev/null +++ b/src/engine/SCons/Node/FS/.aeignore @@ -0,0 +1,4 @@ +*,D +*.pyc +.*.swp +.consign diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py new file mode 100644 index 0000000..aa8cf2a --- /dev/null +++ b/src/engine/SCons/Node/FSTests.py @@ -0,0 +1,109 @@ +__revision__ = "Node/FSTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import os +import sys +import unittest + +import SCons.Node.FS + + + +built_it = None + +class Builder: + def execute(self, target = None, source = None): + global built_it + built_it = 1 + + + +class FSTestCase(unittest.TestCase): + def runTest(self): + """Test FS (file system) Node operations + + This test case handles all of the file system node + tests in one environment, so we don't have to set up a + complicated directory structure for each test individually. + """ + from TestCmd import TestCmd + + test = TestCmd(workdir = '') + test.subdir('sub', ['sub', 'dir']) + + wp = test.workpath('') + sub = test.workpath('sub', '') + sub_dir = test.workpath('sub', 'dir', '') + sub_dir_foo = test.workpath('sub', 'dir', 'foo', '') + sub_dir_foo_bar = test.workpath('sub', 'dir', 'foo', 'bar', '') + sub_foo = test.workpath('sub', 'foo', '') + + os.chdir(sub_dir) + + SCons.Node.FS.init() + + def Dir_test(lpath, path, abspath, up_path): + dir = SCons.Node.FS.lookup(SCons.Node.FS.Dir, lpath) + assert(dir.path == path) + assert(dir.abspath == abspath) + assert(dir.up().path == up_path) + + Dir_test('foo', 'foo/', sub_dir_foo, '.') + Dir_test('foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') + Dir_test('/foo', '/foo/', '/foo/', '/') + Dir_test('/foo/bar', '/foo/bar/', '/foo/bar/', '/foo/') + Dir_test('..', sub, sub, wp) + Dir_test('foo/..', '.', sub_dir, sub) + Dir_test('../foo', sub_foo, sub_foo, sub) + Dir_test('.', '.', sub_dir, sub) + Dir_test('./.', '.', sub_dir, sub) + Dir_test('foo/./bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') + + d1 = SCons.Node.FS.lookup(SCons.Node.FS.Dir, 'd1') + + f1 = SCons.Node.FS.lookup(SCons.Node.FS.File, 'f1', directory = d1) + + assert(f1.path == 'd1/f1') + + try: + f2 = SCons.Node.FS.lookup(SCons.Node.FS.File, 'f1/f2', directory = d1) + except TypeError, x: + node = x.args[0] + assert(node.path == 'd1/f1') + assert(node.__class__.__name__ == 'File') + except: + raise + + try: + dir = SCons.Node.FS.lookup(SCons.Node.FS.Dir, 'd1/f1') + except TypeError, x: + node = x.args[0] + assert(node.path == 'd1/f1') + assert(node.__class__.__name__ == 'File') + except: + raise + + # Test for sub-classing of node building. + global built_it + + built_it = None + assert not built_it + d1.path = "d" # XXX FAKE SUBCLASS ATTRIBUTE + d1.sources = "d" # XXX FAKE SUBCLASS ATTRIBUTE + d1.builder_set(Builder()) + d1.build() + assert built_it + + built_it = None + assert not built_it + f1.path = "f" # XXX FAKE SUBCLASS ATTRIBUTE + f1.sources = "f" # XXX FAKE SUBCLASS ATTRIBUTE + f1.builder_set(Builder()) + f1.build() + assert built_it + + +if __name__ == "__main__": + suite = unittest.TestSuite() + suite.addTest(FSTestCase()) + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py new file mode 100644 index 0000000..2249a96 --- /dev/null +++ b/src/engine/SCons/Node/NodeTests.py @@ -0,0 +1,45 @@ +__revision__ = "Node/NodeTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import os +import sys +import unittest + +import SCons.Node + + + +built_it = None + +class Builder: + def execute(self, target = None, source = None): + global built_it + built_it = 1 + + + +class NodeTestCase(unittest.TestCase): + + def test_build(self): + """Test building a node + """ + node = SCons.Node.Node() + node.builder_set(Builder()) + node.path = "xxx" # XXX FAKE SUBCLASS ATTRIBUTE + node.sources = "yyy" # XXX FAKE SUBCLASS ATTRIBUTE + node.build() + assert built_it + + def test_builder_set(self): + """Test setting a Node's Builder + """ + node = SCons.Node.Node() + b = Builder() + node.builder_set(b) + assert node.builder == b + + + +if __name__ == "__main__": + suite = unittest.makeSuite(NodeTestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py new file mode 100644 index 0000000..fb77c65 --- /dev/null +++ b/src/engine/SCons/Node/__init__.py @@ -0,0 +1,22 @@ +"""SCons.Node + +The Node package for the SCons software construction utility. + +""" + +__revision__ = "Node/__init__.py __REVISION__ __DATE__ __DEVELOPER__" + + + +class Node: + """The base Node class, for entities that we know how to + build, or use to build other Nodes. + """ + def build(self): + self.builder.execute(target = self.path, source = self.sources) + + def builder_set(self, builder): + self.builder = builder + + def env_set(self, env): + self.env = env diff --git a/src/engine/SCons/Scanner/.aeignore b/src/engine/SCons/Scanner/.aeignore new file mode 100644 index 0000000..43fe851 --- /dev/null +++ b/src/engine/SCons/Scanner/.aeignore @@ -0,0 +1,4 @@ +*,D +*.pyc +.*.swp +.consign diff --git a/src/engine/SCons/Scanner/C.py b/src/engine/SCons/Scanner/C.py new file mode 100644 index 0000000..ce6f9c6 --- /dev/null +++ b/src/engine/SCons/Scanner/C.py @@ -0,0 +1,87 @@ +"""SCons.Scanner.C + +This module implements the depenency scanner for C/C++ code. + +""" + +__revision__ = "Scanner/C.py __REVISION__ __DATE__ __DEVELOPER__" + + +import SCons.Scanner +import re +import os.path + +angle_re = re.compile('^[ \t]*#[ \t]*include[ \t]+<([\\w./\\\\]+)>', re.M) +quote_re = re.compile('^[ \t]*#[ \t]*include[ \t]+"([\\w./\\\\]+)"', re.M) + +def CScan(): + "Return a Scanner instance for scanning C/C++ source files" + return SCons.Scanner.Scanner(scan) + +def find_files(filenames, paths): + """ + find_files([str], [str]) -> [str] + + filenames - a list of filenames to find + paths - a list of paths to search in + + returns - the fullnames of the files + + Only the first fullname found is returned for each filename, and any + file that aren't found are ignored. + """ + fullnames = [] + for filename in filenames: + for path in paths: + fullname = os.path.join(path, filename) + if os.path.exists(fullname): + fullnames.append(fullname) + break + + return fullnames + +def scan(filename, env): + """ + scan(str, Environment) -> [str] + + the C/C++ dependency scanner function + + This function is intentionally simple. There are two rules it + follows: + + 1) #include <foo.h> - search for foo.h in CPPPATH followed by the + directory 'filename' is in + 2) #include \"foo.h\" - search for foo.h in the directory 'filename' is + in followed by CPPPATH + + These rules approximate the behaviour of most C/C++ compilers. + + This scanner also ignores #ifdef and other preprocessor conditionals, so + it may find more depencies than there really are, but it never misses + dependencies. + """ + + if hasattr(env, "CPPPATH"): + paths = env.CPPPATH + else: + paths = [] + + file = open(filename) + contents = file.read() + file.close() + + angle_includes = angle_re.findall(contents) + quote_includes = quote_re.findall(contents) + + source_dir = os.path.dirname(filename) + + deps = (find_files(angle_includes, paths + [source_dir]) + + find_files(quote_includes, [source_dir] + paths)) + + return deps + + + + + + diff --git a/src/engine/SCons/Scanner/CTests.py b/src/engine/SCons/Scanner/CTests.py new file mode 100644 index 0000000..6a85120 --- /dev/null +++ b/src/engine/SCons/Scanner/CTests.py @@ -0,0 +1,131 @@ +__revision__ = "Scanner/CTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import TestCmd +import SCons.Scanner.C +import unittest +import sys + +test = TestCmd.TestCmd(workdir = '') + +# create some source files and headers: + +test.write('f1.cpp',""" +#include \"f1.h\" +#include <f2.h> + +int main() +{ + return 0; +} +""") + +test.write('f2.cpp',""" +#include \"d1/f1.h\" +#include <d2/f1.h> +#include \"f1.h\" +#include <f4.h> + +int main() +{ + return 0; +} +""") + +test.write('f3.cpp',""" +#include \t "f1.h" + \t #include "f2.h" +# \t include "f3.h" + +#include \t <d1/f1.h> + \t #include <d1/f2.h> +# \t include <d1/f3.h> + +// #include "never.h" + +const char* x = "#include <never.h>" + +int main() +{ + return 0; +} +""") + + +# for Emacs -> " + +test.subdir('d1', ['d1', 'd2']) + +headers = ['f1.h','f2.h', 'f3.h', 'never.h', + 'd1/f1.h', 'd1/f2.h', 'd1/f3.h', + 'd1/d2/f1.h', 'd1/d2/f2.h', 'd1/d2/f3.h', 'd1/d2/f4.h'] + +for h in headers: + test.write(h, " ") + +# define some helpers: + +class DummyEnvironment: + pass + +def deps_match(deps, headers): + return deps.sort() == map(test.workpath, headers).sort() + +# define some tests: + +class CScannerTestCase1(unittest.TestCase): + def runTest(self): + env = DummyEnvironment + s = SCons.Scanner.C.CScan() + deps = s.scan(test.workpath('f1.cpp'), env) + self.failUnless(deps_match(deps, ['f1.h', 'f2.h'])) + +class CScannerTestCase2(unittest.TestCase): + def runTest(self): + env = DummyEnvironment + env.CPPPATH = [test.workpath("d1")] + s = SCons.Scanner.C.CScan() + deps = s.scan(test.workpath('f1.cpp'), env) + headers = ['f1.h', 'd1/f2.h'] + self.failUnless(deps_match(deps, headers)) + +class CScannerTestCase3(unittest.TestCase): + def runTest(self): + env = DummyEnvironment + env.CPPPATH = [test.workpath("d1")] + s = SCons.Scanner.C.CScan() + deps = s.scan(test.workpath('f2.cpp'), env) + headers = ['f1.h', 'd1/f2.h', 'd1/d2/f1.h'] + self.failUnless(deps_match(deps, headers)) + + +class CScannerTestCase4(unittest.TestCase): + def runTest(self): + env = DummyEnvironment + env.CPPPATH = [test.workpath("d1"), test.workpath("d1/d2")] + s = SCons.Scanner.C.CScan() + deps = s.scan(test.workpath('f2.cpp'), env) + headers = ['f1.h', 'd1/f2.h', 'd1/d2/f1.h', 'd1/d2/f4.h'] + self.failUnless(deps_match(deps, headers)) + +class CScannerTestCase5(unittest.TestCase): + def runTest(self): + env = DummyEnvironment + s = SCons.Scanner.C.CScan() + deps = s.scan(test.workpath('f3.cpp'), env) + headers = ['f1.h', 'f2.h', 'f3.h', 'd1/f1.h', 'd1/f2.h', 'd1/f3.h'] + self.failUnless(deps_match(deps, headers)) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(CScannerTestCase1()) + suite.addTest(CScannerTestCase2()) + suite.addTest(CScannerTestCase3()) + suite.addTest(CScannerTestCase4()) + suite.addTest(CScannerTestCase5()) + return suite + +if __name__ == "__main__": + runner = unittest.TextTestRunner() + result = runner.run(suite()) + if not result.wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Scanner/ScannerTests.py b/src/engine/SCons/Scanner/ScannerTests.py new file mode 100644 index 0000000..9c7163d --- /dev/null +++ b/src/engine/SCons/Scanner/ScannerTests.py @@ -0,0 +1,84 @@ +__revision__ = "Scanner/ScannerTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import unittest +import SCons.Scanner +import sys + +class ScannerTestBase: + + def func(self, filename, env, *args): + self.filename = filename + self.env = env + + if len(args) > 0: + self.arg = args[0] + + return self.deps + + + def test(self, scanner, env, filename, deps, *args): + self.deps = deps + deps = scanner.scan(filename, env) + + self.failUnless(self.filename == filename, "the filename was passed incorrectly") + self.failUnless(self.env == env, "the environment was passed incorrectly") + self.failUnless(self.deps == deps, "the dependencies were returned incorrectly") + + if len(args) > 0: + self.failUnless(self.arg == args[0], "the argument was passed incorrectly") + else: + self.failIf(hasattr(self, "arg"), "an argument was given when it shouldn't have been") + +class DummyEnvironment: + pass + + +class ScannerPositionalTestCase(ScannerTestBase, unittest.TestCase): + "Test the Scanner class using the position argument" + def runTest(self): + s = SCons.Scanner.Scanner(self.func) + env = DummyEnvironment() + env.VARIABLE = "var1" + self.test(s, env, 'f1.cpp', ['f1.h', 'f1.hpp']) + +class ScannerKeywordTestCase(ScannerTestBase, unittest.TestCase): + "Test the Scanner class using the keyword argument" + def runTest(self): + s = SCons.Scanner.Scanner(function = self.func) + env = DummyEnvironment() + env.VARIABLE = "var2" + self.test(s, env, 'f2.cpp', ['f2.h', 'f2.hpp']) + +class ScannerPositionalArgumentTestCase(ScannerTestBase, unittest.TestCase): + "Test the Scanner class using the position argument and optional argument" + def runTest(self): + arg = "this is the argument" + s = SCons.Scanner.Scanner(self.func, arg) + env = DummyEnvironment() + env.VARIABLE = "var3" + self.test(s, env, 'f3.cpp', ['f3.h', 'f3.hpp'], arg) + +class ScannerKeywordArgumentTestCase(ScannerTestBase, unittest.TestCase): + "Test the Scanner class using the keyword argument and optional argument" + def runTest(self): + arg = "this is another argument" + s = SCons.Scanner.Scanner(function = self.func, argument = arg) + env = DummyEnvironment() + env.VARIABLE = "var4" + self.test(s, env, 'f4.cpp', ['f4.h', 'f4.hpp'], arg) + +def suite(): + suite = unittest.TestSuite() + suite.addTest(ScannerPositionalTestCase()) + suite.addTest(ScannerKeywordTestCase()) + suite.addTest(ScannerPositionalArgumentTestCase()) + suite.addTest(ScannerKeywordArgumentTestCase()) + return suite + +if __name__ == "__main__": + runner = unittest.TextTestRunner() + result = runner.run(suite()) + if not result.wasSuccessful(): + sys.exit(1) + + diff --git a/src/engine/SCons/Scanner/__init__.py b/src/engine/SCons/Scanner/__init__.py new file mode 100644 index 0000000..225c7f3 --- /dev/null +++ b/src/engine/SCons/Scanner/__init__.py @@ -0,0 +1,69 @@ +"""SCons.Scanner + +The Scanner package for the SCons software construction utility. + +""" + +__revision__ = "Scanner/__init__.py __REVISION__ __DATE__ __DEVELOPER__" + +__version__ = "__VERSION__" + +class _Null: + pass + +# This is used instead of None as a default argument value so None can be +# used as an actual argument value. +_null = _Null + +class Scanner: + + def __init__(self, function, argument=_null): + """ + Construct a new scanner object given a scanner function. + + 'function' - a scanner function taking two or three arguments and + returning a list of strings. + + 'argument' - an optional argument that will be passed to the + scanner function if it is given. + + The scanner function's first argument will be the name of a file + that should be scanned for dependencies, the second argument will + be an Environment object, the third argument will be the value + passed into 'argument', and the returned list should contain the + file names of all the direct dependencies of the file. + + Examples: + + s = Scanner(my_scanner_function) + + s = Scanner(function = my_scanner_function) + + s = Scanner(function = my_scanner_function, argument = 'foo') + + """ + + # Note: this class could easily work with scanner functions that take + # something other than a filename as an argument (e.g. a database + # node) and a dependencies list that aren't file names. All that + # would need to be changed is the documentation. + + self.function = function + self.argument = argument + + def scan(self, filename, env): + """ + This method does the actually scanning. 'filename' is the filename + that will be passed to the scanner function, and 'env' is the + environment that will be passed to the scanner function. A list of + dependencies will be returned. + """ + + if not self.argument is _null: + return self.function(filename, env, self.argument) + else: + return self.function(filename, env) + + + + diff --git a/src/engine/SCons/Sig/.aeignore b/src/engine/SCons/Sig/.aeignore new file mode 100644 index 0000000..43fe851 --- /dev/null +++ b/src/engine/SCons/Sig/.aeignore @@ -0,0 +1,4 @@ +*,D +*.pyc +.*.swp +.consign diff --git a/src/engine/SCons/Sig/MD5.py b/src/engine/SCons/Sig/MD5.py new file mode 100644 index 0000000..9d461d9 --- /dev/null +++ b/src/engine/SCons/Sig/MD5.py @@ -0,0 +1,70 @@ +"""SCons.Sig.MD5 + +The MD5 signature package for the SCons software construction +utility. + +""" + +__revision__ = "Sig/MD5.py __REVISION__ __DATE__ __DEVELOPER__" + +import md5 +import string + + + +def hexdigest(s): + """Return a signature as a string of hex characters. + """ + # NOTE: This routine is a method in the Python 2.0 interface + # of the native md5 module, but we want SCons to operate all + # the way back to at least Python 1.5.2, which doesn't have it. + h = string.hexdigits + r = '' + for c in s: + i = ord(c) + r = r + h[(i >> 4) & 0xF] + h[i & 0xF] + return r + + + +def _init(): + pass # XXX + +def _end(): + pass # XXX + +def current(obj, sig): + """Return whether a given object is up-to-date with the + specified signature. + """ + return obj.signature() == sig + +def set(): + pass # XXX + +def invalidate(): + pass # XXX + +def collect(*objects): + """Collect signatures from a list of objects, returning the + aggregate signature of the list. + """ + if len(objects) == 1: + sig = objects[0].signature() + else: + contents = string.join(map(lambda o: o.signature(), objects), ', ') + sig = signature(contents) +# if debug: +# pass + return sig + +def signature(contents): + """Generate a signature for a byte string. + """ + return hexdigest(md5.new(contents).digest()) + +def cmdsig(): + pass # XXX + +def srcsig(): + pass # XXX diff --git a/src/engine/SCons/Sig/MD5Tests.py b/src/engine/SCons/Sig/MD5Tests.py new file mode 100644 index 0000000..ac67f1d --- /dev/null +++ b/src/engine/SCons/Sig/MD5Tests.py @@ -0,0 +1,76 @@ +__revision__ = "Sig/MD5Tests.py __REVISION__ __DATE__ __DEVELOPER__" + +import sys +import unittest + +import SCons.Sig.MD5 + + + +class my_obj: + """A dummy object class that satisfies the interface + requirements of the MD5 class. + """ + + def __init__(self, value = ""): + self.value = value + self.sig = None + + def signature(self): + if not self.sig: + self.sig = SCons.Sig.MD5.signature(self.value) + return self.sig + + def current(self, sig): + return SCons.Sig.MD5.current(self, sig) + + + +class MD5TestCase(unittest.TestCase): + + def test__init(self): + pass # XXX + + def test__end(self): + pass # XXX + + def test_current(self): + """Test deciding if an object is up-to-date + + Simple comparison of different "signature" values. + """ + o111 = my_obj(value = '111') + assert not o111.current(SCons.Sig.MD5.signature('110')) + assert o111.current(SCons.Sig.MD5.signature('111')) + assert not o111.current(SCons.Sig.MD5.signature('112')) + + def test_set(self): + pass # XXX + + def test_invalidate(self): + pass # XXX + + def test_collect(self): + """Test collecting a list of signatures into a new signature value + """ + o1 = my_obj(value = '111') + o2 = my_obj(value = '222') + o3 = my_obj(value = '333') + assert '698d51a19d8a121ce581499d7b701668' == SCons.Sig.MD5.collect(o1) + assert '8980c988edc2c78cc43ccb718c06efd5' == SCons.Sig.MD5.collect(o1, o2) + assert '53fd88c84ff8a285eb6e0a687e55b8c7' == SCons.Sig.MD5.collect(o1, o2, o3) + + def test_signature(self): + pass # XXX + + def test_cmdsig(self): + pass # XXX + + def test_srcsig(self): + pass # XXX + + +if __name__ == "__main__": + suite = unittest.makeSuite(MD5TestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Sig/TimeStamp.py b/src/engine/SCons/Sig/TimeStamp.py new file mode 100644 index 0000000..b821742 --- /dev/null +++ b/src/engine/SCons/Sig/TimeStamp.py @@ -0,0 +1,49 @@ +"""SCons.Sig.TimeStamp + +The TimeStamp signature package for the SCons software construction +utility. + +""" + +__revision__ = "Sig/TimeStamp.py __REVISION__ __DATE__ __DEVELOPER__" + +def _init(): + pass # XXX + +def _end(): + pass # XXX + +def current(obj, sig): + """Return whether the object's timestamp is up-to-date. + """ + return obj.signature() >= sig + +def set(): + pass # XXX + +def invalidate(): + pass # XXX + +def collect(*objects): + """Collect timestamps from a list of objects, returning + the most-recent timestamp from the list. + """ + r = 0 + for obj in objects: + s = obj.signature() + if s > r: + r = s + return r + +def signature(contents): + """Generate a timestamp. + """ + pass # XXX +# return md5.new(contents).hexdigest() # 2.0 + return hexdigest(md5.new(contents).digest()) + +def cmdsig(): + pass # XXX + +def srcsig(): + pass # XXX diff --git a/src/engine/SCons/Sig/TimeStampTests.py b/src/engine/SCons/Sig/TimeStampTests.py new file mode 100644 index 0000000..4cc72e4 --- /dev/null +++ b/src/engine/SCons/Sig/TimeStampTests.py @@ -0,0 +1,74 @@ +__revision__ = "Sig/TimeStampTests.py __REVISION__ __DATE__ __DEVELOPER__" + +import sys +import unittest + +import SCons.Sig.TimeStamp + + + +class my_obj: + """A dummy object class that satisfies the interface + requirements of the TimeStamp class. + """ + + def __init__(self, value = ""): + self.value = value + + def signature(self): + return self.value + + + +class TimeStampTestCase(unittest.TestCase): + + def test__init(self): + pass # XXX + + def test__init(self): + pass # XXX + + def test__end(self): + pass # XXX + + def test_current(self): + """Test deciding if an object is up-to-date + + Simple comparison of different timestamp values. + """ + o1 = my_obj(value = 111) + assert SCons.Sig.TimeStamp.current(o1, 110) + assert SCons.Sig.TimeStamp.current(o1, 111) + assert not SCons.Sig.TimeStamp.current(o1, 112) + + def test_set(self): + pass # XXX + + def test_invalidate(self): + pass # XXX + + def test_collect(self): + """Test collecting a list of signatures into a new signature value + into a new timestamp value. + """ + o1 = my_obj(value = 111) + o2 = my_obj(value = 222) + o3 = my_obj(value = 333) + assert 111 == SCons.Sig.TimeStamp.collect(o1) + assert 222 == SCons.Sig.TimeStamp.collect(o1, o2) + assert 333 == SCons.Sig.TimeStamp.collect(o1, o2, o3) + + def test_signature(self): + pass # XXX + + def test_cmdsig(self): + pass # XXX + + def test_srcsig(self): + pass # XXX + + +if __name__ == "__main__": + suite = unittest.makeSuite(TimeStampTestCase, 'test_') + if not unittest.TextTestRunner().run(suite).wasSuccessful(): + sys.exit(1) diff --git a/src/engine/SCons/Sig/__init__.py b/src/engine/SCons/Sig/__init__.py new file mode 100644 index 0000000..c3b06ed --- /dev/null +++ b/src/engine/SCons/Sig/__init__.py @@ -0,0 +1,7 @@ +"""SCons.Sig + +The Signature package for the SCons software construction utility. + +""" + +__revision__ = "Sig/__init__.py __REVISION__ __DATE__ __DEVELOPER__" diff --git a/src/engine/SCons/__init__.py b/src/engine/SCons/__init__.py new file mode 100644 index 0000000..56c5fad --- /dev/null +++ b/src/engine/SCons/__init__.py @@ -0,0 +1,9 @@ +"""SCons + +The main package for the SCons software construction utility. + +""" + +__revision__ = "__init__.py __REVISION__ __DATE__ __DEVELOPER__" + +__version__ = "__VERSION__" diff --git a/src/engine/SCons/exitfuncs.py b/src/engine/SCons/exitfuncs.py new file mode 100644 index 0000000..86d1990 --- /dev/null +++ b/src/engine/SCons/exitfuncs.py @@ -0,0 +1,48 @@ +"""SCons.exitfuncs + +Register functions which are executed when SCons exits for any reason. + +""" + +__revision__ = "exitfuncs.py __REVISION__ __DATE__ __DEVELOPER__" + + + +_exithandlers = [] +def _run_exitfuncs(): + """run any registered exit functions + + _exithandlers is traversed in reverse order so functions are executed + last in, first out. + """ + + while _exithandlers: + func, targs, kargs = _exithandlers.pop() + apply(func, targs, kargs) + +def register(func, *targs, **kargs): + """register a function to be executed upon normal program termination + + func - function to be called at exit + targs - optional arguments to pass to func + kargs - optional keyword arguments to pass to func + """ + _exithandlers.append((func, targs, kargs)) + +import sys + +try: + x = sys.exitfunc + + # if x isn't our own exit func executive, assume it's another + # registered exit function - append it to our list... + if x != _run_exitfuncs: + register(x) + +except AttributeError: + pass + +# make our exit function get run by python when it exits: +sys.exitfunc = _run_exitfuncs + +del sys diff --git a/src/engine/setup.py b/src/engine/setup.py new file mode 100644 index 0000000..10fbef5 --- /dev/null +++ b/src/engine/setup.py @@ -0,0 +1,16 @@ +__revision__ = "setup.py __REVISION__ __DATE__ __DEVELOPER__" + +from string import join, split + +from distutils.core import setup + +setup(name = "scons-pylib", + version = "__VERSION__", + description = "scons", + author = "Steven Knight", + author_email = "knight@baldmt.com", + url = "http://www.scons.org/", + packages = ["SCons", + "SCons.Node", + "SCons.Scanner", + "SCons.Sig"]) |
