Commits

Anonymous committed ac6e59c

Add SConf infrastructure (Autoconf functionality). (Chrisoph Wiedemann)

  • Participants
  • Parent commits 4e0e630

Comments (0)

Files changed (11)

 env2 = env.Copy(CC="cl.exe")
 .EE
 
+.SS Configure contexts
+
+.B scons
+supports
+.I configure contexts,
+an integrated mechanism similar to the
+various AC_CHECK macros in GNU autoconf
+for testing for the existence of C header
+files, libraries, etc.
+In contrast to autoconf,
+.B scons
+does not maintain an explicit cache of the tested values,
+but uses its normal dependency tracking to keep the checked values
+up to date. 
+The following methods can be used to perform checks:
+
+.TP
+.RI Configure( env ", [" custom_tests ", " conf_dir ", " log_file ])
+This creates a configure context, which can be used to perform checks.
+.I env
+specifies the environment for building the tests.
+This environment may be modified when performing checks.
+.I custom_tests
+is a dictionary containing custom tests.
+See also the section about custom tests below. 
+By default, no custom tests are added to the configure context.
+.I conf_dir
+specifies a directory where the test cases are built.
+Note that this directory is not used for building
+normal targets.
+The default value is the directory
+#/.sconf_temp.
+.I log_file
+specifies a file which collects the output from commands
+that are executed to check for the existence of header files, libraries, etc.
+The default is the file #/config.log.
+If you are using the
+.B BuildDir
+method,
+you may want to specify a subdirectory under your build directory.
+
+.EE
+A created
+.B Configure
+instance has the following associated methods:
+
+.TP 
+.RI Configure.Finish( self )
+This method should be called after configuration is done.
+It returns the environment as modified
+by the configuration checks performed.
+After this method is called, no further checks can be performed
+with this configuration context.
+However, you can create a new 
+.RI Configure 
+context to perform additional checks.
+Only one context should be active at a time.
+
+The following Checks are predefined.
+(This list will likely grow larger as time
+goes by and developers contribute new useful tests.)
+
+.TP
+.RI Configure.CheckCHeader( self ", " header )
+Checks if 
+.I header
+is usable in the C-language.
+Returns 1 on success and 0 on failure.
+
+.TP
+.RI Configure.CheckCXXHeader( self ", " header )
+Checks if 
+.I header
+is usable in the C++ language.
+Returns 1 on success and 0 on failure.
+
+.TP 
+.RI Configure.CheckLib( self ", [" library ", " symbol ", " autoadd ])
+Checks if 
+.I library 
+provides 
+.IR symbol .
+If the value of
+.I autoadd
+is 1 and the library provides the specified
+.IR symbol ,
+appends the library to the LIBS construction environment variable.
+.I library 
+may also be None (the default),
+in which case 
+.I symbol 
+is checked with the current LIBS variable.
+The default
+.I symbol
+is "main",
+which just check if
+you can link against the specified
+.IR library .
+The default value for
+.I autoadd
+is 1.
+It is assumed, that the C-language is used.
+This method returns 1 on success and 0 on error.
+
+.TP 
+.RI Configure.CheckLibWithHeader( self ", " library ", " header ", " language ", [" call ", " autoadd ])
+
+In contrast to the 
+.RI Configure.CheckLib 
+call, this call provides a more sophisticated way to check against libraries.
+Again, 
+.I library
+specifies the library to check. 
+.I header
+specifies a header to check for.
+.I language
+may be one of 'C','c','CXX','cxx','C++' and 'c++'.
+.I call
+can be any valid expression (with a trailing ';'). The default is 'main();'.
+.I autoadd
+specifies whether to add the library to the environment (only if the check 
+succeeds). This method returns 1 on success and 0 on error.
+
+.EE
+Example of a typical Configure usage:
+
+.ES
+env = Environment()
+conf = Configure( env )
+if not conf.CheckCHeader( 'math.h' ):
+    print 'We really need math.h!'
+    Exit(1)
+if conf.CheckLibWithHeader( 'qt', 'qapp.h', 'c++', 'QApplication qapp(0,0);' ):
+    # do stuff for qt - usage, e.g.
+    conf.env.Append( CPPFLAGS = '-DWITH_QT' )
+env = conf.Finish() 
+.EE
+
+.EE
+You can define your own custom checks. 
+in addition to the predefined checks.
+These are passed in a dictionary to the Configure function.
+This dictionary maps the names of the checks
+to user defined Python callables 
+(either Python functions or class instances implementing the
+.I __call__
+method).
+The first argument of the call is always a 
+.I CheckContext
+instance followed by the arguments,
+which must be supplied by the user of the check.
+These CheckContext instances define the following methods:
+
+.TP 
+.RI CheckContext.Message( self ", " text )
+
+Usually called before the check is started. 
+.I text
+will be displayed to the user, e.g. 'Checking for library X...'
+
+.TP
+.RI CheckContext.Result( self, ", " res )
+
+Usually called after the check is done. 
+.I res
+can be either an integer or a string. In the former case, 'ok' (res != 0) 
+or 'failed' (res == 0) is displayed to the user, in the latter case the 
+given string is displayed.
+
+.TP
+.RI CheckContext.TryCompile( self ", " text ", " extension )
+Checks if a file with the specified 
+.I extension
+(e.g. '.c') containing 
+.I text 
+can be compiled using the environment's
+.B Object 
+builder. Returns 1 on success and 0 on failure.
+
+.TP 
+.RI CheckContext.TryLink( self ", " text ", " extension )
+Checks, if a file with the specified
+.I extension
+(e.g. '.c') containing 
+.I text 
+can be compiled using the environment's
+.B Program
+builder. Returns 1 on success and 0 on failure.
+
+.TP
+.RI CheckContext.TryRun( self ", " text ", " extension )
+Checks, if a file with the specified
+.I extension
+(e.g. '.c') containing 
+.I text 
+can be compiled using the environment's
+.B Program
+builder. On success, the program is run. If the program
+executes successfully
+(that is, its return status is 0),
+a tuple
+.I (1, outputStr)
+is returned, where
+.I outputStr
+is the standard output of the
+program.
+If the program fails execution
+(its return status is non-zero),
+then (0, '') is returned.
+
+.TP
+.RI CheckContext.TryAction( self ", " action ", [" text ", " extension ])
+Checks if the specified
+.I action 
+with an optional source file (contents
+.I text
+, extension 
+.I extension
+= ''
+) can be executed. 
+.I action 
+may be anything which can be converted to a 
+.B scons
+.RI Action.
+On success,
+.I (1, outputStr)
+is returned, where
+.I outputStr
+is the content of the target file.
+On failure
+.I (0, '')
+is returned.
+
+.TP
+.RI CheckContext.TryBuild( self ", " builder ", [" text ", " extension ])
+Low level implementation for testing specific builds;
+the methods above are based on this metho.
+Given the Builder instance
+.I builder
+and the optional 
+.I text
+of a source file with optional
+.IR extension ,
+this method returns 1 on success and 0 on failure. In addition, 
+.I self.lastTarget 
+is set to the build target node, if the build was successful.
+
+.EE
+Example for implementing and using custom tests:
+
+.ES
+def CheckQt(context, qtdir):
+    context.Message( 'Checking for qt ...' )
+    lastLIBS = context.env['LIBS']
+    lastLIBPATH = context.env['LIBPATH']
+    lastCPPPATH= context.env['CPPPATH']
+    context.env.Append(LIBS = 'qt', LIBPATH = qtdir + '/lib', CPPPATH = qtdir + '/include' )
+    ret = context.TryLink("""
+#include <qapp.h>
+int main(int argc, char **argv) { 
+  QApplication qapp(argc, argv);
+  return 0;
+}
+"""
+    if not ret:
+        context.env.Replace(LIBS = lastLIBS, LIBPATH=lastLIBPATH, CPPPATH=lastCPPPATH)
+    context.Result( ret )
+    return ret
+
+env = Environment()
+conf = Configure( env, custom_tests = 'CheckQt' : CheckQt )
+if not conf.CheckQt('/usr/lib/qt'):
+    print 'We really need qt!'
+    Exit(1)
+env = conf.Finish() 
+.EE
+
 .SS Construction Variable Options
 
 Often when building software, various options need to be specified at build
 
   - Support Import('*') to import everything that's been Export()ed.
 
+  From David Snopek:
+
+  - Contribute the "Autoscons" code for Autoconf-like checking for
+    the existence of libraries, header files and the like.
+
   From Greg Spencer:
 
   - Support the C preprocessor #import statement.
 
+  From Christoph Wiedemann:
+
+  - Integrate David Snopek's "Autoscons" code as the new SConf
+    configuration subsystem, including caching of values between
+    runs (using normal SCons dependency mechanisms), tests, and
+    documentation.
+
 
 
 RELEASE 0.13 - Mon, 31 Mar 2003 20:22:00 -0600

src/engine/MANIFEST.in

 SCons/Scanner/C.py
 SCons/Scanner/Fortran.py
 SCons/Scanner/Prog.py
+SCons/SConf.py
 SCons/Script/SConscript.py
 SCons/Script/__init__.py
 SCons/Sig/__init__.py

src/engine/SCons/Action.py

         else:
             raise SCons.Errors.UserError('Missing SHELL construction variable.')
 
-        if env.has_key('SPAWN'):
-            spawn = env['SPAWN']
+        # for SConf support (by now): check, if we want to pipe the command
+        # output to somewhere else
+        if env.has_key('PIPE_BUILD'):
+            pipe_build = 1
+            if env.has_key('PSPAWN'):
+                pspawn = env['PSPAWN']
+            else:
+                raise SCons.Errors.UserError('Missing PSPAWN construction variable.')
+            if env.has_key('PSTDOUT'):
+                pstdout = env['PSTDOUT']
+            else:
+                raise SCons.Errors.UserError('Missing PSTDOUT construction variable.')
+            if env.has_key('PSTDERR'):
+                pstderr = env['PSTDERR']
+            else:
+                raise SCons.Errors.UserError('Missing PSTDOUT construction variable.')
         else:
-            raise SCons.Errors.UserError('Missing SPAWN construction variable.')
+            pipe_build = 0
+            if env.has_key('SPAWN'):
+                spawn = env['SPAWN']
+            else:
+                raise SCons.Errors.UserError('Missing SPAWN construction variable.')
 
         cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
                                                target, source)
                     # interpreter we are using
                     map(lambda x, e=escape: x.escape(e), cmd_line)
                     cmd_line = map(str, cmd_line)
