Anonymous avatar Anonymous committed 775e51e

Initial revision

Comments (0)

Files changed (34)

+#
+# Construct file to build scons during development.
+# (Kind of ironic that we're using the classic Perl Cons
+# to build its Python child...)
+#
+$project = 'scons';
+
+$env = new cons( ENV => {
+			  AEGIS_PROJECT => $ENV{AEGIS_PROJECT},
+			  PATH => $ENV{PATH},
+			} );
+
+Default qw( . );
+
+#
+# Grab the information that we "build" into the files (using sed).
+#
+chomp($date = $ARG{date} || `date '+%Y/%m/%d %H:%M:%S'`);
+
+$developer = $ARG{developer} || '???';
+
+chomp($revision = $ARG{version} || `aesub '\$version' 2>/dev/null` || '0.01');
+
+@arr = split(/\./, $revision);
+@arr = ($arr[0], map {length($_) == 1 ? "0$_" : $_} @arr[1 .. $#arr]);
+$revision = join('.', @arr);
+pop @arr if $#arr >= 2;
+map {s/^[CD]//, s/^0*(\d\d)$/$1/} @arr;
+$version = join('.', @arr);
+
+#
+# We use %(-%) around the date so date changes don't cause rebuilds.
+#
+$sed_cmd = "sed" .
+       " %( -e 's+__DATE__+$date+' %)" .
+       " -e 's+__DEVELOPER__+$developer+'" .
+       " -e 's+__REVISION__+$revision+'" .
+       " -e 's+__VERSION__+$version+'" .
+       " %< > %>";
+
+#
+# Run everything in the MANIFEST through the sed command we concocted.
+#
+chomp(@files = `cat src/MANIFEST`);
+
+foreach $file (@files) {
+    Command $env "build/$file", "src/$file", $sed_cmd;
+}
+
+#
+# Use the Python distutils to generate the packages.
+#
+$tar_gz = "build/dist/$project-$version.tar.gz";
+
+@targets = (
+    "build/build/bdist.linux-i686/rpm/SOURCES/$project-$version.tar.gz",
+    "build/build/bdist.linux-i686/rpm/SPECS/$project.spec",
+    $tar_gz,
+    "build/dist/$project-$version-1.src.rpm",
+    "build/dist/$project-$version.linux-i686.tar.gz",
+    "build/dist/$project-$version-1.noarch.rpm",
+);
+
+@build_files = map("build/$_", @files);
+
+Command $env [@targets], @build_files, qq(
+    rm -rf build/build build/dist/*
+    cd build && python setup.py bdist bdist_rpm
+);
+
+Depends $env [@targets], 'build/MANIFEST';
+
+#
+# Unpack the .tar.gz created by the distutils into build/test, and
+# add the TestCmd.py module.  The runtest.py script will set PYTHONPATH
+# so that the tests only look under build/test.  This makes sure that
+# our tests pass with what we really packaged, not because of something
+# hanging around in the development directory.
+#
+$test_dir = "build/test";
+
+Command $env "$test_dir/$project-$version/$project/__init__.py", $tar_gz, qq(
+    rm -rf $test_dir/$project-$version
+    tar zxf %< -C $test_dir
+);
+
+Install $env $test_dir, "TestCmd.py";
+
+#
+# If we're running in the actual Aegis project, pack up a complete
+# source .tar.gz from the project files and files in the change,
+# so we can share it with helpful developers who don't use Aegis.
+#
+eval '@src_files = grep($_ !~ /\.(aeignore|consign)$/ && ! $seen{$_}++,
+		    `aegis -list -terse pf 2>/dev/null`,
+		    `aegis -list -terse cf 2>/dev/null`)';
+if (@src_files) {
+    chomp(@src_files);
+
+    foreach $file (@src_files) {
+	Command $env "build/$project-src/$file", $file, $sed_cmd;
+    }
+
+    Command $env "build/dist/$project-src-$version.tar.gz",
+    		$tar_gz,
+		map("build/$project-src/$_", @src_files), qq(
+	rm -rf build/$project-src-$version
+	cp -r build/$project-src build/$project-src-$version
+	find build/$project-src-$version -name .consign -exec rm {} \\;
+	cd build && tar zcf dist/%>:f $project-src-$version
+    );
+}
+"""
+TestCmd.py:  a testing framework for commands and scripts.
+
+The TestCmd module provides a framework for portable automated testing
+of executable commands and scripts (in any language, not just Python),
+especially commands and scripts that require file system interaction.
+
+In addition to running tests and evaluating conditions, the TestCmd module
+manages and cleans up one or more temporary workspace directories, and
+provides methods for creating files and directories in those workspace
+directories from in-line data, here-documents), allowing tests to be
+completely self-contained.
+
+A TestCmd environment object is created via the usual invocation:
+
+    test = TestCmd()
+
+The TestCmd module provides pass_test(), fail_test(), and no_result()
+unbound methods that report test results for use with the Aegis change
+management system.  These methods terminate the test immediately,
+reporting PASSED, FAILED, or NO RESULT respectively, and exiting with
+status 0 (success), 1 or 2 respectively.  This allows for a distinction
+between an actual failed test and a test that could not be properly
+evaluated because of an external condition (such as a full file system
+or incorrect permissions).
+"""
+
+# Copyright 2000 Steven Knight
+# This module is free software, and you may redistribute it and/or modify
+# it under the same terms as Python itself, so long as this copyright message
+# and disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
+# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
+# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+#
+# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
+# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+from string import join, split
+
+__author__ = "Steven Knight <knight@baldmt.com>"
+__revision__ = "TestCmd.py 0.D001 2001/01/14 00:43:41 software"
+__version__ = "0.01"
+
+from types import *
+
+import FCNTL
+import os
+import os.path
+import popen2
+import re
+import shutil
+import stat
+import sys
+import tempfile
+import traceback
+
+tempfile.template = 'testcmd.'
+
+_Cleanup = []
+
+def _clean():
+    global _Cleanup
+    list = _Cleanup[:]
+    _Cleanup = []
+    list.reverse()
+    for test in list:
+	test.cleanup()
+
+sys.exitfunc = _clean
+
+def _caller(tblist, skip):
+    string = ""
+    arr = []
+    for file, line, name, text in tblist:
+	if file[-10:] == "TestCmd.py":
+		break
+	arr = [(file, line, name, text)] + arr
+    atfrom = "at"
+    for file, line, name, text in arr[skip:]:
+	if name == "?":
+	    name = ""
+	else:
+	    name = " (" + name + ")"
+	string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name))
+	atfrom = "\tfrom"
+    return string
+
+def fail_test(self = None, condition = 1, function = None, skip = 0):
+    """Cause the test to fail.
+
+    By default, the fail_test() method reports that the test FAILED
+    and exits with a status of 1.  If a condition argument is supplied,
+    the test fails only if the condition is true.
+    """
+    if not condition:
+	return
+    if not function is None:
+	function()
+    of = ""
+    desc = ""
+    sep = " "
+    if not self is None:
+	if self.program:
+	    of = " of " + self.program
+	    sep = "\n\t"
+	if self.description:
+	    desc = " [" + self.description + "]"
+	    sep = "\n\t"
+
+    at = _caller(traceback.extract_stack(), skip)
+    sys.stderr.write("FAILED test" + of + desc + sep + at)
+
+    sys.exit(1)
+
+def no_result(self = None, condition = 1, function = None, skip = 0):
+    """Causes a test to exit with no valid result.
+
+    By default, the no_result() method reports NO RESULT for the test
+    and exits with a status of 2.  If a condition argument is supplied,
+    the test fails only if the condition is true.
+    """
+    if not condition:
+	return
+    if not function is None:
+	function()
+    of = ""
+    desc = ""
+    sep = " "
+    if not self is None:
+	if self.program:
+	    of = " of " + self.program
+	    sep = "\n\t"
+	if self.description:
+	    desc = " [" + self.description + "]"
+	    sep = "\n\t"
+
+    at = _caller(traceback.extract_stack(), skip)
+    sys.stderr.write("NO RESULT for test" + of + desc + sep + at)
+
+    sys.exit(2)
+
+def pass_test(self = None, condition = 1, function = None):
+    """Causes a test to pass.
+
+    By default, the pass_test() method reports PASSED for the test
+    and exits with a status of 0.  If a condition argument is supplied,
+    the test passes only if the condition is true.
+    """
+    if not condition:
+	return
+    if not function is None:
+	function()
+    sys.stderr.write("PASSED\n")
+    sys.exit(0)
+
+def match_exact(lines = None, matches = None):
+    """
+    """
+    if not type(lines) is ListType:
+	lines = split(lines, "\n")
+    if not type(matches) is ListType:
+	matches = split(matches, "\n")
+    if len(lines) != len(matches):
+	return
+    for i in range(len(lines)):
+	if lines[i] != matches[i]:
+	    return
+    return 1
+
+def match_re(lines = None, res = None):
+    """
+    """
+    if not type(lines) is ListType:
+	lines = split(lines, "\n")
+    if not type(res) is ListType:
+	res = split(res, "\n")
+    if len(lines) != len(res):
+	return
+    for i in range(len(lines)):
+	if not re.compile("^" + res[i] + "$").search(lines[i]):
+	    return
+    return 1
+
+class TestCmd:
+    """Class TestCmd
+    """
+
+    def __init__(self, description = None,
+			program = None,
+			interpreter = None,
+			workdir = None,
+			subdir = None,
+			verbose = 0,
+			match = None):
+	self._cwd = os.getcwd()
+	self.description_set(description)
+	self.program_set(program)
+	self.interpreter_set(interpreter)
+	self.verbose_set(verbose)
+	if not match is None:
+	    self.match_func = match
+	else:
+	    self.match_func = match_re
+	self._dirlist = []
+	self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0}
+	if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '':
+	    self._preserve['pass_test'] = os.environ['PRESERVE']
+	    self._preserve['fail_test'] = os.environ['PRESERVE']
+	    self._preserve['no_result'] = os.environ['PRESERVE']
+	else:
+	    try:
+		self._preserve['pass_test'] = os.environ['PRESERVE_PASS']
+	    except KeyError:
+		pass
+	    try:
+		self._preserve['fail_test'] = os.environ['PRESERVE_FAIL']
+	    except KeyError:
+		pass
+	    try:
+		self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT']
+	    except KeyError:
+		pass
+	self._stdout = []
+	self._stderr = []
+	self.status = None
+	self.condition = 'no_result'
+	self.workdir_set(workdir)
+	self.subdir(subdir)
+
+    def __del__(self):
+	self.cleanup()
+
+    def __repr__(self):
+	return "%x" % id(self)
+
+    def cleanup(self, condition = None):
+	"""Removes any temporary working directories for the specified
+	TestCmd environment.  If the environment variable PRESERVE was
+	set when the TestCmd environment was created, temporary working
+	directories are not removed.  If any of the environment variables
+	PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set
+	when the TestCmd environment was created, then temporary working
+	directories are not removed if the test passed, failed, or had
+	no result, respectively.  Temporary working directories are also
+	preserved for conditions specified via the preserve method.
+
+	Typically, this method is not called directly, but is used when
+	the script exits to clean up temporary working directories as
+	appropriate for the exit status.
+	"""
+	if not self._dirlist:
+	    return
+	if condition is None:
+	    condition = self.condition
+	#print "cleanup(" + condition + "):  ", self._preserve
+	if self._preserve[condition]:
+	    return
+	os.chdir(self._cwd)
+	self.workdir = None
+	list = self._dirlist[:]
+	self._dirlist = []
+	list.reverse()
+	for dir in list:
+	    self.writable(dir, 1)
+	    shutil.rmtree(dir, ignore_errors = 1)
+	try:
+	    global _Cleanup
+	    _Cleanup.remove(self)
+	except (AttributeError, ValueError):
+	    pass
+
+    def description_set(self, description):
+	"""Set the description of the functionality being tested.
+	"""
+	self.description = description
+
+#    def diff(self):
+#	"""Diff two arrays.
+#	"""
+
+    def fail_test(self, condition = 1, function = None, skip = 0):
+	"""Cause the test to fail.
+	"""
+	if not condition:
+	    return
+	self.condition = 'fail_test'
+	fail_test(self = self,
+		  condition = condition,
+		  function = function,
+		  skip = skip)
+
+    def interpreter_set(self, interpreter):
+	"""Set the program to be used to interpret the program
+	under test as a script.
+	"""
+	self.interpreter = interpreter
+
+    def match(self, lines, matches):
+	"""Compare actual and expected file contents.
+	"""
+	return self.match_func(lines, matches)
+
+    def match_exact(self, lines, matches):
+	"""Compare actual and expected file contents.
+	"""
+	return match_exact(lines, matches)
+
+    def match_re(self, lines, res):
+	"""Compare actual and expected file contents.
+	"""
+	return match_re(lines, res)
+
+    def no_result(self, condition = 1, function = None, skip = 0):
+	"""Report that the test could not be run.
+	"""
+	if not condition:
+	    return
+	self.condition = 'no_result'
+	no_result(self = self,
+		  condition = condition,
+		  function = function,
+		  skip = skip)
+
+    def pass_test(self, condition = 1, function = None):
+	"""Cause the test to pass.
+	"""
+	if not condition:
+	    return
+	self.condition = 'pass_test'
+	pass_test(self = self, condition = condition, function = function)
+
+    def preserve(self, *conditions):
+	"""Arrange for the temporary working directories for the
+	specified TestCmd environment to be preserved for one or more
+	conditions.  If no conditions are specified, arranges for
+	the temporary working directories to be preserved for all
+	conditions.
+	"""
+	if conditions is ():
+	    conditions = ('pass_test', 'fail_test', 'no_result')
+	for cond in conditions:
+	    self._preserve[cond] = 1
+
+    def program_set(self, program):
+	"""Set the executable program or script to be tested.
+	"""
+	if program and not os.path.isabs(program):
+	    program = os.path.join(self._cwd, program)
+	self.program = program
+
+    def read(self, file):
+	"""Reads and returns the contents of the specified file name.
+	The file name may be a list, in which case the elements are
+	concatenated with the os.path.join() method.  The file is
+	assumed to be under the temporary working directory unless it
+	is an absolute path name.
+	"""
+	if type(file) is ListType:
+	    file = apply(os.path.join, tuple(file))
+	if not os.path.isabs(file):
+	    file = os.path.join(self.workdir, file)
+	f = os.fdopen(os.open(file, FCNTL.O_RDONLY))
+	contents = f.read()
+	f.close()
+	return contents
+
+    def run(self, program = None,
+		  interpreter = None,
+		  arguments = None,
+		  chdir = None,
+		  stdin = None):
+	"""Runs a test of the program or script for the test
+	environment.  Standard output and error output are saved for
+	future retrieval via the stdout() and stderr() methods.
+	"""
+	if chdir:
+	    oldcwd = os.getcwd()
+	    if not os.path.isabs(chdir):
+		chdir = os.path.join(self.workpath(chdir))
+	    if self.verbose:
+		sys.stderr.write("chdir(" + chdir + ")\n")
+	    os.chdir(chdir)
+	cmd = None
+	if program:
+	    if not os.path.isabs(program):
+		program = os.path.join(self._cwd, program)
+	    cmd = program
+	    if interpreter:
+		cmd = interpreter + " " + cmd
+	else:
+	    cmd = self.program
+	    if self.interpreter:
+		cmd =  self.interpreter + " " + cmd
+	if arguments:
+	    cmd = cmd + " " + arguments
+	if self.verbose:
+	    sys.stderr.write(cmd + "\n")
+	p = popen2.Popen3(cmd, 1)
+	if stdin:
+	    if type(stdin) is ListType:
+		for line in stdin:
+		    p.tochild.write(line)
+	    else:
+		p.tochild.write(stdin)
+	p.tochild.close()
+	self._stdout.append(p.fromchild.read())
+	self._stderr.append(p.childerr.read())
+	self.status = p.wait()
+	if chdir:
+	    os.chdir(oldcwd)
+
+    def stderr(self, run = None):
+	"""Returns the error output from the specified run number.
+	If there is no specified run number, then returns the error
+	output of the last run.  If the run number is less than zero,
+	then returns the error output from that many runs back from the
+	current run.
+	"""
+	if not run:
+	    run = len(self._stderr)
+	elif run < 0:
+	    run = len(self._stderr) + run
+	run = run - 1
+	return self._stderr[run]
+
+    def stdout(self, run = None):
+	"""Returns the standard output from the specified run number.
+	If there is no specified run number, then returns the standard
+	output of the last run.  If the run number is less than zero,
+	then returns the standard output from that many runs back from
+	the current run.
+	"""
+	if not run:
+	    run = len(self._stdout)
+	elif run < 0:
+	    run = len(self._stdout) + run
+	run = run - 1
+	return self._stdout[run]
+
+    def subdir(self, *subdirs):
+	"""Create new subdirectories under the temporary working
+	directory, one for each argument.  An argument may be a list,
+	in which case the list elements are concatenated using the
+	os.path.join() method.  Subdirectories multiple levels deep
+	must be created using a separate argument for each level:
+
+		test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory'])
+
+	Returns the number of subdirectories actually created.
+	"""
+	count = 0
+	for sub in subdirs:
+	    if sub is None:
+		continue
+	    if type(sub) is ListType:
+		sub = apply(os.path.join, tuple(sub))
+	    new = os.path.join(self.workdir, sub)
+	    try:
+		os.mkdir(new)
+	    except:
+		pass
+	    else:
+		count = count + 1
+	return count
+
+    def verbose_set(self, verbose):
+	"""Set the verbose level.
+	"""
+	self.verbose = verbose
+
+    def workdir_set(self, path):
+	"""Creates a temporary working directory with the specified
+	path name.  If the path is a null string (''), a unique
+	directory name is created.
+	"""
+	if (path != None):
+	    if path == '':
+		path = tempfile.mktemp()
+	    if path != None:
+		os.mkdir(path)
+	    self._dirlist.append(path)
+	    global _Cleanup
+	    try:
+		_Cleanup.index(self)
+	    except ValueError:
+		_Cleanup.append(self)
+	    # We'd like to set self.workdir like this:
+	    #	self.workdir = path
+	    # But symlinks in the path will report things
+	    # differently from os.getcwd(), so chdir there
+	    # and back to fetch the canonical path.
+	    cwd = os.getcwd()
+	    os.chdir(path)
+	    self.workdir = os.getcwd()
+	    os.chdir(cwd)
+	else:
+	    self.workdir = None
+
+    def workpath(self, *args):
+	"""Returns the absolute path name to a subdirectory or file
+	within the current temporary working directory.  Concatenates
+	the temporary working directory name with the specified
+	arguments using the os.path.join() method.
+	"""
+	return apply(os.path.join, (self.workdir,) + tuple(args))
+
+    def writable(self, top, write):
+	"""Make the specified directory tree writable (write == 1)
+	or not (write == None).
+	"""
+
+	def _walk_chmod(arg, dirname, names):
+	    st = os.stat(dirname)
+	    os.chmod(dirname, arg(st[stat.ST_MODE]))
+	    for name in names:
+		n = os.path.join(dirname, name)
+		st = os.stat(n)
+		os.chmod(n, arg(st[stat.ST_MODE]))
+
+	def _mode_writable(mode):
+	    return stat.S_IMODE(mode|0200)
+
+	def _mode_non_writable(mode):
+	    return stat.S_IMODE(mode&~0200)
+
+	if write:
+	    f = _mode_writable
+	else:
+	    f = _mode_non_writable
+	os.path.walk(top, _walk_chmod, f)
+
+    def write(self, file, content):
+	"""Writes the specified content text (second argument) to the
+	specified file name (first argument).  The file name may be
+	a list, in which case the elements are concatenated with the
+	os.path.join() method.	The file is created under the temporary
+	working directory.  Any subdirectories in the path must already
+	exist.	"""
+	if type(file) is ListType:
+	    file = apply(os.path.join, tuple(file))
+	if not os.path.isabs(file):
+	    file = os.path.join(self.workdir, file)
+	fd = os.open(file, FCNTL.O_CREAT|FCNTL.O_WRONLY)
+	os.write(fd, content)
+	os.close(fd)
+/*
+ *	aegis - project change supervisor
+ *	This file is in the Public Domain, 1995, Peter Miller.
+ *
+ * MANIFEST: example use of make in project config file
+ *
+ * The make(1) program exists in many forms, usually one is available with each
+ * UNIX version.  The one used in the writing of this section is GNU Make 3.70,
+ * avaiable by anonymous FTP from your nearest GNU archive site.  GNU Make was
+ * chosen because it was the most powerful, it is widely avaiable (usually for
+ * little or no cost) and discussion of the alternatives (SunOS make, BSD 4.3
+ * make, etc), would not be universally applicable.  "Plain vanilla" make
+ * (with no transitive closure, no pattern rules, no functions) is not
+ * sufficiently capable to satisfy the demands placed on it by aegis.
+ *
+ * As mentioned in the Dependency Maintenance Tool chapter of the User Guide,
+ * make is not really sufficient, because it lacks dynamic include dependencies.
+ * However, GNU Make has a form of dynamic include dependencies, and it has a
+ * few quirks, but mostly works well.
+ *
+ * The other feature lacking in make is a search path.  While GNU Make has
+ * functionality called VPATH, the implementation leaves something to be
+ * desired, and can't be used for the search path functionality required by
+ * aegis.  Because of this, the create_symlinks_before_build field of the
+ * project config file is set to true so that aegis will arrange for the
+ * development directory to be fiull of symbolic links, making it appear that
+ * the entire project is in each change's development directory.
+ */
+
+/*
+ * The build_command field of the project config file is used to invoke the
+ * relevant build command.  This command tells make where to find the rules.
+ * The ${s Makefile} expands to a path into the baseline during development
+ * if the file is not in the change.  Look in aesub(5) for more information
+ * about command substitutions.
+ */
+build_command = "cons date='${DAte %Y/%m/%d %H:%M:%S}' developer=${DEVeloper} version=${VERsion}";
+
+/*
+ * The rules used in the User Guide all remove their targets before
+ * constructing them, which qualifies them for the following entry in the
+ * config file.  The files must be removed first, otherwise the baseline would
+ * cease to be self-consistent.
+ */
+link_integration_directory = true;
+
+/*
+ * Another field to be set in this file is one which tells aegis to maintain
+ * symbolic links between the development directory and the basline.  This also
+ * requires that rules remove their targets before constructing them, to ensure
+ * that development builds do not attempt to write their results onto the
+ * read-only versions in the baseline.
+ */
+create_symlinks_before_build = true;
+
+/*
+ * NOT UNTIL AEGIS 3.23; we may not need it anyway.
+remove_symlinks_after_build = false;
+ */
+
+/*
+integrate_begin_command =
+	"";
+*/
+
+/*
+ *	aegis - project change supervisor
+ *	This file is in the Public Domain, 1995, 1998 Peter Miller.
+ *
+ * MANIFEST: example of using rcs in the project config file
+ *
+ * The entries for the commands are listed below.  RCS uses a slightly
+ * different model than aegis wants, so some maneuvering is required.
+ * The command strings in this section assume that the RCS commands ci and co
+ * and rcs and rlog are in the command search PATH, but you may like to
+ * hard-wire the paths, or set PATH at the start of each.  You should also note
+ * that the strings are always handed to the Bourne shell to be executed, and
+ * are set to exit with an error immediately a sub-command fails.
+ *
+ * In these commands, the RCS file is kept unlocked, since only the owner will
+ * be checking changes in.  The RCS functionality for coordinating shared
+ * access is not required.
+ *
+ * One advantage of using RCS version 5.6 or later is that binary files are
+ * supported, should you want to have binary files in the baseline.
+ *
+ * The ${quote ...} construct is used to quote filenames which contain
+ * shell special characters.  A minimum of quoting is performed, so if
+ * the filenames do not contail shell special characters, no quotes will
+ * be used.
+ */
+
+/*
+ * This command is used to create a new file history.
+ * This command is always executed as the project owner.
+ * The following substitutions are available:
+ *
+ * ${Input}
+ *	absolute path of the source file
+ * ${History}
+ *	absolute path of the history file
+ *
+ * The "ci -f" option is used to specify that a copy is to be checked-in even
+ *	if there are no changes.
+ * The "ci -u" option is used to specify that an unlocked copy will remain in
+ *	the baseline.
+ * The "ci -d" option is used to specify that the file time rather than the
+ *	current time is to be used for the new revision.
+ * The "ci -M" option is used to specify that the mode date on the original
+ *	file is not to be altered.
+ * The "ci -t" option is used to specify that there is to be no description
+ *	text for the new RCS file.
+ * The "ci -m" option is used to specify that the change number is to be stored
+ *	in the file log if this is actually an update (typically from aenf
+ *	after aerm on the same file name).
+ * The "rcs -U" option is used to specify that the new RCS file is to have
+ *	unstrict locking.
+ * The "rcs -kk" option is used to specify that keyword substitution is
+ *	disabled (only keyword names, not values, are substituted).
+ */
+history_create_command =
+	"ci -f -u -d -M -m$c -t/dev/null ${quote $input} ${quote $history,v}; \
+rcs -kk -U ${quote $history,v}";
+
+
+/*
+ * This command is used to get a specific edit back from history.
+ * This command is always executed as the project owner.
+ * The following substitutions are available:
+ *
+ * ${History}
+ *	absolute path of the history file
+ * ${Edit}
+ *	edit number, as given by history_\%query_\%command
+ * ${Output}
+ *	absolute path of the destination file
+ *
+ * The "co -r" option is used to specify the edit to be retrieved.
+ * The "co -p" option is used to specify that the results be printed on the
+ *	standard output; this is because the destination filename will never
+ *	look anything like the history source filename.
+ * The "rcs -kk" option is used to specify that keyword substitution is
+ *	disabled (only keyword names, not values, are substituted).
+ */
+history_get_command =
+	"co -kk -r${quote $edit} -p ${quote $history,v} > ${quote $output}";
+
+/*
+ * This command is used to add a new "top-most" entry to the history file.
+ * This command is always executed as the project owner.
+ * The following substitutions are available:
+ *
+ * ${Input}
+ *	absolute path of source file
+ * ${History}
+ *	absolute path of history file
+ *
+ * The "ci -f" option is used to specify that a copy is to be checked-in even
+ *	if there are no changes.
+ * The "ci -u" option is used to specify that an unlocked copy will remain in
+ *	the baseline.
+ * The "ci -d" option is used to specify that the file time rather than the
+ *	current time is to be used for the new revision.
+ * The "ci -M" option is used to specify that the mode date on the original
+ *	file is not to be altered.
+ * The "ci -m" option is used to specify that the change number is to be stored
+ *	in the file log, which allows rlog to be used to find the change
+ *	numbers to which each revision of the file corresponds.
+ *
+ * It is possible for a a very cautious approach has been taken, in which case
+ * the history_put_command may be set to the same string specified above for
+ * the history_create_command.
+ */
+history_put_command =
+	"ci -f -u -d -M -m$c ${quote $input} ${quote $history,v}";
+
+/*
+ * This command is used to query what the history mechanism calls the top-most
+ * edit of a history file.  The result may be any arbitrary string, it need not
+ * be anything like a number, just so long as it uniquely identifies the edit
+ * for use by the history_get_command at a later date.  The edit number is to
+ * be printed on the standard output.  This command is always executed as the
+ * project owner.
+ *
+ * The following substitutions are available:
+ *
+ * ${History}
+ *	absolute path of the history file
+ */
+history_query_command =
+	"rlog -r ${quote $history,v} | awk '/^head:/ {print $$2}'";
+
+/*
+ * RCS also provides a merge program, which can be used to provide a three-way
+ * merge.  It has an ouput format some sites prefer to the fmerge output.
+ *
+ * This command is used by aed(1) to produce a difference listing when a file
+ * in the development directory is out of date compared to the current version
+ * in the baseline.
+ *
+ * All of the command substitutions described in aesub(5) are available.
+ * In addition, the following substitutions are also available:
+ *
+ * ${ORiginal}
+ *	The absolute path name of a file containing the common ancestor
+ *	version of ${MostRecent} and {$Input}.  Usually the version originally
+ *	copied into the change.  Usually in a temporary file.
+ * ${Most_Recent}
+ *	The absolute path name of a file containing the most recent version.
+ *	Usually in the baseline.
+ * ${Input}
+ *	The absolute path name of the edited version of the file.  Usually in
+ *	the development directory.
+ * ${Output}
+ *	The absolute path name of the file in which to write the difference
+ *	listing.  Usually in the development directory.
+ *
+ * An exit status of 0 means successful, even of the files differ (and they
+ * usually do).  An exit status which is non-zero means something is wrong.
+ *
+ * The "merge -L" options are used to specify labels for the baseline and the
+ *	development directory, respecticvely, when conflict lines are inserted
+ *	into the result.
+ * The "merge -p" options is used to specify that the results are to be printed
+ *	on the standard output.
+ */
+
+diff3_command =
+	"set +e; \
+merge -p -L baseline -L C$c ${quote $mostrecent} ${quote $original} \
+${quote $input} > ${quote $output}; \
+test $? -le 1";
+
+diff_command =
+	"set +e; \
+	diff -c ${quote $original} ${quote $input} > ${quote $output}; \
+	test $? -le 1";
+
+/*
+ * We use an intermediary test.pl script to execute tests.
+ * This serves as glue between the tests themselves (which are
+ * written to conform to Perl conventions) and Aegis' expectations.
+ * See the comments in the test.pl script itself for details.
+ */
+test_command = "python runtest.py -v ${VERsion} ${File_Name}";
+
+/*
+ *
+ */
+file_template =
+[
+	{
+		pattern = [ "src/scons/*__init__.py" ];
+		body = "${read_file ${source template/__init__.py abs}}";
+	},
+	{
+		pattern = [ "src/scons/*Tests.py" ];
+		body = "${read_file ${source template/test.py abs}}";
+	},
+	{
+		pattern = [ "src/scons/*.py" ];
+		body = "${read_file ${source template/file.py abs}}";
+	},
+];
+#!/usr/bin/env python
+
+import getopt, os, os.path, re, string, sys
+
+opts, tests = getopt.getopt(sys.argv[1:], "dv:")
+
+debug = ''
+version = None
+
+for o, a in opts:
+    if o == '-d': debug = "/usr/lib/python1.5/pdb.py"
+    if o == '-v': version = a
+
+if not version:
+    version = os.popen("aesub '$version'").read()[:-1]
+
+match = re.compile(r'^[CD]0*')
+
+def aegis_to_version(aever):
+    arr = string.split(aever, '.')
+    end = max(len(arr) - 1, 2)
+    arr = map(lambda e: match.sub('', e), arr[:end])
+    def rep(e):
+    	if len(e) == 1:
+	    e = '0' + e
+	return e
+    arr[1:] = map(rep, arr[1:])
+    return string.join(arr, '.')
+
+version = aegis_to_version(version)
+
+cwd = os.getcwd()
+
+map(os.path.abspath, tests)
+
+build_test = os.path.join(cwd, "build", "test")
+scons_ver = os.path.join(build_test, "scons-" + version)
+
+os.chdir(scons_ver)
+
+os.environ['PYTHONPATH']  = scons_ver + ':' + build_test
+
+exit = 0
+
+for path in tests:
+    if not os.path.isabs(path):
+	path = os.path.join(cwd, path)
+    if os.system("python " + debug + " " + path):
+	exit = 1
+
+sys.exit(exit)
+*,D
+*.pyc
+.*.swp
+.consign
+MANIFEST
+scons/__init__.py
+scons/Builder.py
+scons/Defaults.py
+scons/Environment.py
+scons/Node/__init__.py
+scons/Node/FS.py
+scons/Sig/__init__.py
+scons/Sig/MD5.py
+scons/Sig/TimeStamp.py
+scons.py
+setup.py
+#!/usr/bin/env python
+
+import getopt
+import os.path
+import string
+import sys
+
+opts, targets = getopt.getopt(sys.argv[1:], 'f:')
+
+Scripts = []
+
+for o, a in opts:
+    if o == '-f': Scripts.append(a)
+
+if not Scripts:
+    Scripts.append('SConstruct')
+
+
+# XXX The commented-out code here adds any "scons" subdirs in anything
+# along sys.path to sys.path.  This was an attempt at setting up things
+# so we can import "node.FS" instead of "scons.Node.FS".  This doesn't
+# quite fit our testing methodology, though, so save it for now until
+# the right solutions pops up.
+#
+#dirlist = []
+#for dir in sys.path:
+#    scons = os.path.join(dir, 'scons')
+#    if os.path.isdir(scons):
+#	dirlist = dirlist + [scons]
+#    dirlist = dirlist + [dir]
+#
+#sys.path = dirlist
+
+from scons.Node.FS import init, Dir, File, lookup
+from scons.Environment import Environment
+
+init()
+
+
+
+def Conscript(filename):
+    Scripts.append(filename)
+
+
+
+while Scripts:
+    file, Scripts = Scripts[0], Scripts[1:]
+    execfile(file)
+
+
+
+for path in targets:
+	target = lookup(File, path)
+	target.build()

src/scons/.aeignore

+*,D
+*.pyc
+.*.swp
+.consign

src/scons/Builder.py

+"""scons.Builder
+
+XXX
+
+"""
+
+__revision__ = "Builder.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import os
+from types import *
+from scons.Node.FS import Dir, File, lookup
+
+
+
+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 = File):
+	self.name = name
+	self.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, target = None, source = None):
+	node = lookup(self.node_class, target)
+	node.builder_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.
+	"""
+	# XXX THIS SHOULD BE DONE BY TURNING Builder INTO A FACTORY
+	# FOR SUBCLASSES FOR StringType AND FunctionType
+	t = type(self.action)
+	if t == StringType:
+	    cmd = self.action % kw
+	    print cmd
+	    os.system(cmd)
+	elif t == FunctionType:
+	    # XXX WHAT SHOULD WE PRINT HERE
+	    self.action(kw)

src/scons/BuilderTests.py

+__revision__ = "BuilderTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import sys
+import unittest
+
+from scons.Builder import Builder
+from TestCmd import TestCmd
+
+
+# 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(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 the simple ability to create a Builder
+	and retrieve the supplied action attribute.
+	"""
+	builder = Builder(action = "foo")
+	assert builder.action == "foo"
+
+    def test_cmp(self):
+	"""Test simple comparisons of Builder objects.
+	"""
+	b1 = Builder(input_suffix = '.o')
+	b2 = Builder(input_suffix = '.o')
+	assert b1 == b2
+	b3 = Builder(input_suffix = '.x')
+	assert b1 != b3
+	assert b2 != b3
+
+    def test_execute(self):
+	"""Test the ability to execute simple Builders, one
+	a string that executes an external command, and one an
+	internal function.
+	"""
+	cmd = "python %s %s xyzzy" % (act_py, outfile)
+	builder = Builder(action = cmd)
+	builder.execute()
+	assert test.read(outfile) == "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 = Builder(action = function)
+	builder.execute(out = outfile)
+	assert test.read(outfile) == "function\n"
+
+    def test_insuffix(self):
+	"""Test the ability to create a Builder with a specified
+	input suffix, making sure that the '.' separator is
+	appended to the beginning if it isn't already present.
+	"""
+	builder = Builder(input_suffix = '.c')
+	assert builder.insuffix == '.c'
+	builder = Builder(input_suffix = 'c')
+	assert builder.insuffix == '.c'
+
+    def test_name(self):
+	"""Test the ability to create a Builder with a specified
+	name.
+	"""
+	builder = Builder(name = 'foo')
+	assert builder.name == 'foo'
+
+    def test_node_class(self):
+	"""Test the ability to create a Builder that creates nodes
+	of the specified class.
+	"""
+	class Foo:
+		pass
+	builder = Builder(node_class = Foo)
+	assert builder.node_class is Foo
+
+    def test_outsuffix(self):
+	"""Test the ability to create a Builder with a specified
+	output suffix, making sure that the '.' separator is
+	appended to the beginning if it isn't already present.
+	"""
+	builder = Builder(input_suffix = '.o')
+	assert builder.insuffix == '.o'
+	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)

src/scons/Defaults.py

+"""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__"
+
+
+
+from scons.Builder import Builder
+
+
+
+Object = Builder(name = 'Object', action = 'cc -c -o %(target)s %(source)s')
+Program = Builder(name = 'Program', action = 'cc -o %(target)s %(source)s')
+
+Builders = [Object, Program]

src/scons/Environment.py

+"""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))
+	for b in kw['BUILDERS']:
+	    setattr(self, b.name, 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.
+	"""
+	return copy.deepcopy(self)
+
+    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

src/scons/EnvironmentTests.py

+__revision__ = "EnivronmentTests.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 the ability to execute simple builders through
+	different environment, one 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 the ability to copy a construction Environment.
+	Update the copy independently afterwards and check that
+	the original remains intact (that is, no dangling
+	references point to objects in the copied environment).
+	"""
+	env1 = Environment(XXX = 'x', YYY = 'y')
+	env2 = env1.Copy()
+	env1copy = env1.Copy()
+	env2.Update(YYY = 'yyy')
+	assert env1 != env2
+	assert env1 == env1copy
+
+    def test_Dictionary(self):
+	"""Test the simple ability to retrieve known construction
+	variables 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 the simple ability to create construction
+	Environments.  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 the ability to update a construction Environment
+	with new construction variables after it was first created.
+	"""
+	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 the ability to substitute construction variables
+	into a string.  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)

