1. SCons
  2. Core
  3. SCons

Commits

Steven Knight  committed c659f90

Add BuildDir(), Export(), and Install() functionality (courtesy Charles Crain).

  • Participants
  • Parent commits 9364a80
  • Branches default

Comments (0)

Files changed (15)

File src/CHANGES.txt

View file
   - Fixed the version comment in the scons.bat (the UNIX geek used
     # instead of @rem).
 
+  - Added support for the Install method (courtesy Charles Crain).
+
+  - Added support for the BuildDir method (courtesy Charles Crain).
+
+  - Added the Export method (courtesy Charles Crain).
+
 
 
 RELEASE 0.01 - Thu Dec 13 19:25:23 CST 2001

File src/engine/SCons/Environment.py

View file
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 
-
+import os
 import copy
 import os.path
 import re
 import SCons.Defaults
 from SCons.Errors import UserError
 from UserList import UserList
+import SCons.Node.FS
+import sys
+import shutil
 
-def Command():
-    pass	# XXX
+def installFunc(env, target, source):
+    try:
+        os.unlink(target)
+    except OSError:
+        pass
+    
+    try:
+        SCons.Node.FS.file_link(source[0], target)
+        print 'Install file: "%s" as "%s"' % \
+              (source[0], target)
+        return 0
+    except IOError, e:
+        sys.stderr.write('Unable to install "%s" as "%s"\n%s\n' % \
+                         (source[0], target, str(e)))
+        return -1
+    except OSError, e:
+        sys.stderr.write('Unable to install "%s" as "%s"\n%s\n' % \
+                         (source[0], target, str(e)))
+        return -1
 
-def Install():
-    pass	# XXX
+InstallBuilder = SCons.Builder.Builder(name='Install',
+                                       action=installFunc)
 
 def InstallAs():
     pass	# XXX
         bld = SCons.Builder.Builder(name="Command", action=action)
         return bld(self, target, source)
 
+    def Install(self, dir, source):
+        """Install specified files in the given directory."""
+        sources = SCons.Util.scons_str2nodes(source)
+        dnodes = SCons.Util.scons_str2nodes(dir,
+                                            SCons.Node.FS.default_fs.Dir)
+        tgt = []
+        for dnode in dnodes:
+            for src in sources:
+                target = SCons.Node.FS.default_fs.File(src.name, dnode)
+                tgt.append(InstallBuilder(self, target, src))
+        if len(tgt) == 1:
+            tgt = tgt[0]
+        return tgt
+
     def subst(self, string):
 	"""Recursively interpolates construction variables from the
 	Environment into the specified string, returning the expanded

File src/engine/SCons/EnvironmentTests.py

View file
 	s = env3.get_scanner(".cxx")
 	assert s == None, s
 
-    def test_Command(self):
-	pass	# XXX
-
     def test_Copy(self):
 	"""Test construction Environment copying
 
 	assert env1 == env2
 
     def test_Install(self):
-	pass	# XXX
+	"""Test Install method"""
+        env=Environment()
+        tgt = env.Install('export', [ 'build/foo1', 'build/foo2' ])
+        paths = map(str, tgt)
+        paths.sort()
+        assert paths == [ 'export/foo1', 'export/foo2' ], paths
+        for tnode in tgt:
+            assert tnode.builder == InstallBuilder
 
     def test_InstallAs(self):
 	pass	# XXX

File src/engine/SCons/Node/FS.py

View file
 import SCons.Node
 from UserDict import UserDict
 import sys
+from SCons.Errors import UserError
+
+try:
+    import os
+    file_link = os.link
+except AttributeError:
+    import shutil
+    import stat
+    def file_link(src, dest):
+        shutil.copyfile(src, dest)
+        st=os.stat(src)
+        os.chmod(dest, stat.S_IMODE(st[stat.ST_MODE]))
 
 class PathName:
     """This is a string like object with limited capabilities (i.e.,
         self.Root = PathDict()
         self.Top = self.__doLookup(Dir, path)
         self.Top.path = '.'
+        self.Top.srcpath = '.'
         self.Top.path_ = os.path.join('.', '')
         self.cwd = self.Top
 
         name, directory = self.__transformPath(name, directory)
         return self.__doLookup(Dir, name, directory)
 
-
+    def BuildDir(self, build_dir, src_dir):
+        """Link the supplied build directory to the source directory
+        for purposes of building files."""
+        dirSrc = self.Dir(src_dir)
+        dirBuild = self.Dir(build_dir)
+        if not dirSrc.is_under(self.Top) or not dirBuild.is_under(self.Top):
+            raise UserError, "Both source and build directories must be under top of build tree."
+        if dirSrc.is_under(dirBuild):
+            raise UserError, "Source directory cannot be under build directory."
+        dirBuild.link(dirSrc)
+        
 
 class Entry(SCons.Node.Node):
     """A generic class for file system entries.  This class if for
         self.abspath_ = self.abspath
         self.dir = directory
 	self.use_signature = 1
+        self.__doSrcpath()
+
+    def adjust_srcpath(self):
+        self.__doSrcpath()
+        
+    def __doSrcpath(self):
+        if self.dir:
+            if str(self.dir.srcpath) == '.':
+                self.srcpath = self.name
+            else:
+                self.srcpath = os.path.join(self.dir.srcpath, self.name)
+        else:
+            self.srcpath = self.name
 
     def __str__(self):
 	"""A FS node's string representation is its path name."""
             return 0
         return None
 
+    def is_under(self, dir):
+        if self is dir:
+            return 1
+        if not self.dir:
+            return 0
+        return self.dir.is_under(dir)
+
 
 
 # XXX TODO?
         self.builder = 1
         self._sconsign = None
 
+    def __doReparent(self):
+        for ent in self.entries.values():
+            if not ent is self and not ent is self.dir:
+                ent.adjust_srcpath()
+
+    def adjust_srcpath(self):
+        Entry.adjust_srcpath(self)
+        self.__doReparent()
+                
+    def link(self, srcdir):
+        """Set this directory as the build directory for the
+        supplied source directory."""
+        self.srcpath = srcdir.path
+        self.__doReparent()
+
     def up(self):
         return self.entries['..']
 
 class File(Entry):
     """A class for files in a file system.
     """
+    def __init__(self, name, directory = None):
+        Entry.__init__(self, name, directory)
+        self._morph()
+        
     def _morph(self):
-	"""Turn a file system node into a File object.  Nothing
-	to be done, actually, because all of the info we need
-	is handled by our base Entry class initialization."""
-        pass
+        """Turn a file system node into a File object."""
+        self.created = 0
 
     def root(self):
         return self.dir.root()
 
     def get_contents(self):
-        return open(self.path, "r").read()
+        if not self.exists():
+            return ''
+        return open(str(self), "r").read()
 
     def get_timestamp(self):
         if self.exists():
                     self.add_implicit(scn.scan(self.path, self.env),
                                       scn)
                     self.scanned[scn] = 1
+                    
+    def exists(self):
+        if not self.created:
+            self.created = 1
+            if self.srcpath != self.path and \
+               os.path.exists(self.srcpath):
+                if os.path.exists(self.path):
+                    os.unlink(self.path)
+                self.__createDir()
+                file_link(self.srcpath, self.path)
+        return Entry.exists(self)
 
     def __createDir(self):
         # ensure that the directories for this node are

File src/engine/SCons/Node/FSTests.py

View file
 import sys
 import unittest
 import SCons.Node.FS
-
+from TestCmd import TestCmd
+from SCons.Errors import UserError
 
 built_it = None
 
     def get_scanner(self, skey):
         return self.scanner
 
+class BuildDirTestCase(unittest.TestCase):
+    def runTest(self):
+        """Test build dir functionality"""
+        fs = SCons.Node.FS.FS()
+        f1 = fs.File('build/test1')
+        fs.BuildDir('build', 'src')
+        f2 = fs.File('build/test2')
+        assert f1.srcpath == 'src/test1', f1.srcpath
+        assert f2.srcpath == 'src/test2', f2.srcpath
+
+        fs = SCons.Node.FS.FS()
+        f1 = fs.File('build/test1')
+        fs.BuildDir('build', '.')
+        f2 = fs.File('build/test2')
+        assert f1.srcpath == 'test1', f1.srcpath
+        assert f2.srcpath == 'test2', f2.srcpath
+
+        fs = SCons.Node.FS.FS()
+        fs.BuildDir('build/var1', 'src')
+        fs.BuildDir('build/var2', 'src')
+        f1 = fs.File('build/var1/test1')
+        f2 = fs.File('build/var2/test1')
+        assert f1.srcpath == 'src/test1', f1.srcpath
+        assert f2.srcpath == 'src/test1', f2.srcpath
+
+        exc_caught = 0
+        try:
+            fs = SCons.Node.FS.FS()
+            fs.BuildDir('/test/foo', '.')
+        except UserError:
+            exc_caught = 1
+        assert exc_caught, "Should have caught a UserError."
+
+        exc_caught = 0
+        try:
+            fs = SCons.Node.FS.FS()
+            fs.BuildDir('build', '/test/foo')
+        except UserError:
+            exc_caught = 1
+        assert exc_caught, "Should have caught a UserError."
+
+        exc_caught = 0
+        try:
+            fs = SCons.Node.FS.FS()
+            fs.BuildDir('build', 'build/src')
+        except UserError:
+            exc_caught = 1
+        assert exc_caught, "Should have caught a UserError."
 
 class FSTestCase(unittest.TestCase):
     def runTest(self):
         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'])
 
 if __name__ == "__main__":
     suite = unittest.TestSuite()
     suite.addTest(FSTestCase())
+    suite.addTest(BuildDirTestCase())
     if not unittest.TextTestRunner().run(suite).wasSuccessful():
         sys.exit(1)

File src/engine/SCons/Node/NodeTests.py

View file
         assert n2.children_are_executed()
         assert n1.children_are_executed()
 
-
-
+    def test_rescan(self):
+        """Test that built nodes are rescanned."""
+        class DummyScanner:
+            pass
+        
+        class TestNode(SCons.Node.Node):
+            def scan(self):
+                for scn in self.scanners:
+                    if not self.scanned.has_key(scn):
+                        n=SCons.Node.Node()
+                        n.scanner_set(scn)
+                        self.add_implicit([ n ], scn)
+                    self.scanned[scn] = 1
+        tn=TestNode()
+        tn.builder_set(Builder())
+        tn.env_set(Environment())
+        ds = DummyScanner()
+        tn.scanner_set(ds)
+        tn.scan()
+        map(lambda x: x.scan(), tn.depends)
+        assert tn.scanned[ds]
+        assert len(tn.implicit[ds]) == 1, tn.implicit
+        tn.build()
+        assert len(tn.implicit[ds]) == 2, tn.implicit
+        for dep in tn.implicit[ds]:
+            assert dep.scanned[ds] == 1
 
 if __name__ == "__main__":
     suite = unittest.makeSuite(NodeTestCase, 'test_')

File src/engine/SCons/Node/__init__.py

View file
                                     target = self, source = self.sources)
 	if stat != 0:
 	    raise BuildError(node = self, stat = stat)
