diff options
author | Steven Knight <knight@baldmt.com> | 2003-04-23 03:54:48 (GMT) |
---|---|---|
committer | Steven Knight <knight@baldmt.com> | 2003-04-23 03:54:48 (GMT) |
commit | 6a62c2995bb3d60005b839a5c5a1285ecd5aa859 (patch) | |
tree | f3d9a137283b24eb6202f5ee13bb79f0075ef1d5 | |
parent | 78d494e47d685a52b47ba9e071025be152086c74 (diff) | |
download | SCons-6a62c2995bb3d60005b839a5c5a1285ecd5aa859.zip SCons-6a62c2995bb3d60005b839a5c5a1285ecd5aa859.tar.gz SCons-6a62c2995bb3d60005b839a5c5a1285ecd5aa859.tar.bz2 |
Add SConf infrastructure (Autoconf functionality). (Chrisoph Wiedemann)
-rw-r--r-- | doc/man/scons.1 | 277 | ||||
-rw-r--r-- | src/CHANGES.txt | 12 | ||||
-rw-r--r-- | src/engine/MANIFEST.in | 1 | ||||
-rw-r--r-- | src/engine/SCons/Action.py | 30 | ||||
-rw-r--r-- | src/engine/SCons/ActionTests.py | 60 | ||||
-rw-r--r-- | src/engine/SCons/Platform/posix.py | 121 | ||||
-rw-r--r-- | src/engine/SCons/Platform/win32.py | 50 | ||||
-rw-r--r-- | src/engine/SCons/SConf.py | 609 | ||||
-rw-r--r-- | src/engine/SCons/SConfTests.py | 296 | ||||
-rw-r--r-- | src/engine/SCons/Script/SConscript.py | 2 | ||||
-rw-r--r-- | test/Configure.py | 228 |
11 files changed, 1662 insertions, 24 deletions
diff --git a/doc/man/scons.1 b/doc/man/scons.1 index 18c5d80..91afc37 100644 --- a/doc/man/scons.1 +++ b/doc/man/scons.1 @@ -2868,6 +2868,283 @@ method: 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 diff --git a/src/CHANGES.txt b/src/CHANGES.txt index f6f2746..10cf7b3 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -75,10 +75,22 @@ RELEASE 0.14 - XXX - 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 diff --git a/src/engine/MANIFEST.in b/src/engine/MANIFEST.in index 02646ac..251b48a 100644 --- a/src/engine/MANIFEST.in +++ b/src/engine/MANIFEST.in @@ -24,6 +24,7 @@ SCons/Scanner/__init__.py 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 diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index 45f4c98..9ad925a 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -210,10 +210,28 @@ class CommandAction(ActionBase): 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) @@ -234,7 +252,11 @@ class CommandAction(ActionBase): # 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 diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index eedc865..fb77fe6 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -31,6 +31,7 @@ def Func(): pass import os +import re import StringIO import sys import types @@ -61,6 +62,9 @@ try: 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) """) @@ -80,6 +84,7 @@ class Environment: 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 @@ -94,6 +99,8 @@ class Environment: 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): @@ -102,6 +109,12 @@ class Environment: 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 @@ -348,10 +361,15 @@ class CommandActionTestCase(unittest.TestCase): """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 @@ -359,7 +377,7 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -367,7 +385,7 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -375,7 +393,7 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -385,7 +403,7 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -395,7 +413,7 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -411,7 +429,7 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -423,14 +441,14 @@ class CommandActionTestCase(unittest.TestCase): 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 @@ -445,20 +463,38 @@ class CommandActionTestCase(unittest.TestCase): # 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... """ diff --git a/src/engine/SCons/Platform/posix.py b/src/engine/SCons/Platform/posix.py index 777b928..fd78de4 100644 --- a/src/engine/SCons/Platform/posix.py +++ b/src/engine/SCons/Platform/posix.py @@ -32,11 +32,13 @@ selection method. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" -import SCons.Util -import string import os -import sys import os.path +import popen2 +import string +import sys + +import SCons.Util def escape(arg): "escape shell special characters" @@ -49,7 +51,7 @@ def escape(arg): 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(): @@ -58,7 +60,10 @@ def env_spawn(sh, escape, cmd, args, env): 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 @@ -82,7 +87,110 @@ def fork_spawn(sh, escape, cmd, args, env): 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() @@ -91,8 +199,10 @@ def generate(env): # 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'] = {} @@ -109,6 +219,7 @@ def generate(env): env['SHLIBSUFFIX'] = '.so' env['LIBPREFIXES'] = '$LIBPREFIX' env['LIBSUFFIXES'] = [ '$LIBSUFFIX', '$SHLIBSUFFIX' ] + env['PSPAWN'] = pspawn env['SPAWN'] = spawn env['SHELL'] = 'sh' env['ESCAPE'] = escape diff --git a/src/engine/SCons/Platform/win32.py b/src/engine/SCons/Platform/win32.py index 4286e24..d97e61e 100644 --- a/src/engine/SCons/Platform/win32.py +++ b/src/engine/SCons/Platform/win32.py @@ -32,11 +32,14 @@ selection method. __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, @@ -57,8 +60,6 @@ class TempFileMunge: (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' @@ -84,6 +85,48 @@ class TempFileMunge: # 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") @@ -157,6 +200,7 @@ def generate(env): 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 diff --git a/src/engine/SCons/SConf.py b/src/engine/SCons/SConf.py new file mode 100644 index 0000000..64cd5f9 --- /dev/null +++ b/src/engine/SCons/SConf.py @@ -0,0 +1,609 @@ +"""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 diff --git a/src/engine/SCons/SConfTests.py b/src/engine/SCons/SConfTests.py new file mode 100644 index 0000000..0b737c0 --- /dev/null +++ b/src/engine/SCons/SConfTests.py @@ -0,0 +1,296 @@ +# +# __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) + diff --git a/src/engine/SCons/Script/SConscript.py b/src/engine/SCons/Script/SConscript.py index e77b879..e3da64f 100644 --- a/src/engine/SCons/Script/SConscript.py +++ b/src/engine/SCons/Script/SConscript.py @@ -38,6 +38,7 @@ import SCons.Errors import SCons.Node import SCons.Node.FS import SCons.Platform +import SCons.SConf import SCons.Script import SCons.Tool import SCons.Util @@ -508,6 +509,7 @@ def BuildDefaultGlobals(): 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 diff --git a/test/Configure.py b/test/Configure.py new file mode 100644 index 0000000..b78cdcb --- /dev/null +++ b/test/Configure.py @@ -0,0 +1,228 @@ +#!/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' )) |