src/scons/Node/.aeignore

+*,D
+*.pyc
+.*.swp
+.consign

src/scons/Node/FS.py

+"""scons.Node.FS
+
+File system nodes.
+
+"""
+
+__revision__ = "Node/FS.py __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import os
+import os.path
+from scons.Node import 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(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(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)

src/scons/Node/FS/.aeignore

+*,D
+*.pyc
+.*.swp
+.consign

src/scons/Node/FSTests.py

+__revision__ = "Node/FSTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import sys
+import unittest
+
+from scons.Node.FS import init, lookup, Dir, File
+
+
+
+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):
+	"""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)
+
+	init()
+
+	def Dir_test(lpath, path, abspath, up_path):
+	    dir = lookup(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 = lookup(Dir, 'd1')
+
+	f1 = lookup(File, 'f1', directory = d1)
+
+	assert(f1.path == 'd1/f1')
+
+	try:
+	    f2 = lookup(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 = lookup(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)

src/scons/Node/NodeTests.py

+__revision__ = "Node/NodeTests.py __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import sys
+import unittest
+
+from scons.Node import 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 the ability to build a node.
+	"""
+	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):
+	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)

src/scons/Node/__init__.py

+"""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

src/scons/Sig/.aeignore

+*,D
+*.pyc
+.*.swp
+.consign

src/scons/Sig/MD5.py

+"""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