+
+        # If we succesfully build a node, then we need to rescan for
+        # implicit dependencies, since it might have changed on us.
+
+        # XXX Modify this so we only rescan using the scanner(s) relevant
+        # to this build.
+        for scn in self.scanners:
+            try:
+                del self.scanned[scn]
+            except KeyError:
+                pass
+        
+        self.scan()
+
+        for scn in self.scanners:
+            try:
+                for dep in self.implicit[scn]:
+                    w=Walker(dep)
+                    while not w.is_done():
+                        w.next().scan()
+            except KeyError:
+                pass
 	return stat
 
     def builder_set(self, builder):

File src/engine/SCons/Scanner/C.py

View file
 
     fs, cpppath = args
 
-    try:
+    if fs.File(filename, fs.Top).exists():
         file = open(filename)
         contents = file.read()
         file.close()
             source_dir = (fs.Dir(dir, fs.Top),)
         else:
             source_dir = ( fs.Top, )
+
         return (SCons.Util.find_files(angle_includes, cpppath + source_dir,
                                       fs.File)
                 + SCons.Util.find_files(quote_includes, source_dir + cpppath,
                                         fs.File))
-    except (IOError, OSError):
-        return []
+    return []

File src/engine/SCons/Scanner/CTests.py

View file
 import sys
 import os
 import os.path