-                    ret = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
+                    if pipe_build:
+                        ret = pspawn( shell, escape, cmd_line[0], cmd_line,
+                                      ENV, pstdout, pstderr )
+                    else:
+                        ret = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
                     if ret:
                         return ret
         return 0

src/engine/SCons/ActionTests.py

     pass
 
 import os
+import re
 import StringIO
 import sys
 import types
 except:
     pass
 f.close()
+if os.environ.has_key( 'ACTPY_PIPE' ):
+    sys.stdout.write( 'act.py: stdout: executed act.py\\n' )
+    sys.stderr.write( 'act.py: stderr: executed act.py\\n' ) 
 sys.exit(0)
 """)
 
         self.d = {}
         self.d['SHELL'] = scons_env['SHELL']
         self.d['SPAWN'] = scons_env['SPAWN']
+        self.d['PSPAWN'] = scons_env['PSPAWN']
         self.d['ESCAPE'] = scons_env['ESCAPE']
         for k, v in kw.items():
             self.d[k] = v
         return self.d.get(s, s)
     def __getitem__(self, item):
         return self.d[item]
+    def __setitem__(self, item, value):
+        self.d[item] = value
     def has_key(self, item):
         return self.d.has_key(item)
     def get(self, key, value):
         return self.d.items()
     def Dictionary(self):
         return self.d
+    def Copy(self, **kw):
+        res = Environment()
+        res.d = SCons.Environment.our_deepcopy(self.d)
+        for k, v in kw.items():
+            res.d[k] = v        
+        return res
     def sig_dict(self):
         d = {}
         for k,v in self.items(): d[k] = v
         """Test execution of command Actions
 
         """
+        try:
+            env = self.env
+        except AttributeError:
+            env = Environment()
+            
         cmd1 = r'%s %s %s xyzzy' % (python, act_py, outfile)
 
         act = SCons.Action.CommandAction(cmd1)
-        r = act([], [], Environment())
+        r = act([], [], env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'xyzzy'\n", c
         cmd2 = r'%s %s %s $TARGET' % (python, act_py, outfile)
 
         act = SCons.Action.CommandAction(cmd2)
-        r = act('foo', [], Environment())
+        r = act('foo', [], env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'foo'\n", c
         cmd3 = r'%s %s %s ${TARGETS}' % (python, act_py, outfile)
 
         act = SCons.Action.CommandAction(cmd3)
-        r = act(['aaa', 'bbb'], [], Environment())
+        r = act(['aaa', 'bbb'], [], env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'aaa' 'bbb'\n", c
         cmd4 = r'%s %s %s $SOURCES' % (python, act_py, outfile)
 
         act = SCons.Action.CommandAction(cmd4)
-        r = act([], ['one', 'two'], Environment())
+        r = act([], ['one', 'two'], env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'one' 'two'\n", c
         act = SCons.Action.CommandAction(cmd4)
         r = act([],
                         source = ['three', 'four', 'five'],
-                        env = Environment())
+                        env = env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'three' 'four'\n", c
         act = SCons.Action.CommandAction(cmd5)
         r = act(target = 'out5',
                         source = [],
-                        env = Environment(ENV = {'XYZZY' : 'xyzzy'}))
+                        env = env.Copy(ENV = {'XYZZY' : 'xyzzy'}))
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: 'out5' 'XYZZY'\nact.py: 'xyzzy'\n", c
         act = SCons.Action.CommandAction(cmd6)
         r = act(target = [Obj('111'), Obj('222')],
                         source = [Obj('333'), Obj('444'), Obj('555')],
-                        env = Environment())
+                        env = env.Copy())
         assert r == 0
         c = test.read(outfile, 'r')
         assert c == "act.py: '222' '111' '333' '444'\n", c
 
         act = SCons.Action.CommandAction(cmd7)
 
-        global show_string
+        global show_string 
         show_string = ""
         def my_show(string):
             global show_string
             show_string = show_string + string + "\n"
         act.show = my_show
 
-        r = act([], [], Environment())
+        r = act([], [], env.Copy())
         assert r == 0
         assert show_string == expect7, show_string
 
 
         # Test that a nonexistent command returns 127
         act = SCons.Action.CommandAction(python + "_XyZzY_")
-        r = act([], [], Environment(out = outfile))
+        r = act([], [], env.Copy(out = outfile))
         assert r == expect_nonexistent, "r == %d" % r
 
         # Test that trying to execute a directory returns 126
         dir, tail = os.path.split(python)
         act = SCons.Action.CommandAction(dir)
-        r = act([], [], Environment(out = outfile))
+        r = act([], [], env.Copy(out = outfile))
         assert r == expect_nonexecutable, "r == %d" % r
 
         # Test that trying to execute a non-executable file returns 126
         act = SCons.Action.CommandAction(outfile)
-        r = act([], [], Environment(out = outfile))
+        r = act([], [], env.Copy(out = outfile))
         assert r == expect_nonexecutable, "r == %d" % r
 
+
+    def test_pipe_execute(self):
+        """Test capturing piped output from an action
+        """
+        pipe_file = open( test.workpath('pipe.out'), "w" )
+        self.env = Environment(ENV = {'ACTPY_PIPE' : '1'}, PIPE_BUILD = 1,
+                               PSTDOUT = pipe_file, PSTDERR = pipe_file)
+        # everything should also work when piping output
+        self.test_execute()
+        self.env['PSTDOUT'].close()
+        pipe_out = test.read( test.workpath('pipe.out') )
+        if sys.platform == 'win32':
+            cr = '\r'
+        else:
+            cr = ''
+        found = re.findall( "act.py: stdout: executed act.py%s\nact.py: stderr: executed act.py%s\n" % (cr, cr), pipe_out )
+        assert len(found) == 8, found
+
     def test_set_handler(self):
         """Test setting the command handler...
         """

src/engine/SCons/Platform/posix.py

 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import os
+import os.path
+import popen2
+import string
+import sys
+
 import SCons.Util
-import string
-import os
-import sys
-import os.path
 
 def escape(arg):
     "escape shell special characters"
 
     return '"' + arg + '"'
 
-def env_spawn(sh, escape, cmd, args, env):
+def _get_env_command(sh, escape, cmd, args, env):
     if env:
         s = 'env -i '
         for key in env.keys():
         s = s + escape(string.join(args))
     else:
         s = string.join(args)
+    return s
 
+def env_spawn(sh, escape, cmd, args, env):
+    s = _get_env_command( sh, escape, cmd, args, env)
     stat = os.system(s)
     if stat & 0xff:
         return stat | 0x80
         if stat & 0xff:
             return stat | 0x80
         return stat >> 8
-            
+
+def piped_env_spawn(sh, escape, cmd, args, env, stdout, stderr):
+    # spawn using Popen3 combined with the env command
+    # the command name and the command's stdout is written to stdout
+    # the command's stderr is written to stderr
+    s = _get_env_command( sh, escape, cmd, args, env)
+    # write the command line out
+    if stdout != None:
+        stdout.write(string.join(args) + '\n')
+    proc = popen2.Popen3(s, 1)
+    # process stdout
+    if stdout != None:
+        #for line in proc.fromchild.xreadlines():
+        #    stdout.write(line)
+        while 1:
+            line = proc.fromchild.readline()
+            if not line:
+                break
+            stdout.write(line)
+    # process stderr
+    if stderr != None:
+        #for line in proc.childerr.xreadlines():
+        #    stderr.write(line)
+        while 1:
+            line = proc.childerr.readline()
+            if not line:
+                break
+            stderr.write(line)
+    stat = proc.wait()
+    if stat & 0xff:
+        return stat | 0x80
+    return stat >> 8
+    
+def piped_fork_spawn(sh, escape, cmd, args, env, stdout, stderr):
+    # spawn using fork / exec and providing a pipe for the command's
+    # stdout / stderr stream
+    if stdout != stderr:
+        (rFdOut, wFdOut) = os.pipe()
+        (rFdErr, wFdErr) = os.pipe()
+    else:
+        (rFdOut, wFdOut) = os.pipe()
+        rFdErr = rFdOut
+        wFdErr = wFdOut
+    # write the command line out
+    if stdout != None:
+        stdout.write(string.join(args) + '\n')
+    # do the fork
+    pid = os.fork()
+    if not pid:
+        # Child process
+        os.close( rFdOut )
+        if rFdOut != rFdErr:
+            os.close( rFdErr )
+        os.dup2( wFdOut, 1 ) # is there some symbolic way to do that ?
+        os.dup2( wFdErr, 2 )
+        os.close( wFdOut )
+        if stdout != stderr:
+            os.close( wFdErr )
+        exitval = 127
+        args = [sh, '-c', string.join(args)]
+        try:
+            os.execvpe(sh, args, env)
+        except OSError, e:
+            exitval = exitvalmap[e[0]]
+            stderr.write("scons: %s: %s\n" % (cmd, e[1]))
+        os._exit(exitval)
+    else:
+        # Parent process
+        pid, stat = os.waitpid(pid, 0)
+        os.close( wFdOut )
+        if stdout != stderr:
+            os.close( wFdErr )        
+        childOut = os.fdopen( rFdOut )
+        if stdout != stderr:
+            childErr = os.fdopen( rFdErr )
+        else:
+            childErr = childOut
+        # process stdout
+        if stdout != None:
+            #for line in childOut.xreadlines():
+            #    stdout.write(line)
+            while 1:
+                line = childOut.readline()
+                if not line:
+                    break
+                stdout.write(line)
+        # process stderr
+        if stderr != None:
+            #for line in childErr.xreadlines():
+            #    stderr.write(line)
+            while 1:
+                line = childErr.readline()
+                if not line:
+                    break
+                stdout.write(line)
+        os.close( rFdOut )
+        if stdout != stderr:
+            os.close( rFdErr )
+        if stat & 0xff:
+            return stat | 0x80
+        return stat >> 8
+
+           
+    
 def generate(env):
 
     # If the env command exists, then we can use os.system()
     # threads (i.e. -j) and is more efficient than forking Python.
     if env.Detect('env'):
         spawn = env_spawn
+        pspawn = piped_env_spawn
     else:
         spawn = fork_spawn
+        pspawn = piped_fork_spawn
 
     if not env.has_key('ENV'):
         env['ENV']        = {}
     env['SHLIBSUFFIX']    = '.so'
     env['LIBPREFIXES']    = '$LIBPREFIX'
     env['LIBSUFFIXES']    = [ '$LIBSUFFIX', '$SHLIBSUFFIX' ]
+    env['PSPAWN']         = pspawn
     env['SPAWN']          = spawn
     env['SHELL']          = 'sh'
     env['ESCAPE']         = escape

src/engine/SCons/Platform/win32.py

 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-import SCons.Util
 import os
 import os.path
+import popen2
 import string
 import sys
+import tempfile
+
+import SCons.Util
 
 class TempFileMunge:
     """A callable class.  You can set an Environment variable to this,
            (reduce(lambda x, y: x + len(y), cmd, 0) + len(cmd)) <= 2048:
             return self.cmd
         else:
-            import tempfile
-
             # In Cygwin, we want to use rm to delete the temporary file,
             # because del does not exist in the sh shell.
             rm = env.Detect('rm') or 'del'
 # you had better have cmd or command.com in your PATH when you run
 # scons.
 
+def piped_spawn(sh, escape, cmd, args, env, stdout, stderr):
+    if not sh:
+        sys.stderr.write("scons: Could not find command interpreter, is it in your PATH?\n")
+        return 127
+    else:
+        # NOTE: This is just a big, big hack.  What we do is simply pipe the
+        # output to a temporary file and then write it to the streams.
+        # I DO NOT know the effect of adding these to a command line that
+        # already has indirection symbols.
+        tmpFile = os.path.normpath(tempfile.mktemp())
+        args.append(">" + str(tmpFile))
+        args.append("2>&1")
+        if stdout != None:
+            # ToDo: use the printaction instead of that
+            stdout.write(string.join(args) + "\n")
+        try:
+            try:
+                args = [sh, '/C', escape(string.join(args)) ]
+                ret = os.spawnve(os.P_WAIT, sh, args, env)
+            except OSError, e:
+                ret = exitvalmap[e[0]]
+                stderr.write("scons: %s: %s\n" % (cmd, e[1]))
+            try:
+                input = open( tmpFile, "r" )
+                while 1:
+                    line = input.readline()
+                    if not line:
+                        break
+                    if stdout != None:
+                        stdout.write(line)
+                    if stderr != None and stderr != stdout:
+                        stderr.write(line)
+            finally:
+                input.close()
+        finally:
+            try:
+                os.remove( tmpFile )
+            except OSError:
+                # What went wrong here ??
+                pass
+        return ret
+        
 def spawn(sh, escape, cmd, args, env):
     if not sh:
         sys.stderr.write("scons: Could not find command interpreter, is it in your PATH?\n")
     env['SHLIBSUFFIX']    = '.dll'
     env['LIBPREFIXES']    = [ '$LIBPREFIX', '$SHLIBPREFIX' ]
     env['LIBSUFFIXES']    = [ '$LIBSUFFIX', '$SHLIBSUFFIX' ]