src/scons/Sig/MD5Tests.py

+__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 the ability to decide if an object is up-to-date
+	with 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 the ability to collect a sequence of object 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)

src/scons/Sig/TimeStamp.py

+"""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

src/scons/Sig/TimeStampTests.py

+__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 the ability to decide if an object is up-to-date
+	with 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 the ability to collect a sequence of object timestamps
+	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)

src/scons/Sig/__init__.py

+"""scons.Sig
+
+The Signature package for the scons software construction utility.
+
+"""
+
+__revision__ = "Sig/__init__.py __REVISION__ __DATE__ __DEVELOPER__"

src/scons/__init__.py

+"""scons
+
+The main package for the scons software construction utility.
+
+"""
+
+__revision__ = "__init__.py __REVISION__ __DATE__ __DEVELOPER__"
+
+__version__ = "__VERSION__"
+__revision__ = "setup.py __REVISION__ __DATE__ __DEVELOPER__"
+
+from string import join, split
+
+from distutils.core import setup
+
+setup(name = "scons",
+      version = "__VERSION__",
+      description = "scons",
+      author = "Steven Knight",
+      author_email = "knight@baldmt.com",
+      url = "http://www.baldmt.com/scons",
+      packages = ["scons"],
+      scripts = ["scons.py"])

template/.aeignore

+*,D
+.consign

template/__init__.py

+"""${subst '/' '.' ${subst '^src/' '' ${subst '/[^/]*$' '' $filename}}}
+
+XXX
+
+"""
+
+__revision__ = "${subst '^src/scons/' '' $filename} __REVISION__ __DATE__ __DEVELOPER__"
+
+__version__ = "__VERSION__"
+"""${subst '/' '.' ${subst '^src/' '' ${subst '\.py$' '' $filename}}}
+
+XXX
+
+"""
+
+__revision__ = "${subst '^src/scons/' '' $filename} __REVISION__ __DATE__ __DEVELOPER__"
+
+
+
+import XXX
+__revision__ = "${subst '^src/scons/' '' $filename} __REVISION__ __DATE__ __DEVELOPER__"
+
+from TestCmd import TestCmd
+*,D
+.*.swp
+.consign
+#!/usr/bin/env python
+
+__revision__ = "test/t0001.t __REVISION__ __DATE__ __DEVELOPER__"
+
+from TestCmd import TestCmd
+
+test = TestCmd(program = 'scons.py', workdir = '', interpreter = 'python')
+
+test.write('SConstruct', """
+import os
+print "SConstruct", os.getcwd()
+Conscript('SConscript')
+""")
+
+# XXX I THINK THEY SHOULD HAVE TO RE-IMPORT OS HERE,
+# WHICH THEY DO FOR THE SECOND TEST BELOW, BUT NOT THE FIRST...
+test.write('SConscript', """
+import os
+print "SConscript " + os.getcwd()
+""")
+
+wpath = test.workpath()
+
+test.run(chdir = '.')
+test.fail_test(test.stdout() != ("SConstruct %s\nSConscript %s\n" % (wpath, wpath)))
+
+test.run(chdir = '.', arguments = '-f SConscript')
+test.fail_test(test.stdout() != ("SConscript %s\n" % wpath))
+
+test.pass_test()
+#!/usr/bin/env python
+
+__revision__ = "test/t0001.t __REVISION__ __DATE__ __DEVELOPER__"
+
+from TestCmd import TestCmd
+
+test = TestCmd(program = 'scons.py', workdir = '', interpreter = 'python')
+
+test.write('SConstruct', """
+env = Environment()
+env.Program(target = 'foo', source = 'foo.c')
+""")
+
+test.write('foo.c', """
+int
+main(int argc, char *argv[])
+{
+	printf("foo.c\n");
+	exit (0);
+}
+""")
+
+test.run(chdir = '.', arguments = 'foo')
+
+test.run(program = test.workpath('foo'))
+
+test.fail_test(test.stdout() != "foo.c\n")
+
+test.pass_test()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to