+import SCons.Node.FS
 
 test = TestCmd.TestCmd(workdir = '')
 
         env = DummyEnvironment([])
         s = SCons.Scanner.C.CScan()
         deps = s.instance(env).scan(test.workpath('f3.cpp'), env)
+        
+        # Make sure exists() gets called on the file node being
+        # scanned, essential for cooperation with BuildDir functionality.
+        assert SCons.Node.FS.default_fs.File(test.workpath('f3.cpp')).created
+        
         headers =  ['f1.h', 'f2.h', 'f3.h', 'fi.h', 'fj.h',
                     'd1/f1.h', 'd1/f2.h', 'd1/f3.h']
         deps_match(self, deps, map(test.workpath, headers))

File src/engine/SCons/Script.py

View file
 import string
 import sys
 import traceback
+import copy
 
 # Strip the script directory from sys.path() so on case-insensitive
 # (WIN32) systems Python doesn't think that the "scons" script is the
 
 
 
-def SConscript(filename):
+def SConscript(sconscript, export={}):
     global scripts
-    scripts.append(SCons.Node.FS.default_fs.File(filename))
+    scripts.append( (SCons.Node.FS.default_fs.File(sconscript), export) )
 
 def Default(*targets):
     for t in targets:
 	print "Use scons -H for help about command-line options."
 	sys.exit(0)
 