+    env['PSPAWN']         = piped_spawn
     env['SPAWN']          = spawn
     env['SHELL']          = cmd_interp
     env['TEMPFILE']       = TempFileMunge

src/engine/SCons/SConf.py

+"""SCons.SConf
+
+Autoconf-like configuration support.
+"""
+
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import cPickle
+import os
+import shutil
+import sys
+from types import *
+
+import SCons.Action
+import SCons.Builder
+import SCons.Errors
+import SCons.Node.FS
+import SCons.Taskmaster
+import SCons.Util
+import SCons.Warnings
+
+_ac_build_counter = 0
+_ac_config_counter = 0
+_activeSConfObjects = {}
+
+class SConfWarning(SCons.Warnings.Warning):
+    pass
+SCons.Warnings.enableWarningClass( SConfWarning )
+
+
+def _createSource( target, source, env ):
+    fd = open(str(target[0]), "w")
+    fd.write(env['SCONF_TEXT'])
+    fd.close()
+
+
+class SConf:
+    """This is simply a class to represent a configure context. After
+    creating a SConf object, you can call any tests. After finished with your
+    tests, be sure to call the Finish() method, which returns the modified
+    environment.
+    Some words about caching: In most cases, it is not necessary to cache
+    Test results explicitely. Instead, we use the scons dependency checking
+    mechanism. For example, if one wants to compile a test program
+    (SConf.TryLink), the compiler is only called, if the program dependencies
+    have changed. However, if the program could not be compiled in a former
+    SConf run, we need to explicitely cache this error.
+    """
+
+    def __init__(self, env, custom_tests = {}, conf_dir='#/.sconf_temp',
+                 log_file='#config.log'):
+        """Constructor. Pass additional tests in the custom_tests-dictinary,
+        e.g. custom_tests={'CheckPrivate':MyPrivateTest}, where MyPrivateTest
+        defines a custom test.
+        Note also the conf_dir and log_file arguments (you may want to
+        build tests in the BuildDir, not in the SourceDir)
+        """
+        if len(_activeSConfObjects.keys()) > 0:
+            raise (SCons.Errors.UserError,
+                   "Only one SConf object may be active at one time")
+        self.env = env
+        if log_file != None:
+            self.logfile = SCons.Node.FS.default_fs.File(log_file)
+        else:
+            self.logfile = None
+        self.logstream = None
+        self.lastTarget = None
+
+        # add default tests
+        default_tests = {
+                 'CheckCHeader'       : CheckCHeader,
+                 'CheckCXXHeader'     : CheckCXXHeader,
+                 'CheckLib'           : CheckLib,
+                 'CheckLibWithHeader' : CheckLibWithHeader
+               }
+        self.AddTests(default_tests)
+        self.AddTests(custom_tests)
+        self.confdir = SCons.Node.FS.default_fs.Dir(conf_dir)
+        self.cache = {}
+        self._startup()
+
+    def Finish(self):
+        """Call this method after finished with your tests:
+        env = sconf.Finish()"""
+        global _lastSConfObj
+        _lastSConfObj = None
+        self._shutdown()
+        return self.env
+
+    def setCache(self, nodes, already_done = []):
+        # Set up actions used for caching errors
+        # Caching positive tests should not be necessary, cause
+        # the build system knows, if test objects/programs/outputs
+        # are up to date.
+        for n in nodes:
+            # The 'n in already_done' expression is not really efficient.
+            # We may do something more sophisticated in the future :-),
+            # but there should not be that many dependencies in configure
+            # tests
+            if (n.has_builder() and
+                not n in already_done):
+                    n.add_pre_action(SCons.Action.Action(self._preCache))
+                    n.add_post_action(SCons.Action.Action(self._postCache))
+                    already_done.append( n )
+            self.setCache(n.children())
+
+    def BuildNodes(self, nodes):
+        """
+        Tries to build the given nodes immediately. Returns 1 on success,
+        0 on error.
+        """
+
+        import SCons.Script    # really ugly, but we need BuildTask :-(
+        # Is it better to provide a seperate Task for SConf builds ?
+        class SConfBuildTask(SCons.Script.BuildTask):
+            """Errors in SConf builds are not fatal, so we override
+            the do_failed method"""
+            def do_failed(self, status=2):
+                pass
+
+        if self.logstream != None:
+            # override stdout / stderr to write in log file
+            oldStdout = sys.stdout
+            sys.stdout = self.logstream
+            oldStderr = sys.stderr
+            sys.stderr = self.logstream
+
+        self.setCache( nodes )
+        ret = 1
+
+        try:
+            oldPwd = SCons.Node.FS.default_fs.getcwd()
+            SCons.Node.FS.default_fs.chdir(SCons.Node.FS.default_fs.Top)
+            # ToDo: use user options for calc
+            calc = SCons.Sig.Calculator(max_drift=0)
+            tm = SCons.Taskmaster.Taskmaster( nodes,
+                                              SConfBuildTask,
+                                              calc )
+            # we don't want to build tests in parallel
+            jobs = SCons.Job.Jobs(1, tm )
+            try:
+                jobs.run()
+            except:
+                pass
+
+
+            for n in nodes:
+                state = n.get_state()
+                if (state != SCons.Node.executed and
+                    state != SCons.Node.up_to_date):
+                    # the node could not be built. we return 0 in this case
+                    ret = 0
+            SCons.Node.FS.default_fs.chdir(oldPwd)
+        finally:
+            if self.logstream != None:
+                # restore stdout / stderr
+                sys.stdout = oldStdout
+                sys.stderr = oldStderr
+        return ret
+
+
+    def TryBuild(self, builder, text = None, extension = ""):
+        """Low level TryBuild implementation. Normally you don't need to
+        call that - you can use TryCompile / TryLink / TryRun instead
+        """
+        global _ac_build_counter
+
+        nodesToBeBuilt = []
+
+        #target = self.confdir.File("conftest_" + str(_ac_build_counter))
+        f = "conftest_" + str(_ac_build_counter)
+        target = os.path.join(str(self.confdir), f)
+        if text != None:
+            source = self.confdir.File(f + extension)
+            sourceNode = self.env.SConfSourceBuilder(target=source,
+                                                     source=None)
+            nodesToBeBuilt.append(sourceNode)
+        else:
+            source = None
+        self.env['SCONF_TEXT'] = text
+
+        node = builder(target = target, source = source)
+        nodesToBeBuilt.append(node)
+        ret = self.BuildNodes(nodesToBeBuilt)
+
+        del self.env['SCONF_TEXT']
+
+        _ac_build_counter = _ac_build_counter + 1
+        if ret:
+            self.lastTarget = node
+        else:
+            self.lastTarget = None
+
+        return ret
+
+    def TryAction(self, action, text = None, extension = ""):
+        """Tries to execute the given action with optional source file
+        contents <text> and optional source file extension <extension>,
+        Returns the status (0 : failed, 1 : ok) and the contents of the
+        output file.
+        """
+        builder = SCons.Builder.Builder(action=action)
+        self.env.Append( BUILDERS = {'SConfActionBuilder' : builder} )
+        ok = self.TryBuild(self.env.SConfActionBuilder, text, extension)
+        del self.env['BUILDERS']['SConfActionBuilder']
+        if ok:
+            outputStr = self.lastTarget.get_contents()
+            return (1, outputStr)
+        return (0, "")
+
+    def TryCompile( self, text, extension):
+        """Compiles the program given in text to an env.Object, using extension
+        as file extension (e.g. '.c'). Returns 1, if compilation was
+        successful, 0 otherwise. The target is saved in self.lastTarget (for
+        further processing).
+        """
+        return self.TryBuild(self.env.Object, text, extension)
+
+    def TryLink( self, text, extension ):
+        """Compiles the program given in text to an executable env.Program,
+        using extension as file extension (e.g. '.c'). Returns 1, if
+        compilation was successful, 0 otherwise. The target is saved in
+        self.lastTarget (for further processing).
+        """
+        #ok = self.TryCompile( text, extension)
+        #if( ok ):
+        return self.TryBuild(self.env.Program, text, extension )
+        #else:
+        #    return 0
+
+    def TryRun(self, text, extension ):
+        """Compiles and runs the program given in text, using extension
+        as file extension (e.g. '.c'). Returns (1, outputStr) on success,
+        (0, '') otherwise. The target (a file containing the program's stdout)
+        is saved in self.lastTarget (for further processing).
+        """
+        ok = self.TryLink(text, extension)
+        if( ok ):
+            prog = self.lastTarget
+            output = SCons.Node.FS.default_fs.File(str(prog)+'.out')
+            node = self.no_pipe_env.Command(output, prog, "%s >%s" % (str(prog),
+                                                              str(output)))
+            ok = self.BuildNodes([node])
+            if ok:
+                outputStr = output.get_contents()
+                return( 1, outputStr)
+        return (0, "")
+
+    class TestWrapper:
+        """A wrapper around Tests (to ensure sanity)"""
+        def __init__(self, test, sconf):
+            self.test = test
+            self.sconf = sconf
+        def __call__(self, *args, **kw):
+            if not self.sconf.active:
+                raise (SCons.Errors.UserError,
+                       "Test called after sconf.Finish()")
+            context = CheckContext(self.sconf)
+            ret = apply(self.test, (context,) +  args, kw)
+            context.Result("error: no result")
+            return ret
+
+    def AddTest(self, test_name, test_instance):
+        """Adds test_class to this SConf instance. It can be called with
+        self.test_name(...)"""
+        setattr(self, test_name, SConf.TestWrapper(test_instance, self))
+
+    def AddTests(self, tests):
+        """Adds all the tests given in the tests dictionary to this SConf
+        instance
+        """
+        for name in tests.keys():
+            self.AddTest(name, tests[name])
+
+    def _preCache(self, target, source, env):
+        # Action before target is actually built
+        #
+        # We record errors in the cache. Only non-exisiting targets may
+        # have recorded errors
+        needs_rebuild = target[0].exists()
+        buildSig = target[0].builder.get_contents(target, source, env)
+        for node in source:
+            if node.get_state() != SCons.Node.up_to_date:
+                # if any of the sources has changed, we cannot use our cache
+                needs_rebuild = 1
+        if not self.cache.has_key( str(target[0]) ):
+            # We have no recorded error, so we try to build the target
+            needs_rebuild = 1
+        else:
+            lastBuildSig = self.cache[str(target[0])]['builder']
+            if lastBuildSig != buildSig:
+                needs_rebuild = 1
+        if not needs_rebuild:
+            # When we are here, we can savely pass the recorded error
+            print ('(cached): Building "%s" failed in a previous run.' %
+                   str(target[0]))
+            return 1
+        else:
+            # Otherwise, we try to record an error
+            self.cache[str(target[0])] = {
+               'builder' :  buildSig
+            }
+
+    def _postCache(self, target, source, env):
+        # Action after target is successfully built
+        #
+        # No error during build -> remove the recorded error
+        del self.cache[str(target[0])]
+
+    def _loadCache(self):
+        # try to load build-error cache
+        try:
+            cacheDesc = cPickle.load(open(str(self.confdir.File(".cache"))))
+            if cacheDesc['scons_version'] != SCons.__version__:
+                raise Exception, "version mismatch"
+            self.cache = cacheDesc['data']
+        except:
+            self.cache = {}
+            #SCons.Warnings.warn( SConfWarning,
+            #                     "Couldn't load SConf cache (assuming empty)" )
+
+    def _dumpCache(self):
+        # try to dump build-error cache
+        try:
+            cacheDesc = {'scons_version' : SCons.__version__,
+                         'data'          : self.cache }
+            cPickle.dump(cacheDesc, open(str(self.confdir.File(".cache")),"w"))
+        except:
+            SCons.Warnings.warn( SConfWarning,
+                                 "Couldn't dump SConf cache" )
+
+    def createDir(self, node):
+        if not node.up().exists():
+            self.createDir( node.up() )
+        if not node.exists():
+            SCons.Node.FS.Mkdir(node, None, self.env)
+            node._exists = 1
+
+    def _startup(self):
+        """Private method. Set up logstream, and set the environment
+        variables necessary for a piped build
+        """
+        global _ac_config_counter
+        global _activeSConfObjects
+
+        #def createDir( node, self = self ):
+        #    if not node.up().exists():
+        #        createDir( node.up() )
+        #    if not node.exists():
+        #        SCons.Node.FS.Mkdir(node, None, self.env)
+        #        node._exists = 1
+        self.createDir(self.confdir)
+        # we don't want scons to build targets confdir automatically
+        # cause we are doing it 'by hand'
+        self.confdir.up().add_ignore( [self.confdir] )
+        self.confdir.set_state( SCons.Node.up_to_date )
+
+        self.no_pipe_env = self.env.Copy()
+
+        # piped spawn will print its own actions (CHANGE THIS!)
+        SCons.Action.print_actions = 0
+        if self.logfile != None:
+            # truncate logfile, if SConf.Configure is called for the first time
+            # in a build
+            if _ac_config_counter == 0:
+                log_mode = "w"
+            else:
+                log_mode = "a"
+            self.logstream = open(str(self.logfile), log_mode)
+            # logfile may stay in a build directory, so we tell
+            # the build system not to override it with a eventually
+            # existing file with the same name in the source directory
+            self.logfile.dir.add_ignore( [self.logfile] )
+            self.env['PIPE_BUILD'] = 1
+            self.env['PSTDOUT'] = self.logstream
+            self.env['PSTDERR'] = self.logstream
+        else:
+            self.logstream = None
+        # we use a special builder to create source files from TEXT
+        action = SCons.Action.Action(_createSource,varlist=['SCONF_TEXT'])
+        sconfSrcBld = SCons.Builder.Builder(action=action)
+        self.env.Append( BUILDERS={'SConfSourceBuilder':sconfSrcBld} )
+        self.active = 1
+        # only one SConf instance should be active at a time ...
+        _activeSConfObjects[self] = None
+        _ac_config_counter = _ac_config_counter + 1
+        self._loadCache()
+
+    def _shutdown(self):
+        """Private method. Reset to non-piped spawn"""
+        global _activeSConfObjets
+
+        if not self.active:
+            raise SCons.Errors.UserError, "Finish may be called only once!"
+        # Piped Spawn print its own actions. CHANGE THIS!
+        SCons.Action.print_actions = 1
+        if self.logstream != None:
+            self.logstream.close()
+            self.logstream = None
+            # clean up environment
+            del self.env['PIPE_BUILD']
+            del self.env['PSTDOUT']
+            del self.env['PSTDERR']
+        # remove the SConfSourceBuilder from the environment
+        blds = self.env['BUILDERS']
+        del blds['SConfSourceBuilder']
+        self.env.Replace( BUILDERS=blds )
+        self.active = 0
+        del _activeSConfObjects[self]
+        self._dumpCache()
+
+
+class CheckContext:
+    """Provides a context for configure tests. Defines how a test writes to the
+    screen and log file.
+
+    A typical test is just a callable with an instance of CheckContext as
+    first argument:
+
+    def CheckCustom(context, ...)
+    context.Message('Checking my weird test ... ')
+    ret = myWeirdTestFunction(...)
+    context.Result(ret)
+
+    Often, myWeirdTestFunction will be one of
+    context.TryCompile/context.TryLink/context.TryRun. The results of
+    those are cached, for they are only rebuild, if the dependencies have
+    changed.
+    """
+
+    def __init__(self, sconf):
+        """Constructor. Pass the corresponding SConf instance."""
+        self.sconf = sconf
+        self.cached = 0
+        self.show_result = 0
+
+    def Message(self, text):
+        """Inform about what we are doing right now, e.g.
+        'Checking for SOMETHING ... '
+        """
+        # write to config.log
+        if self.sconf.logstream != None:
+            self.sconf.logstream.write(text + '\n')
+        sys.stdout.write(text)
+        self.show_result = 0
+
+    def Result(self, res ):
+        """Inform about the result of the test. res may be an integer or a
+        string. In case of an integer, the written text will be 'ok' or
+        'failed'.
+        """
+        if( type(res) == IntType ):
+            if res:
+                text = "ok"
+            else:
+                text = "failed"
+        elif( type(res) == StringType ):
+            text = res
+        else:
+            raise TypeError, "Expected string or int"
+        if( self.cached ):
+            text = text + " (cached)"
+        if self.show_result == 0:
+            if self.sconf.logstream != None:
+                self.sconf.logstream.write("Result: " + text + "\n\n")
+            sys.stdout.write(text + "\n")
+            self.show_result = 1
+
+
+    def TryBuild(self, *args, **kw):
+        return apply(self.sconf.TryBuild, args, kw)
+
+    def TryAction(self, *args, **kw):
+        return apply(self.sconf.TryAction, args, kw)
+
+    def TryCompile(self, *args, **kw):
+        return apply(self.sconf.TryCompile, args, kw)
+
+    def TryLink(self, *args, **kw):
+        return apply(self.sconf.TryLink, args, kw)
+
+    def TryRun(self, *args, **kw):
+        return apply(self.sconf.TryRun, args, kw)
+
+    def __getattr__( self, attr ):
+        if( attr == 'env' ):
+            return self.sconf.env
+        else:
+            raise AttributeError, "CheckContext instance has no attribute '%s'" % attr
+
+def CheckCHeader(test, header):
+    """
+    A test for a c header file.
+    """
+    # ToDo: Support also system header files (i.e. #include <header.h>)
+    test.Message("Checking for C header %s... " % header)
+    ret = test.TryCompile("#include \"%s\"\n\n" % header, ".c")
+    test.Result( ret )
+    return ret
+
+
+def CheckCXXHeader(test, header):
+    """
+    A test for a c++ header file.
+    """
+    # ToDo: Support also system header files (i.e. #include <header.h>)
+    test.Message("Checking for C header %s... " % header)
+    ret = test.TryCompile("#include \"%s\"\n\n" % header, ".cpp")
+    test.Result( ret )
+    return ret
+
+def CheckLib(test, library=None, symbol="main", autoadd=1):
+    """
+    A test for a library. See also CheckLibWithHeader.
+    Note that library may also be None to test whether the given symbol
+    compiles without flags.
+    """
+    # ToDo: accept path for the library
+    test.Message("Checking for %s in library %s... " % (symbol, library))
+    oldLIBS = test.env.get( 'LIBS', [] )
+
+    # NOTE: we allow this at in the case that we don't know what the
+    # library is called like when we get --libs from a configure script
+    if library != None:
+        test.env.Append(LIBS = [ library ])
+
+    text = ""
+    if symbol != "main":
+        text = text + """
+#ifdef __cplusplus
+extern "C"
+#endif
+char %s();""" % symbol
+    text = text + """
+int
+main() {
+%s();
+return 0;
+}
+\n\n""" % symbol
+
+    ret = test.TryLink( text, ".c" )
+    if not autoadd or not ret:
+        test.env.Replace(LIBS=oldLIBS)
+
+    test.Result(ret)
+    return ret
+
+def CheckLibWithHeader(test, library, header, language, call="main();", autoadd=1):
+    # ToDo: accept path for library. Support system header files.
+    """
+    Another (more sophisticated) test for a library.
+    Checks, if library and header is available for language (maybe 'C'
+    or 'CXX'). Call maybe be a valid expression _with_ a trailing ';'.
+    As in CheckLib, we support library=None, to test if the call compiles
+    without extra link flags.
+    """
+    test.Message("Checking for %s in library %s (header %s) ... " %
+                 (call, library, header))
+    oldLIBS= test.env.get( 'LIBS', [] )
+
+    # NOTE: we allow this at in the case that we don't know what the
+    # library is called like when we get --libs from a configure script
+    if library != None:
+        test.env.Append(LIBS = [ library ])
+
+    text  = """\
+#include "%s"
+int main() {
+  %s
+}
+""" % (header, call)
+
+    if language in ["C", "c"]:
+        extension=".c"
+    elif language in ["CXX", "cxx", "C++", "c++"]:
+        extension=".cpp"
+    else:
+        raise SCons.Errors.UserError, "Unknown language!"
+
+    ret = test.TryLink( text, extension)
+    if not autoadd or not ret:
+        test.env.Replace( LIBS = oldLIBS )
+
+    test.Result(ret)
+    return ret