+def BuildDir(build_dir, src_dir):
+    SCons.Node.FS.default_fs.BuildDir(build_dir, src_dir)
 
+def Export(**kw):
+    # A convenient shorthand to pass exports to the SConscript function.
+    return kw
 
 #
 # After options are initialized, the following variables are
     def opt_f(opt, arg):
 	global scripts
 	if arg == '-':
-            scripts.append(arg)
+            scripts.append( ( arg, {} ) )
 	else:
-	    scripts.append(SCons.Node.FS.default_fs.File(arg))
+            scripts.append( (SCons.Node.FS.default_fs.File(arg), {}) )
 
     Option(func = opt_f,
 	short = 'f', long = ['file', 'makefile', 'sconstruct'], arg = 'FILE',
     if not scripts:
         for file in ['SConstruct', 'Sconstruct', 'sconstruct']:
             if os.path.isfile(file):
-                scripts.append(SCons.Node.FS.default_fs.File(file))
+                scripts.append( (SCons.Node.FS.default_fs.File(file), {}) )
                 break
 
     if help_option == 'H':
     sys.path = include_dirs + sys.path
 
     while scripts:
-        f, scripts = scripts[0], scripts[1:]
+        f, exports = scripts.pop(0)
+        script_env = copy.copy(globals())
+        script_env.update(exports)
         if f == "-":
-	    exec sys.stdin in globals()
+            exec sys.stdin in script_env
 	else:
-            try:
-                file = open(f.path, "r")
-	    except IOError, s:
+            if f.exists():
+                file = open(str(f), "r")
+                SCons.Node.FS.default_fs.chdir(f.dir)
+                exec file in script_env
+            else:
                 sys.stderr.write("Ignoring missing SConscript '%s'\n" % f.path)
-	    else:
-                SCons.Node.FS.default_fs.chdir(f.dir)
-                exec file in globals()
+
     SCons.Node.FS.default_fs.chdir(SCons.Node.FS.default_fs.Top)
 
     if help_option == 'h':

File test/BuildDir.py

View file
+#!/usr/bin/env python
+#
+# Copyright (c) 2001 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os.path
+import sys
+import time
+import TestSCons
+
+if sys.platform == 'win32':
+    _exe = '.exe'
+else:
+    _exe = ''
+
+test = TestSCons.TestSCons()
+
+foo1 = test.workpath('build/var1/foo1' + _exe)
+foo2 = test.workpath('build/var1/foo2' + _exe)
+foo3 = test.workpath('build/var2/foo1' + _exe)
+foo4 = test.workpath('build/var2/foo2' + _exe)
+
+test.write('SConstruct', """
+BuildDir('build/var1', 'src')
+BuildDir('build/var2', 'src')
+SConscript('build/var1/SConscript')
+SConscript('build/var2/SConscript')
+""")
+
+test.subdir('src')
+test.write('src/SConscript', """
+import os
+import os.path
+
+def buildIt(target, source, env):
+    if not os.path.exists('build'):
+        os.mkdir('build')
+    f1=open(source[0], 'r')
+    f2=open(target, 'w')
+    f2.write(f1.read())
+    f2.close()
+    f1.close()
+    return 0
+
+env = Environment()
+env.Command(target='f2.c', source='f2.in', action=buildIt)
+env.Program(target='foo2', source='f2.c')
+env.Program(target='foo1', source='f1.c')
+""")
+
+test.write('src/f1.c', r"""
+#include "f1.h"
+
+int
+main(int argc, char *argv[])
+{
+	argv[argc++] = "--";
+	printf(F1_STR);
+	exit (0);
+}
+""")
+
+test.write('src/f2.in', r"""
+#include "f2.h"
+
+int
+main(int argc, char *argv[])
+{
+	argv[argc++] = "--";
+	printf(F2_STR);
+	exit (0);
+}
+""")
+
+test.write('src/f1.h', """
+#define F1_STR "f1.c\n"
+""")
+
+test.write('src/f2.h', """
+#define F2_STR "f2.c\n"
+""")
+
+test.run(arguments = '.')
+
+test.run(program = foo1, stdout = "f1.c\n")
+test.run(program = foo2, stdout = "f2.c\n")
+test.run(program = foo3, stdout = "f1.c\n")
+test.run(program = foo4, stdout = "f2.c\n")
+
+test.pass_test()

File test/CPPPATH.py

View file
 env = Environment(CPPPATH = ['include'])
 obj = env.Object(target='prog', source='subdir/prog.c')
 env.Program(target='prog', source=obj)
-SConscript('subdir/SConscript')
+SConscript('subdir/SConscript', Export(env=env))
 """)
 
 test.write(['subdir', 'SConscript'], """

File test/Depends.py

View file
 env.Foo(target = 'f1.out', source = 'f1.in')
 env.Foo(target = 'f2.out', source = 'f2.in')
 env.Bar(target = 'f3.out', source = 'f3.in')
-SConscript('subdir/SConscript')
+SConscript('subdir/SConscript', Export(env=env))
 """ % (python, python))
 
 test.write(['subdir', 'SConscript'], """

File test/Install.py

View file
+#!/usr/bin/env python
+#
+# Copyright (c) 2001 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os.path
+import sys
+import time
+import TestSCons
+
+if sys.platform == 'win32':
+    _exe = '.exe'
+else:
+    _exe = ''
+
+test = TestSCons.TestSCons()
+
+foo1 = test.workpath('export/foo1' + _exe)
+foo2 = test.workpath('export/foo2' + _exe)
+
+test.write('SConstruct', """
+env=Environment()
+t=env.Program(target='foo1', source='f1.c')
+env.Install(dir='export', source=t)
+t=env.Program(target='foo2', source='f2.c')
+env.Install(dir='export', source=t)
+""")
+
+test.write('f1.c', """
+#include <stdio.h>
+
+int main(void)
+{
+   printf("f1.c\n");
+   return 0;
+}
+""")
+
+test.write('f2.c', """
+#include <stdio.h>
+
+int main(void)
+{
+   printf("f2.c\n");
+   return 0;
+}
+""")
+
+test.run(arguments = '.')
+
+test.run(program = foo1, stdout = "f1.c\n")
+test.run(program = foo2, stdout = "f2.c\n")
+
+# make sure the programs didn't get rebuilt, because nothing changed:
+oldtime1 = os.path.getmtime(foo1)
+oldtime2 = os.path.getmtime(foo2)
+
+test.write('f1.c', """
+#include <stdio.h>
+
+int main(void)
+{
+   printf("f1.c again\n");
+   return 0;
+}
+""")
+
+time.sleep(2) # introduce a small delay, to make the test valid
+
+test.run(arguments = '.')
+
+test.fail_test(oldtime1 == os.path.getmtime(foo1))
+test.fail_test(oldtime2 != os.path.getmtime(foo2))
+
+test.pass_test()

File test/errors.py

View file
   File ".*Script.py", line \d+, in main
     _main\(\)
   File ".*Script.py", line \d+, in _main
-    exec file in globals\(\)
+    exec file in script_env
   File "SConstruct3", line \d+, in \?
     raise InternalError, 'error inside'
 InternalError: error inside