src/engine/SCons/SConfTests.py

+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import re
+import StringIO
+import sys
+import unittest
+
+import TestCmd
+
+import SCons.Environment
+import SCons.SConf
+
+scons_env = SCons.Environment.Environment()
+sys.stdout = StringIO.StringIO()
+
+if sys.platform == 'win32':
+    existing_lib = "msvcrt"
+else:
+    existing_lib = "m"
+
+def clearFileCache(dir):
+    # mostly from FS.Dir.__clearRepositoryCache, but set also state
+    # of nodes to None
+    for node in dir.entries.values():
+        if node != dir.dir:
+            if node != dir and isinstance(node, SCons.Node.FS.Dir):
+                clearFileCache(node)
+            else:
+                node._srcreps = None
+                del node._srcreps
+                node._rfile = None
+                del node._rfile
+                node._rexists = None
+                del node._rexists
+                node._exists = None
+                del node._exists
+                node._srcnode = None
+                del node._srcnode
+                node.set_state(None)
+
+class SConfTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.test = TestCmd.TestCmd(workdir = '')
+
+    def tearDown(self):
+        self.test.cleanup()
+
+    def _resetSConfState(self):
+                        
+        clearFileCache( SCons.Node.FS.default_fs.Dir(self.test.workpath()) )
+        SCons.SConf._ac_config_counter = 0
+        SCons.SConf._ac_build_counter = 0
+            
+        
+    def _baseTryXXX(self, TryFunc):
+        def checks(sconf, TryFunc):
+            res1 = TryFunc( sconf, "int main() { return 0; }", ".c" )
+            res2 = TryFunc( sconf, "not a c program", ".c" )
+            return (res1,res2)
+        
+        self._resetSConfState()
+        sconf = SCons.SConf.SConf(scons_env,
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            res = checks( sconf, TryFunc )
+            assert res[0] and not res[1] 
+        finally:
+            sconf.Finish()
+
+        # test the caching mechanism
+        self._resetSConfState()
+
+        sconf = SCons.SConf.SConf(scons_env,
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            res = checks( sconf, TryFunc )
+            assert res[0] and not res[1] 
+        finally:
+            sconf.Finish()
+
+        # we should have exactly one one error cached 
+        log = self.test.read( self.test.workpath('config.log') )
+        expr = re.compile( ".*(\(cached\))", re.DOTALL ) 
+        firstOcc = expr.match( log )
+        assert firstOcc != None 
+        secondOcc = expr.match( log, firstOcc.end(0) )
+        assert secondOcc == None 
+
+    def test_TryCompile(self):
+        self._baseTryXXX( SCons.SConf.SConf.TryCompile )
+        
+    def test_TryLink(self):
+        self._baseTryXXX( SCons.SConf.SConf.TryLink )
+
+    def test_TryRun(self):
+        def checks(sconf):
+            prog = """
+#include <stdio.h>
+int main() {
+  printf( "Hello" );
+  return 0;
+}
+"""
+            res1 = sconf.TryRun( prog, ".c" ) 
+            res2 = sconf.TryRun( "not a c program", ".c" )
+            return (res1, res2)
+        
+        self._resetSConfState()
+        sconf = SCons.SConf.SConf(scons_env,
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            res = checks(sconf)
+            assert res[0][0] and res[0][1] == "Hello" 
+            assert not res[1][0] and res[1][1] == ""
+        finally:
+            sconf.Finish()
+
+        # test the caching mechanism
+        self._resetSConfState()
+
+        sconf = SCons.SConf.SConf(scons_env,
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            res = checks(sconf)
+            assert res[0][0] and res[0][1] == "Hello" 
+            assert not res[1][0] and res[1][1] == ""
+        finally:
+            sconf.Finish()
+
+        # we should have exactly one one error cached 
+        log = self.test.read( self.test.workpath('config.log') )
+        expr = re.compile( ".*(\(cached\))", re.DOTALL )
+        firstOcc = expr.match( log )
+        assert firstOcc != None 
+        secondOcc = expr.match( log, firstOcc.end(0) )
+        assert secondOcc == None 
+
+
+    def test_TryAction(self):
+        def actionOK(target, source, env):
+            open(str(target[0]), "w").write( "RUN OK" )
+            return None
+        def actionFAIL(target, source, env):
+            return 1
+        self._resetSConfState()
+        sconf = SCons.SConf.SConf(scons_env,
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            (ret, output) = sconf.TryAction(action=actionOK)
+            assert ret and output == "RUN OK"
+            (ret, output) = sconf.TryAction(action=actionFAIL)
+            assert not ret and output == ""
+        finally:
+            sconf.Finish()
+            
+        
+
+    def test_StandardTests(self):
+        def CHeaderChecks( sconf ):
+            res1 = sconf.CheckCHeader( "stdio.h" )
+            res2 = sconf.CheckCHeader( "HopefullyNotCHeader.noh" )
+            return (res1,res2)
+
+        def CXXHeaderChecks(sconf):
+            res1 = sconf.CheckCXXHeader( "vector" )
+            res2 = sconf.CheckCXXHeader( "HopefullyNotCXXHeader.noh" )
+            return (res1,res2)
+
+        def LibChecks(sconf):
+            res1 = sconf.CheckLib( existing_lib, "main", autoadd=0 )
+            res2 = sconf.CheckLib( "hopefullynolib", "main", autoadd=0 )
+            return (res1, res2)
+        
+        def LibChecksAutoAdd(sconf):
+            def libs(env):
+                if env.has_key( "LIBS" ):
+                    return env['LIBS']
+                else:
+                    return []
+            env = sconf.env.Copy()
+            res1 = sconf.CheckLib( existing_lib, "main", autoadd=1 )
+            libs1 = (libs(env), libs(sconf.env) )
+            sconf.env = env.Copy()
+            res2 = sconf.CheckLib( existing_lib, "main", autoadd=0 )
+            libs2 = (libs(env), libs(sconf.env) )
+            sconf.env = env.Copy()
+            return ((res1, libs1), (res2, libs2))
+
+        def LibWithHeaderChecks(sconf):
+            res1 = sconf.CheckLibWithHeader( existing_lib, "math.h", "C", autoadd=0 )
+            res2 = sconf.CheckLibWithHeader( "hopefullynolib", "math.h", "C", autoadd=0 )
+            return (res1, res2)
+
+        def LibWithHeaderChecksAutoAdd(sconf):
+            def libs(env):
+                if env.has_key( "LIBS" ):
+                    return env['LIBS']
+                else:
+                    return []
+            env = sconf.env.Copy()
+            res1 = sconf.CheckLibWithHeader( existing_lib, "math.h", "C", autoadd=1 )
+            libs1 = (libs(env), libs(sconf.env) )
+            sconf.env = env.Copy()
+            res2 = sconf.CheckLibWithHeader( existing_lib, "math.h", "C", autoadd=0 )
+            libs2 = (libs(env), libs(sconf.env) )
+            sconf.env = env.Copy()
+            return ((res1, libs1), (res2, libs2))
+
+        self._resetSConfState()
+        sconf = SCons.SConf.SConf(scons_env,
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            (res1, res2) = CHeaderChecks(sconf)
+            assert res1 and not res2 
+            (res1, res2) = CXXHeaderChecks(sconf)
+            assert res1 and not res2 
+            (res1, res2) = LibChecks(sconf)
+            assert res1 and not res2 
+            ((res1, libs1), (res2, libs2)) = LibChecksAutoAdd(sconf)
+            assert res1 and res2 
+            assert len(libs1[1]) - 1 == len(libs1[0]) and libs1[1][-1] == existing_lib
+            assert len(libs2[1]) == len(libs2[0]) 
+            (res1, res2) = LibWithHeaderChecks(sconf)
+            assert res1 and not res2 
+            ((res1, libs1), (res2, libs2)) = LibWithHeaderChecksAutoAdd(sconf)
+            assert res1 and res2 
+            assert len(libs1[1]) - 1 == len(libs1[0]) and libs1[1][-1] == existing_lib
+            assert len(libs2[1]) == len(libs2[0]) 
+        finally:
+            sconf.Finish()
+
+    def test_CustomChecks(self):
+
+        def CheckCustom(test):
+            test.Message( "Checking UserTest ... " )
+            prog = """
+#include <stdio.h>
+
+int main() {
+  printf( "Hello" );
+  return 0;
+}
+"""
+            (ret, output) = test.TryRun( prog, ".c" )
+            test.Result( ret )
+            assert ret and output == "Hello" 
+            return ret
+        
+
+        self._resetSConfState()
+        sconf = SCons.SConf.SConf(scons_env,
+                                  custom_tests={'CheckCustom': CheckCustom},
+                                  conf_dir=self.test.workpath('config.tests'),
+                                  log_file=self.test.workpath('config.log'))
+        try:
+            ret = sconf.CheckCustom()
+            assert ret 
+        finally:
+            sconf.Finish()
+            
+
+if __name__ == "__main__":
+    suite = unittest.makeSuite(SConfTestCase, 'test_')
+    if not unittest.TextTestRunner().run(suite).wasSuccessful ():
+        sys.exit(1)
+        

src/engine/SCons/Script/SConscript.py

 import SCons.Node
 import SCons.Node.FS
 import SCons.Platform
+import SCons.SConf
 import SCons.Script
 import SCons.Tool
 import SCons.Util
     globals['Builder']           = SCons.Builder.Builder
     globals['CacheDir']          = SCons.Node.FS.default_fs.CacheDir
     globals['Clean']             = Clean
+    globals['Configure']         = SCons.SConf.SConf
     globals['CScan']             = SCons.Defaults.CScan
     globals['Default']           = Default
     globals['Dir']               = SCons.Node.FS.default_fs.Dir

test/Configure.py

+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import re
+import sys
+
+import TestCmd
+import TestSCons
+
+if sys.platform == 'win32':
+    lib = 'msvcrt'
+else:
+    lib = 'm'
+
+test = TestSCons.TestSCons() 
+python = TestSCons.python
+
+def checkLog( test, logfile, numUpToDate, numCache ):
+    test.fail_test(not os.path.exists(test.workpath('config.log')))
+    log = test.read(test.workpath(logfile))
+    test.fail_test( len( re.findall( "is up to date", log ) ) != numUpToDate )
+    test.fail_test( len( re.findall( "\(cached\): Building \S+ failed in a previous run.", log ) ) != numCache )
+    
+
+
+try:
+
+    # 1.1 if checks are ok, the cache mechanism should work
+
+    test.write( 'SConstruct', """
+env = Environment()
+conf = Configure(env)
+r1 = conf.CheckLibWithHeader( '%s', 'math.h', 'c' )
+r2 = conf.CheckLibWithHeader( None, 'math.h', 'c' )
+r3 = conf.CheckLib( '%s', autoadd=0 )
+r4 = conf.CheckLib( None, autoadd=0 )
+r5 = conf.CheckCHeader( 'math.h' )
+r6 = conf.CheckCXXHeader( 'vector' )
+env = conf.Finish()
+if not (r1 and r2 and r3 and r4 and r5 and r6):
+     Exit(1)
+""" % (lib, lib))
+
+    required_stdout = test.wrap_stdout(build_str='scons: "." is up to date.\n',
+                                       read_str=
+    """Checking for main(); in library %s (header math.h) ... ok
+Checking for main(); in library None (header math.h) ... ok
+Checking for main in library %s... ok
+Checking for main in library None... ok
+Checking for C header math.h... ok
+Checking for C header vector... ok
+""" % (lib, lib))
+
+    test.run(stdout = required_stdout)
+    checkLog(test,'config.log', 0, 0 )
+
+    test.run(stdout = required_stdout)
+    checkLog(test,'config.log',12, 0 )
+
+    # 1.2 if checks are not ok, the cache mechanism should work as well
+    #     (via explicit cache)
+
+    test.write( 'SConstruct', """
+env = Environment()
+conf = Configure(env)
+r1 = conf.CheckCHeader( 'no_std_c_header.h' ) # leads to compile error
+r2 = conf.CheckLib( 'no_c_library_SAFFDG' )   # leads to link error
+env = conf.Finish()
+if not (not r1 and not r2):
+     print "FAIL: ", r1, r2
+     Exit(1)
+""")
+
+    required_stdout = test.wrap_stdout(build_str='scons: "." is up to date.\n',
+                                       read_str=
+    """Checking for C header no_std_c_header.h... failed
+Checking for main in library no_c_library_SAFFDG... failed
+""")
+
+    test.run(stdout = required_stdout)
+    checkLog(test, 'config.log', 0, 0 )
+    
+    test.run(stdout = required_stdout)
+    checkLog(test, 'config.log', 2, 2 )
+
+
+    # 2.1 test that normal builds work together with Sconf
+    test.write( 'SConstruct', """
+env = Environment()
+conf = Configure(env)
+r1 = conf.CheckCHeader( 'math.h' )
+r2 = conf.CheckCHeader( 'no_std_c_header.h' ) # leads to compile error
+env = conf.Finish()
+Export( 'env' )
+SConscript( 'SConscript' )
+""")
+    test.write( 'SConscript', """
+Import( 'env' )
+env.Program( 'TestProgram', 'TestProgram.c' )
+""")
+    test.write( 'TestProgram.c', """
+#include <stdio.h>
+
+int main() {
+  printf( "Hello\\n" );
+}
+""")
+    test.match_func = TestCmd.match_re_dotall
+    required_stdout = test.wrap_stdout(build_str='.*',
+                                       read_str=
+    """Checking for C header math.h... ok
+Checking for C header no_std_c_header.h... failed
+""")
+    test.run( stdout = required_stdout )
+    checkLog( test, 'config.log', 0, 0 )
+    
+    test.run( stdout = required_stdout )
+    checkLog( test, 'config.log', 3, 1 )
+
+
+    # 2.2 test that BuildDir builds work together with Sconf
+    test.write( 'SConstruct', """
+env = Environment()
+BuildDir( 'build', '.' )
+conf = Configure(env, conf_dir='build/config.tests', log_file='build/config.log')
+r1 = conf.CheckCHeader( 'math.h' )
+r2 = conf.CheckCHeader( 'no_std_c_header.h' ) # leads to compile error
+env = conf.Finish()
+Export( 'env' )
+# print open( 'build/config.log' ).readlines()
+SConscript( 'build/SConscript' )
+""")
+    test.write( 'SConscript', """
+Import( 'env' )
+env.Program( 'TestProgram', 'TestProgram.c' )
+""")
+    test.write( 'TestProgram.c', """
+#include <stdio.h>
+
+int main() {
+  printf( "Hello\\n" );
+}
+""")
+    required_stdout = test.wrap_stdout(build_str='.*',
+                                       read_str=
+    """Checking for C header math.h... ok
+Checking for C header no_std_c_header.h... failed
+""")
+    test.run( stdout = required_stdout )
+    checkLog( test, 'build/config.log', 0, 0 )
+    
+    test.run( stdout = required_stdout )
+    checkLog( test, 'build/config.log', 3, 1 )
+    
+
+    # 3.1 test custom tests
+    compileOK = '#include <stdio.h>\\nint main() {printf("Hello");return 0;}'
+    compileFAIL = "syntax error" 
+    linkOK = compileOK
+    linkFAIL = "void myFunc(); int main() { myFunc(); }"
+    runOK = compileOK
+    runFAIL = "int main() { return 1; }"
+    test.write( 'pyAct.py', 'import sys\nopen(sys.argv[1], "w").write(sys.argv[2] + "\\n"),\nsys.exit(int(sys.argv[2]))\n' ) 
+    test.write( 'SConstruct', """ 
+def CheckCustom(test):
+    test.Message( 'Executing MyTest...' )
+    retCompileOK = test.TryCompile( '%s', '.c' )
+    retCompileFAIL = test.TryCompile( '%s', '.c' )
+    retLinkOK = test.TryLink( '%s', '.c' )
+    retLinkFAIL = test.TryLink( '%s', '.c' )
+    (retRunOK, outputRunOK) = test.TryRun( '%s', '.c' )
+    (retRunFAIL, outputRunFAIL) = test.TryRun( '%s', '.c' )
+    (retActOK, outputActOK) = test.TryAction( '%s pyAct.py $TARGET 0' )
+    (retActFAIL, outputActFAIL) = test.TryAction( '%s pyAct.py $TARGET 1' )
+    resOK = retCompileOK and retLinkOK and retRunOK and outputRunOK=="Hello"
+    resOK = resOK and retActOK and int(outputActOK)==0
+    resFAIL = retCompileFAIL or retLinkFAIL or retRunFAIL or outputRunFAIL!=""
+    resFAIL = resFAIL or retActFAIL or outputActFAIL!=""
+    test.Result( resOK and not resFAIL )
+    return resOK and not resFAIL
+
+env = Environment()
+conf = Configure( env, custom_tests={'CheckCustom' : CheckCustom} )
+conf.CheckCustom()
+env = conf.Finish()
+""" % (compileOK, compileFAIL, linkOK, linkFAIL, runOK, runFAIL,
+       python, python ) )
+    required_stdout = test.wrap_stdout(build_str='.*',
+                                       read_str="Executing MyTest...ok\n")
+    test.run(stdout = required_stdout)
+    checkLog( test, 'config.log', 0, 0 )
+
+    test.run(stdout = required_stdout)
+    checkLog( test, 'config.log', 12, 4 )
+
+    test.pass_test()
+    
+finally:
+    pass
+    #os.system( 'find . -type f -exec ls -l {} \;' )
+    #print "-------------config.log------------------"
+    #print test.read( test.workpath('config.log' ))
+    #print "-------------build/config.log------------"    
+    #print test.read( test.workpath('build/config.log' ))