From 1aba02432a309e348c59a3145602bb7ec309e8e0 Mon Sep 17 00:00:00 2001 From: stevenknight Date: Fri, 9 Jan 2009 16:43:32 +0000 Subject: [PATCH] Issue 1086: add support for generic batch build actions, and specific support for batched compilation for Microsoft Visual C/C++. Merged revisions 3819-3851,3854-3869,3871-3877,3880 via svnmerge from http://scons.tigris.org/svn/scons/branches/sgk_batch ........ r3820 | stevenknight | 2008-12-09 23:59:14 -0800 (Tue, 09 Dec 2008) | 6 lines Issue 1086: Batch compilation support: * $MSVC_BATCH to control Visual C/C++ batch compilation. * New $CHANGED_SOURCES, $CHANGED_TARGETS, $UNCHANGED_SOURCES and $UNCHANGED_TARGETS construction variables. * New Action(batch_key=, targets=) keyword arguments. ........ r3880 | stevenknight | 2009-01-07 20:50:41 -0800 (Wed, 07 Jan 2009) | 3 lines Use UniqueList objects to collect the all_children(), all_prerequisites() and all_sources() lists instead of calling uniquer_hashables() by hand. ........ git-svn-id: http://scons.tigris.org/svn/scons/trunk@3883 fdb21ef1-2011-0410-befe-b5e4ea1792b1 --- SConstruct | 58 ++++- doc/man/scons.1 | 171 +++++++++++-- doc/scons.mod | 15 ++ doc/user/actions.in | 182 +++++++++++++- doc/user/actions.xml | 182 +++++++++++++- doc/user/builders-writing.in | 293 +++++++++++++---------- src/CHANGES.txt | 11 + src/engine/SCons/Action.py | 160 +++++++++---- src/engine/SCons/ActionTests.py | 17 +- src/engine/SCons/Builder.py | 82 ++++--- src/engine/SCons/BuilderTests.py | 63 ++--- src/engine/SCons/Environment.py | 17 +- src/engine/SCons/Environment.xml | 32 +++ src/engine/SCons/EnvironmentTests.py | 78 ++++-- src/engine/SCons/Executor.py | 345 +++++++++++++++++++++++---- src/engine/SCons/ExecutorTests.py | 65 +++-- src/engine/SCons/Node/FS.py | 3 + src/engine/SCons/Node/FSTests.py | 16 +- src/engine/SCons/Node/NodeTests.py | 5 +- src/engine/SCons/Node/__init__.py | 4 +- src/engine/SCons/SConfTests.py | 9 +- src/engine/SCons/Subst.py | 63 +++-- src/engine/SCons/Taskmaster.py | 18 +- src/engine/SCons/TaskmasterTests.py | 33 ++- src/engine/SCons/Tool/javah.py | 2 +- src/engine/SCons/Tool/mslink.py | 5 +- src/engine/SCons/Tool/msvc.py | 79 +++++- src/engine/SCons/Tool/msvc.xml | 20 ++ src/engine/SCons/Util.py | 47 ++-- test/Batch/Boolean.py | 73 ++++++ test/Batch/CHANGED_SOURCES.py | 118 +++++++++ test/Batch/SOURCES.py | 120 ++++++++++ test/Batch/action-changed.py | 90 +++++++ test/Batch/callable.py | 103 ++++++++ test/Batch/generated.py | 76 ++++++ test/Batch/up_to_date.py | 87 +++++++ test/MSVC/batch.py | 155 ++++++++++++ test/Scanner/generated.py | 7 +- 38 files changed, 2413 insertions(+), 491 deletions(-) create mode 100644 test/Batch/Boolean.py create mode 100644 test/Batch/CHANGED_SOURCES.py create mode 100644 test/Batch/SOURCES.py create mode 100644 test/Batch/action-changed.py create mode 100644 test/Batch/callable.py create mode 100644 test/Batch/generated.py create mode 100644 test/Batch/up_to_date.py create mode 100644 test/MSVC/batch.py diff --git a/SConstruct b/SConstruct index 03957203..4b386299 100644 --- a/SConstruct +++ b/SConstruct @@ -76,6 +76,7 @@ dh_builddeb = whereis('dh_builddeb') fakeroot = whereis('fakeroot') gzip = whereis('gzip') rpmbuild = whereis('rpmbuild') or whereis('rpm') +hg = whereis('hg') svn = whereis('svn') unzip = whereis('unzip') zip = whereis('zip') @@ -104,12 +105,45 @@ version = ARGUMENTS.get('VERSION', '') if not version: version = default_version +hg_status_lines = [] +svn_status_lines = [] + +if hg: + cmd = "%s status --all 2> /dev/null" % hg + hg_status_lines = os.popen(cmd, "r").readlines() + +if svn: + cmd = "%s status --verbose 2> /dev/null" % svn + svn_status_lines = os.popen(cmd, "r").readlines() + revision = ARGUMENTS.get('REVISION', '') +def generate_build_id(revision): + return revision + +if not revision and hg: + hg_heads = os.popen("%s heads 2> /dev/null" % hg, "r").read() + cs = re.search('changeset:\s+(\S+)', hg_heads) + if cs: + revision = cs.group(1) + b = re.search('branch:\s+(\S+)', hg_heads) + if b: + revision = b.group(1) + ':' + revision + def generate_build_id(revision): + result = revision + if filter(lambda l: l[0] in 'AMR!', hg_status_lines): + result = result + '[MODIFIED]' + return result + if not revision and svn: svn_info = os.popen("%s info 2> /dev/null" % svn, "r").read() m = re.search('Revision: (\d+)', svn_info) if m: revision = m.group(1) + def generate_build_id(revision): + result = 'r' + revision + if filter(lambda l: l[0] in 'ACDMR', svn_status_lines): + result = result + '[MODIFIED]' + return result checkpoint = ARGUMENTS.get('CHECKPOINT', '') if checkpoint: @@ -120,19 +154,10 @@ if checkpoint: checkpoint = 'r' + revision version = version + '.' + checkpoint -svn_status = None -svn_status_lines = [] - -if svn: - svn_status = os.popen("%s status --verbose 2> /dev/null" % svn, "r").read() - svn_status_lines = svn_status[:-1].split('\n') - build_id = ARGUMENTS.get('BUILD_ID') if build_id is None: if revision: - build_id = 'r' + revision - if filter(lambda l: l[0] in 'ACDMR', svn_status_lines): - build_id = build_id + '[MODIFIED]' + build_id = generate_build_id(revision) else: build_id = '' @@ -1173,17 +1198,24 @@ SConscript('doc/SConscript') # source archive from the project files and files in the change. # -if not svn_status: - "Not building in a Subversion tree; skipping building src package." -else: +sfiles = None +if hg_status_lines: + slines = filter(lambda l: l[0] in 'ACM', hg_status_lines) + sfiles = map(lambda l: l.split()[-1], slines) +elif svn_status_lines: slines = filter(lambda l: l[0] in ' MA', svn_status_lines) sentries = map(lambda l: l.split()[-1], slines) sfiles = filter(os.path.isfile, sentries) +else: + "Not building in a Mercurial or Subversion tree; skipping building src package." +if sfiles: remove_patterns = [ + '.hgt/*', '.svnt/*', '*.aeignore', '*.cvsignore', + '*.hgignore', 'www/*', ] diff --git a/doc/man/scons.1 b/doc/man/scons.1 index 5526c5e4..d1f9dcd7 100644 --- a/doc/man/scons.1 +++ b/doc/man/scons.1 @@ -1481,11 +1481,15 @@ These warnings are enabled by default. --warn=reserved-variable, --warn=no-reserved-variable Enables or disables warnings about attempts to set the reserved construction variable names +.BR CHANGED_SOURCES , +.BR CHANGED_TARGETS , .BR TARGET , .BR TARGETS , -.BR SOURCE +.BR SOURCE , +.BR SOURCES , +.BR UNCHANGED_SOURCES or -.BR SOURCES . +.BR UNCHANGED_TARGETS . These warnings are disabled by default. .TP @@ -8644,16 +8648,25 @@ a = Action(build_it, varlist=['XXX']) The .BR Action () global function -also takes a +can be passed the following +optional keyword arguments +to modify the Action object's behavior: + +.IP .B chdir -keyword argument -which specifies that +The +.B chdir +keyword argument specifies that scons will execute the action after changing to the specified directory. -If the chdir argument is +If the +.B chdir +argument is a string or a directory Node, scons will change to the specified directory. -If the chdir argument +If the +.B chdir +argument is not a string or Node and is non-zero, then scons will change to the @@ -8688,6 +8701,8 @@ a = Action("build < ${SOURCE.file} > ${TARGET.file}", chdir=1) .EE +.IP +.B exitstatfunc The .BR Action () global function @@ -8703,6 +8718,7 @@ or modified value. This can be used, for example, to specify that an Action object's return value should be ignored +under special conditions and SCons should, therefore, consider that the action always suceeds: @@ -8714,6 +8730,111 @@ a = Action("build < ${SOURCE.file} > ${TARGET.file}", exitstatfunc=always_succeed) .EE +.IP +.B batch_key +The +.B batch_key +keyword argument can be used +to specify that the Action can create multiple target files +by processing multiple independent source files simultaneously. +(The canonical example is "batch compilation" +of multiple object files +by passing multiple source files +to a single invocation of a compiler +such as Microsoft's Visual C / C++ compiler.) +If the +.B batch_key +argument is any non-False, non-callable Python value, +the configured Action object will cause +.B scons +to collect all targets built with the Action object +and configured with the same construction environment +into single invocations of the Action object's +command line or function. +Command lines will typically want to use the +.BR CHANGED_SOURCES +construction variable +(and possibly +.BR CHANGED_TARGETS +as well) +to only pass to the command line those sources that +have actually changed since their targets were built. + +Example: + +.ES +a = Action('build $CHANGED_SOURCES', batch_key=True) +.EE + +The +.B batch_key +argument may also be +a callable function +that returns a key that +will be used to identify different +"batches" of target files to be collected +for batch building. +A +.B batch_key +function must take the following arguments: + +.IP action +The action object. + +.IP env +The construction environment +configured for the target. + +.IP target +The list of targets for a particular configured action. + +.IP source +The list of source for a particular configured action. + +The returned key should typically +be a tuple of values derived from the arguments, +using any appropriate logic to decide +how multiple invocations should be batched. +For example, a +.B batch_key +function may decide to return +the value of a specific construction +variable from the +.B env +argument +which will cause +.B scons +to batch-build targets +with matching values of that variable, +or perhaps return the +.BR id () +of the entire construction environment, +in which case +.B scons +will batch-build +all targets configured with the same construction environment. +Returning +.B None +indicates that +the particular target should +.I not +be part of any batched build, +but instead will be built +by a separate invocation of action's +command or function. +Example: + +.ES +def batch_key(action, env, target, source): + tdir = target[0].dir + if tdir.name == 'special': + # Don't batch-build any target + # in the special/ subdirectory. + return None + return (id(action), id(env), tdir) +a = Action('build $CHANGED_SOURCES', batch_key=batch_key) +.EE + .SS Miscellaneous Action Functions .B scons @@ -8906,20 +9027,42 @@ prefix. Besides construction variables, scons provides the following variables for each command execution: -.IP TARGET -The file name of the target being built, or the file name of the first -target if multiple targets are being built. +.IP CHANGED_SOURCES +The file names of all sources of the build command +that have changed since the target was last built. -.IP TARGETS -The file names of all targets being built. +.IP CHANGED_TARGETS +The file names of all targets that would be built +from sources that have changed since the target was last built. .IP SOURCE -The file name of the source of the build command, or the file name of the -first source if multiple sources are being built. +The file name of the source of the build command, +or the file name of the first source +if multiple sources are being built. .IP SOURCES The file names of the sources of the build command. +.IP TARGET +The file name of the target being built, +or the file name of the first target +if multiple targets are being built. + +.IP TARGETS +The file names of all targets being built. + +.IP UNCHANGED_SOURCES +The file names of all sources of the build command +that have +.I not +changed since the target was last built. + +.IP UNCHANGED_TARGETS +The file names of all targets that would be built +from sources that have +.I not +changed since the target was last built. + (Note that the above variables are reserved and may not be set in a construction environment.) diff --git a/doc/scons.mod b/doc/scons.mod index 767b40ce..a2927cf8 100644 --- a/doc/scons.mod +++ b/doc/scons.mod @@ -401,6 +401,8 @@ --> builder function"> +build action"> +build actions"> builder method"> Configure Contexts"> @@ -442,6 +444,19 @@ typedef"> + + +action="> +batch_key="> +cmdstr="> +exitstatfunc="> +strfunction="> +varlist="> + - + - XXX + &SCons; supports several types of &build_actions; + that can be performed to build one or more target files. + Usually, a &build_action; is a command-line string + that invokes an external command. + A build action can also be an external command + specified as a list of arguments, + or even a Python function. - + -
- XXX + - + Build action objects are created by the &Action; function. + This function is, in fact, what &SCons; uses + to interpret the &action; + keyword argument when you call the &Builder; function. + So the following line that creates a simple Builder: - XXX + - + + b = Builder(action = 'build < $SOURCE > $TARGET') + -
+ + + Is equivalent to: + + + + + b = Builder(action = Action('build < $SOURCE > $TARGET')) + + + + + The advantage of using the &Action; function directly + is that it can take a number of additional options + to modify the action's behavior in many useful ways. + + + +
+ Command Strings as Actions + +
+ Suppressing Command-Line Printing + + + + XXX + + + +
+ +
+ Ignoring Exit Status + + + + XXX + + + +
+ +
+ +
+ Argument Lists as Actions + + + + XXX + + + +
+ +
+ Python Functions as Actions + + + + XXX + + + +
+ +
+ Modifying How an Action is Printed + +
+ XXX: the &strfunction; keyword argument + + + + XXX + + + +
+ +
+ XXX: the &cmdstr; keyword argument + + + + XXX + + + +
+ +
+ +
+ Making an Action Depend on Variable Contents: the &varlist; keyword argument + + + + XXX + + + +
+ +
+ chdir=1 + + + + XXX + + + +
+ +
+ Batch Building of Multiple Targets from Separate Sources: the &batch_key; keyword argument + + + + XXX + + + +
+ +
+ Manipulating the Exit Status of an Action: the &exitstatfunc; keyword argument + + + + XXX + + + +
+ + diff --git a/doc/user/actions.xml b/doc/user/actions.xml index 928b7ea0..04178b0d 100644 --- a/doc/user/actions.xml +++ b/doc/user/actions.xml @@ -222,19 +222,183 @@ solutions to the above limitations. --> - + - XXX + &SCons; supports several types of &build_actions; + that can be performed to build one or more target files. + Usually, a &build_action; is a command-line string + that invokes an external command. + A build action can also be an external command + specified as a list of arguments, + or even a Python function. - + -
- XXX + - + Build action objects are created by the &Action; function. + This function is, in fact, what &SCons; uses + to interpret the &action; + keyword argument when you call the &Builder; function. + So the following line that creates a simple Builder: - XXX + - + + b = Builder(action = 'build < $SOURCE > $TARGET') + -
+ + + Is equivalent to: + + + + + b = Builder(action = Action('build < $SOURCE > $TARGET')) + + + + + The advantage of using the &Action; function directly + is that it can take a number of additional options + to modify the action's behavior in many useful ways. + + + +
+ Command Strings as Actions + +
+ Suppressing Command-Line Printing + + + + XXX + + + +
+ +
+ Ignoring Exit Status + + + + XXX + + + +
+ +
+ +
+ Argument Lists as Actions + + + + XXX + + + +
+ +
+ Python Functions as Actions + + + + XXX + + + +
+ +
+ Modifying How an Action is Printed + +
+ XXX: the &strfunction; keyword argument + + + + XXX + + + +
+ +
+ XXX: the &cmdstr; keyword argument + + + + XXX + + + +
+ +
+ +
+ Making an Action Depend on Variable Contents: the &varlist; keyword argument + + + + XXX + + + +
+ +
+ chdir=1 + + + + XXX + + + +
+ +
+ Batch Building of Multiple Targets from Separate Sources: the &batch_key; keyword argument + + + + XXX + + + +
+ +
+ Manipulating the Exit Status of an Action: the &exitstatfunc; keyword argument + + + + XXX + + + +
+ + diff --git a/doc/user/builders-writing.in b/doc/user/builders-writing.in index dc6e95b9..2460b372 100644 --- a/doc/user/builders-writing.in +++ b/doc/user/builders-writing.in @@ -102,7 +102,7 @@ This functionality could be invoked as in the following example: programs, libraries, documents. you frequently want to be able to build some other type of file - not supported directly by &SCons; + not supported directly by &SCons;. Fortunately, &SCons; makes it very easy to define your own &Builder; objects for any custom file types you want to build. @@ -186,7 +186,8 @@ This functionality could be invoked as in the following example: - With the &Builder; so attached to our &consenv; + With the &Builder; attached to our &consenv; + with the name &Foo;, we can now actually call it like so: @@ -859,164 +860,198 @@ This functionality could be invoked as in the following example: + +
Where To Put Your Custom Builders and Tools - + - The site_scons directory gives you a place to - put Python modules you can import into your SConscripts - (site_scons), add-on tools that can integrate into &SCons; - (site_scons/site_tools), and a site_scons/site_init.py file that - gets read before any &SConstruct; or &SConscript;, allowing you to - change &SCons;'s default behavior. + The site_scons directory gives you a place to + put Python modules you can import into your SConscripts + (site_scons), add-on tools that can integrate into &SCons; + (site_scons/site_tools), and a site_scons/site_init.py file that + gets read before any &SConstruct; or &SConscript;, allowing you to + change &SCons;'s default behavior. - + - + - If you get a tool from somewhere (the &SCons; wiki or a third party, - for instance) and you'd like to use it in your project, the - site_scons dir is the simplest place to put it. - Tools come in two flavors; either a Python function that operates on - an &Environment; or a Python file containing two functions, exists() - and generate(). + If you get a tool from somewhere (the &SCons; wiki or a third party, + for instance) and you'd like to use it in your project, the + site_scons dir is the simplest place to put it. + Tools come in two flavors; either a Python function that operates on + an &Environment; or a Python file containing two functions, exists() + and generate(). - + - + - A single-function Tool can just be included in your - site_scons/site_init.py file where it will be - parsed and made available for use. For instance, you could have a - site_scons/site_init.py file like this: + A single-function Tool can just be included in your + site_scons/site_init.py file where it will be + parsed and made available for use. For instance, you could have a + site_scons/site_init.py file like this: - + - - - def TOOL_ADD_HEADER(env): - """A Tool to add a header from $HEADER to the source file""" - add_header = Builder(action=['echo "$HEADER" > $TARGET', - 'cat $SOURCE >> $TARGET']) - env.Append(BUILDERS = {'AddHeader' : add_header}) - env['HEADER'] = '' # set default value - - - env=Environment(tools=['default', TOOL_ADD_HEADER], HEADER="=====") - env.AddHeader('tgt', 'src') - - - hi there - - + + + def TOOL_ADD_HEADER(env): + """A Tool to add a header from $HEADER to the source file""" + add_header = Builder(action=['echo "$HEADER" > $TARGET', + 'cat $SOURCE >> $TARGET']) + env.Append(BUILDERS = {'AddHeader' : add_header}) + env['HEADER'] = '' # set default value + + + env=Environment(tools=['default', TOOL_ADD_HEADER], HEADER="=====") + env.AddHeader('tgt', 'src') + + + hi there + + - + - and a &SConstruct; like this: + and a &SConstruct; like this: - + - - # Use TOOL_ADD_HEADER from site_scons/site_init.py - env=Environment(tools=['default', TOOL_ADD_HEADER], HEADER="=====") - env.AddHeader('tgt', 'src') - + + # Use TOOL_ADD_HEADER from site_scons/site_init.py + env=Environment(tools=['default', TOOL_ADD_HEADER], HEADER="=====") + env.AddHeader('tgt', 'src') + - + - The TOOL_ADD_HEADER tool method will be - called to add the AddHeader tool to the - environment. + The TOOL_ADD_HEADER tool method will be + called to add the AddHeader tool to the + environment. - + - + - - Similarly, a more full-fledged tool with - exists() and generate() - methods can be installed in - site_scons/site_tools/toolname.py. Since - site_scons/site_tools is automatically added - to the head of the tool search path, any tool found there will be - available to all environments. Furthermore, a tool found there - will override a built-in tool of the same name, so if you need to - change the behavior of a built-in tool, site_scons gives you the - hook you need. - + + Similarly, a more full-fledged tool with + exists() and generate() + methods can be installed in + site_scons/site_tools/toolname.py. Since + site_scons/site_tools is automatically added + to the head of the tool search path, any tool found there will be + available to all environments. Furthermore, a tool found there + will override a built-in tool of the same name, so if you need to + change the behavior of a built-in tool, site_scons gives you the + hook you need. + - - Many people have a library of utility Python functions they'd like - to include in &SConscript;s; just put that module in - site_scons/my_utils.py or any valid Python module name of your - choice. For instance you can do something like this in - site_scons/my_utils.py to add build_id and MakeWorkDir functions: - - - - - from SCons.Script import * # for Execute and Mkdir - def build_id(): - """Return a build ID (stub version)""" - return "100" - def MakeWorkDir(workdir): - """Create the specified dir immediately""" - Execute(Mkdir(workdir)) - - - import my_utils - MakeWorkDir('/tmp/work') - print "build_id=" + my_utils.build_id() - - + + Many people have a library of utility Python functions they'd like + to include in &SConscript;s; just put that module in + site_scons/my_utils.py or any valid Python module name of your + choice. For instance you can do something like this in + site_scons/my_utils.py to add build_id and MakeWorkDir functions: + + + + + from SCons.Script import * # for Execute and Mkdir + def build_id(): + """Return a build ID (stub version)""" + return "100" + def MakeWorkDir(workdir): + """Create the specified dir immediately""" + Execute(Mkdir(workdir)) + + + import my_utils + MakeWorkDir('/tmp/work') + print "build_id=" + my_utils.build_id() + + - + - And then in your &SConscript; or any sub-&SConscript; anywhere in - your build, you can import my_utils and use it: + And then in your &SConscript; or any sub-&SConscript; anywhere in + your build, you can import my_utils and use it: - + - - import my_utils - print "build_id=" + my_utils.build_id() - my_utils.MakeWorkDir('/tmp/work') - + + import my_utils + print "build_id=" + my_utils.build_id() + my_utils.MakeWorkDir('/tmp/work') + - - Note that although you can put this library in - site_scons/site_init.py, - it is no better there than site_scons/my_utils.py - since you still have to import that module into your &SConscript;. - Also note that in order to refer to objects in the SCons namespace - such as &Environment; or &Mkdir; or &Execute; in any file other - than a &SConstruct; or &SConscript; you always need to do - - - from SCons.Script import * - + + Note that although you can put this library in + site_scons/site_init.py, + it is no better there than site_scons/my_utils.py + since you still have to import that module into your &SConscript;. + Also note that in order to refer to objects in the SCons namespace + such as &Environment; or &Mkdir; or &Execute; in any file other + than a &SConstruct; or &SConscript; you always need to do + + + from SCons.Script import * + - - This is true in modules in site_scons such as - site_scons/site_init.py as well. - + + This is true in modules in site_scons such as + site_scons/site_init.py as well. + - + - If you have a machine-wide site dir you'd like to use instead of - ./site_scons, use the - --site-dir option to point to your dir. - site_init.py and - site_tools will be located under that dir. - To avoid using a site_scons dir at all, even - if it exists, use the --no-site-dir option. + If you have a machine-wide site dir you'd like to use instead of + ./site_scons, use the + --site-dir option to point to your dir. + site_init.py and + site_tools will be located under that dir. + To avoid using a site_scons dir at all, even + if it exists, use the --no-site-dir option. - +
diff --git a/src/CHANGES.txt b/src/CHANGES.txt index 76cca21e..3cdc3044 100644 --- a/src/CHANGES.txt +++ b/src/CHANGES.txt @@ -10,6 +10,11 @@ RELEASE 1.X - XXX + From Stanislav Baranov, Ted Johnson and Steven Knight: + + - Add support for batch compilation of Visual Studio C/C++ source + files, controlled by a new MSVC_BATCH construction variable. + From Steven Knight: - Print the message, "scons: Build interrupted." on error output, @@ -21,6 +26,12 @@ RELEASE 1.X - XXX - Fix use of $SOURCE and $SOURCES attributes when there are no sources specified in the Builder call. + - Add support for new $CHANGED_SOURCES, $CHANGED_TARGETS, + $UNCHANGED_SOURCES and $UNCHANGED_TARGETS variables. + + - Add general support for batch builds through new batch_key= and + targets= keywords to Action object creation. + From Arve Knudsen: - Make linker tools differentiate properly between SharedLibrary diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index bc1d7240..e106e74e 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -392,7 +392,7 @@ def _do_create_list_action(act, kw): aa = _do_create_action(a, kw) if aa is not None: acts.append(aa) if not acts: - return None + return ListAction([]) elif len(acts) == 1: return acts[0] else: @@ -414,6 +414,11 @@ class ActionBase: def __cmp__(self, other): return cmp(self.__dict__, other) + def no_batch_key(self, env, target, source): + return None + + batch_key = no_batch_key + def genstring(self, target, source, env): return str(self) @@ -446,15 +451,18 @@ class ActionBase: self.presub_env = None # don't need this any more return lines - def get_executor(self, env, overrides, tlist, slist, executor_kw): - """Return the Executor for this Action.""" - return SCons.Executor.Executor(self, env, overrides, - tlist, slist, executor_kw) + def get_targets(self, env, executor): + """ + Returns the type of targets ($TARGETS, $CHANGED_TARGETS) used + by this action. + """ + return self.targets class _ActionAction(ActionBase): """Base class for actions that create output objects.""" def __init__(self, cmdstr=_null, strfunction=_null, varlist=(), presub=_null, chdir=None, exitstatfunc=None, + batch_key=None, targets='$TARGETS', **kw): self.cmdstr = cmdstr if strfunction is not _null: @@ -469,6 +477,19 @@ class _ActionAction(ActionBase): exitstatfunc = default_exitstatfunc self.exitstatfunc = exitstatfunc + self.targets = targets + + if batch_key: + if not callable(batch_key): + # They have set batch_key, but not to their own + # callable. The default behavior here will batch + # *all* targets+sources using this action, separated + # for each construction environment. + def default_batch_key(self, env, target, source): + return (id(self), id(env)) + batch_key = default_batch_key + SCons.Util.AddMethod(self, batch_key, 'batch_key') + def print_cmd_line(self, s, target, source, env): sys.stdout.write(s + "\n") @@ -477,7 +498,8 @@ class _ActionAction(ActionBase): presub=_null, show=_null, execute=_null, - chdir=_null): + chdir=_null, + executor=None): if not is_List(target): target = [target] if not is_List(source): @@ -498,15 +520,27 @@ class _ActionAction(ActionBase): chdir = str(chdir.abspath) except AttributeError: if not is_String(chdir): - chdir = str(target[0].dir) + if executor: + chdir = str(executor.batches[0].targets[0].dir) + else: + chdir = str(target[0].dir) if presub: + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() t = string.join(map(str, target), ' and ') l = string.join(self.presub_lines(env), '\n ') out = "Building %s with action:\n %s\n" % (t, l) sys.stdout.write(out) cmd = None if show and self.strfunction: - cmd = self.strfunction(target, source, env) + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() + try: + cmd = self.strfunction(target, source, env, executor) + except TypeError: + cmd = self.strfunction(target, source, env) if cmd: if chdir: cmd = ('os.chdir(%s)\n' % repr(chdir)) + cmd @@ -524,7 +558,7 @@ class _ActionAction(ActionBase): if chdir: os.chdir(chdir) try: - stat = self.execute(target, source, env) + stat = self.execute(target, source, env, executor=executor) if isinstance(stat, SCons.Errors.BuildError): s = exitstatfunc(stat.status) if s: @@ -657,8 +691,11 @@ class CommandAction(_ActionAction): return string.join(map(str, self.cmd_list), ' ') return str(self.cmd_list) - def process(self, target, source, env): - result = env.subst_list(self.cmd_list, 0, target, source) + def process(self, target, source, env, executor=None): + if executor: + result = env.subst_list(self.cmd_list, 0, executor=executor) + else: + result = env.subst_list(self.cmd_list, 0, target, source) silent = None ignore = None while 1: @@ -675,20 +712,23 @@ class CommandAction(_ActionAction): pass return result, ignore, silent - def strfunction(self, target, source, env): + def strfunction(self, target, source, env, executor=None): if self.cmdstr is None: return None if self.cmdstr is not _null: from SCons.Subst import SUBST_RAW - c = env.subst(self.cmdstr, SUBST_RAW, target, source) + if executor: + c = env.subst(self.cmdstr, SUBST_RAW, executor=executor) + else: + c = env.subst(self.cmdstr, SUBST_RAW, target, source) if c: return c - cmd_list, ignore, silent = self.process(target, source, env) + cmd_list, ignore, silent = self.process(target, source, env, executor) if silent: return '' return _string_from_cmd_list(cmd_list[0]) - def execute(self, target, source, env): + def execute(self, target, source, env, executor=None): """Execute a command action. This will handle lists of commands as well as individual commands, @@ -733,7 +773,10 @@ class CommandAction(_ActionAction): # reasonable for just about everything else: ENV[key] = str(value) - cmd_list, ignore, silent = self.process(target, map(rfile, source), env) + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() + cmd_list, ignore, silent = self.process(target, map(rfile, source), env, executor) # Use len() to filter out any "command" that's zero-length. for cmd_line in filter(len, cmd_list): @@ -748,7 +791,7 @@ class CommandAction(_ActionAction): command=cmd_line) return 0 - def get_presig(self, target, source, env): + def get_presig(self, target, source, env, executor=None): """Return the signature contents of this action's command line. This strips $(-$) and everything in between the string, @@ -760,16 +803,22 @@ class CommandAction(_ActionAction): cmd = string.join(map(str, cmd)) else: cmd = str(cmd) - return env.subst_target_source(cmd, SUBST_SIG, target, source) + if executor: + return env.subst_target_source(cmd, SUBST_SIG, executor=executor) + else: + return env.subst_target_source(cmd, SUBST_SIG, target, source) - def get_implicit_deps(self, target, source, env): + def get_implicit_deps(self, target, source, env, executor=None): icd = env.get('IMPLICIT_COMMAND_DEPENDENCIES', True) if is_String(icd) and icd[:1] == '$': icd = env.subst(icd) if not icd or icd in ('0', 'None'): return [] from SCons.Subst import SUBST_SIG - cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, target, source) + if executor: + cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, executor=executor) + else: + cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, target, source) res = [] for cmd_line in cmd_list: if cmd_line: @@ -785,14 +834,21 @@ class CommandGeneratorAction(ActionBase): self.generator = generator self.gen_kw = kw self.varlist = kw.get('varlist', ()) + self.targets = kw.get('targets', '$TARGETS') - def _generate(self, target, source, env, for_signature): + def _generate(self, target, source, env, for_signature, executor=None): # ensure that target is a list, to make it easier to write # generator functions: if not is_List(target): target = [target] - ret = self.generator(target=target, source=source, env=env, for_signature=for_signature) + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() + ret = self.generator(target=target, + source=source, + env=env, + for_signature=for_signature) #TODO(1.5) gen_cmd = Action(ret, **self.gen_kw) gen_cmd = apply(Action, (ret,), self.gen_kw) if not gen_cmd: @@ -809,25 +865,33 @@ class CommandGeneratorAction(ActionBase): act = self._generate([], [], env, 1) return str(act) - def genstring(self, target, source, env): - return self._generate(target, source, env, 1).genstring(target, source, env) + def batch_key(self, env, target, source): + return self._generate(target, source, env, 1).batch_key(env, target, source) + + def genstring(self, target, source, env, executor=None): + return self._generate(target, source, env, 1, executor).genstring(target, source, env) def __call__(self, target, source, env, exitstatfunc=_null, presub=_null, - show=_null, execute=_null, chdir=_null): - act = self._generate(target, source, env, 0) + show=_null, execute=_null, chdir=_null, executor=None): + act = self._generate(target, source, env, 0, executor) + if act is None: + raise UserError("While building `%s': Cannot deduce file extension from source files: %s" % (repr(map(str, target)), repr(map(str, source)))) return act(target, source, env, exitstatfunc, presub, - show, execute, chdir) + show, execute, chdir, executor) - def get_presig(self, target, source, env): + def get_presig(self, target, source, env, executor=None): """Return the signature contents of this action's command line. This strips $(-$) and everything in between the string, since those parts don't affect signatures. """ - return self._generate(target, source, env, 1).get_presig(target, source, env) + return self._generate(target, source, env, 1, executor).get_presig(target, source, env) - def get_implicit_deps(self, target, source, env): - return self._generate(target, source, env, 1).get_implicit_deps(target, source, env) + def get_implicit_deps(self, target, source, env, executor=None): + return self._generate(target, source, env, 1, executor).get_implicit_deps(target, source, env) + + def get_targets(self, env, executor): + return self._generate(None, None, env, 1, executor).get_targets(env, executor) @@ -864,14 +928,17 @@ class LazyAction(CommandGeneratorAction, CommandAction): return CommandGeneratorAction def _generate_cache(self, env): - c = env.get(self.var, '') + if env: + c = env.get(self.var, '') + else: + c = '' #TODO(1.5) gen_cmd = Action(c, **self.gen_kw) gen_cmd = apply(Action, (c,), self.gen_kw) if not gen_cmd: raise SCons.Errors.UserError("$%s value %s cannot be used to create an Action." % (self.var, repr(c))) return gen_cmd - def _generate(self, target, source, env, for_signature): + def _generate(self, target, source, env, for_signature, executor=None): return self._generate_cache(env) def __call__(self, target, source, env, *args, **kw): @@ -915,12 +982,15 @@ class FunctionAction(_ActionAction): except AttributeError: return "unknown_python_function" - def strfunction(self, target, source, env): + def strfunction(self, target, source, env, executor=None): if self.cmdstr is None: return None if self.cmdstr is not _null: from SCons.Subst import SUBST_RAW - c = env.subst(self.cmdstr, SUBST_RAW, target, source) + if executor: + c = env.subst(self.cmdstr, SUBST_RAW, executor=executor) + else: + c = env.subst(self.cmdstr, SUBST_RAW, target, source) if c: return c def array(a): @@ -953,9 +1023,12 @@ class FunctionAction(_ActionAction): return str(self.execfunction) return "%s(target, source, env)" % name - def execute(self, target, source, env): + def execute(self, target, source, env, executor=None): exc_info = (None,None,None) try: + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() rsources = map(rfile, source) try: result = self.execfunction(target=target, source=rsources, env=env) @@ -971,7 +1044,10 @@ class FunctionAction(_ActionAction): result = SCons.Errors.convert_to_BuildError(result, exc_info) result.node=target result.action=self - result.command=self.strfunction(target, source, env) + try: + result.command=self.strfunction(target, source, env, executor) + except TypeError: + result.command=self.strfunction(target, source, env) # FIXME: This maintains backward compatibility with respect to # which type of exceptions were returned by raising an @@ -1013,6 +1089,7 @@ class ListAction(ActionBase): # our children will have had any varlist # applied; we don't need to do it again self.varlist = () + self.targets = '$TARGETS' def genstring(self, target, source, env): return string.join(map(lambda a, t=target, s=source, e=env: @@ -1038,10 +1115,13 @@ class ListAction(ActionBase): "") def __call__(self, target, source, env, exitstatfunc=_null, presub=_null, - show=_null, execute=_null, chdir=_null): + show=_null, execute=_null, chdir=_null, executor=None): + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() for act in self.list: stat = act(target, source, env, exitstatfunc, presub, - show, execute, chdir) + show, execute, chdir, executor) if stat: return stat return 0 @@ -1111,7 +1191,7 @@ class ActionCaller: kw[key] = self.subst(self.kw[key], target, source, env) return kw - def __call__(self, target, source, env): + def __call__(self, target, source, env, executor=None): args = self.subst_args(target, source, env) kw = self.subst_kw(target, source, env) #TODO(1.5) return self.parent.actfunc(*args, **kw) diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py index 643e9fa8..ae6a15c2 100644 --- a/src/engine/SCons/ActionTests.py +++ b/src/engine/SCons/ActionTests.py @@ -405,13 +405,6 @@ class ActionTestCase(unittest.TestCase): a2 = SCons.Action.Action(a1) assert a2 is a1, a2 -class ActionBaseTestCase(unittest.TestCase): - def test_get_executor(self): - """Test the ActionBase.get_executor() method""" - a = SCons.Action.Action('foo') - x = a.get_executor({}, {}, [], [], {}) - assert x is not None, x - class _ActionActionTestCase(unittest.TestCase): def test__init__(self): @@ -1589,13 +1582,14 @@ class FunctionActionTestCase(unittest.TestCase): c = test.read(outfile, 'r') assert c == "class1b\n", c - def build_it(target, source, env, self=self): + def build_it(target, source, env, executor=None, self=self): self.build_it = 1 return 0 - def string_it(target, source, env, self=self): + def string_it(target, source, env, executor=None, self=self): self.string_it = 1 return None - act = SCons.Action.FunctionAction(build_it, { 'strfunction' : string_it }) + act = SCons.Action.FunctionAction(build_it, + { 'strfunction' : string_it }) r = act([], [], Environment()) assert r == 0, r assert self.build_it @@ -1996,8 +1990,7 @@ class ActionCompareTestCase(unittest.TestCase): if __name__ == "__main__": suite = unittest.TestSuite() - tclasses = [ ActionBaseTestCase, - _ActionActionTestCase, + tclasses = [ _ActionActionTestCase, ActionTestCase, CommandActionTestCase, CommandGeneratorActionTestCase, diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py index 1d295163..21638a5f 100644 --- a/src/engine/SCons/Builder.py +++ b/src/engine/SCons/Builder.py @@ -118,6 +118,15 @@ class _Null: _null = _Null +def match_splitext(path, suffixes = []): + if suffixes: + matchsuf = filter(lambda S,path=path: path[-len(S):] == S, + suffixes) + if matchsuf: + suf = max(map(None, map(len, matchsuf), matchsuf))[1] + return [path[:-len(suf)], path[-len(suf):]] + return SCons.Util.splitext(path) + class DictCmdGenerator(SCons.Util.Selector): """This is a callable class that can be used as a command generator function. It holds on to a dictionary @@ -142,20 +151,22 @@ class DictCmdGenerator(SCons.Util.Selector): return [] if self.source_ext_match: + suffixes = self.src_suffixes() ext = None for src in map(str, source): - my_ext = SCons.Util.splitext(src)[1] + my_ext = match_splitext(src, suffixes)[1] if ext and my_ext != ext: raise UserError("While building `%s' from `%s': Cannot build multiple sources with different extensions: %s, %s" % (repr(map(str, target)), src, ext, my_ext)) ext = my_ext else: - ext = SCons.Util.splitext(str(source[0]))[1] + ext = match_splitext(str(source[0]), self.src_suffixes())[1] if not ext: + #return ext raise UserError("While building `%s': Cannot deduce file extension from source files: %s" % (repr(map(str, target)), repr(map(str, source)))) try: - ret = SCons.Util.Selector.__call__(self, env, source) + ret = SCons.Util.Selector.__call__(self, env, source, ext) except KeyError, e: raise UserError("Ambiguous suffixes after environment substitution: %s == %s == %s" % (e[0], e[1], e[2])) if ret is None: @@ -295,8 +306,9 @@ def _node_errors(builder, env, tlist, slist): if t.builder != builder: msg = "Two different builders (%s and %s) were specified for the same target: %s" % (t.builder.get_name(env), builder.get_name(env), t) raise UserError, msg - if t.get_executor().targets != tlist: - msg = "Two different target lists have a target in common: %s (from %s and from %s)" % (t, map(str, t.get_executor().targets), map(str, tlist)) + # TODO(batch): list constructed each time! + if t.get_executor().get_all_targets() != tlist: + msg = "Two different target lists have a target in common: %s (from %s and from %s)" % (t, map(str, t.get_executor().get_all_targets()), map(str, tlist)) raise UserError, msg elif t.sources != slist: msg = "Multiple ways to build the same target were specified for: %s (from %s and from %s)" % (t, map(str, t.sources), map(str, slist)) @@ -441,30 +453,10 @@ class BuilderBase: if not env: env = self.env if env: - matchsuf = filter(lambda S,path=path: path[-len(S):] == S, - self.src_suffixes(env)) - if matchsuf: - suf = max(map(None, map(len, matchsuf), matchsuf))[1] - return [path[:-len(suf)], path[-len(suf):]] - return SCons.Util.splitext(path) - - def get_single_executor(self, env, tlist, slist, executor_kw): - if not self.action: - raise UserError, "Builder %s must have an action to build %s."%(self.get_name(env or self.env), map(str,tlist)) - return self.action.get_executor(env or self.env, - [], # env already has overrides - tlist, - slist, - executor_kw) - - def get_multi_executor(self, env, tlist, slist, executor_kw): - try: - executor = tlist[0].get_executor(create = 0) - except (AttributeError, IndexError): - return self.get_single_executor(env, tlist, slist, executor_kw) + suffixes = self.src_suffixes(env) else: - executor.add_sources(slist) - return executor + suffixes = [] + return match_splitext(path, suffixes) def _adjustixes(self, files, pre, suf, ensure_suffix=False): if not files: @@ -566,11 +558,37 @@ class BuilderBase: # The targets are fine, so find or make the appropriate Executor to # build this particular list of targets from this particular list of # sources. + + executor = None + key = None + if self.multi: - get_executor = self.get_multi_executor - else: - get_executor = self.get_single_executor - executor = get_executor(env, tlist, slist, executor_kw) + try: + executor = tlist[0].get_executor(create = 0) + except (AttributeError, IndexError): + pass + else: + executor.add_sources(slist) + + if executor is None: + if not self.action: + fmt = "Builder %s must have an action to build %s." + raise UserError, fmt % (self.get_name(env or self.env), + map(str,tlist)) + key = self.action.batch_key(env or self.env, tlist, slist) + if key: + try: + executor = SCons.Executor.GetBatchExecutor(key) + except KeyError: + pass + else: + executor.add_batch(tlist, slist) + + if executor is None: + executor = SCons.Executor.Executor(self.action, env, [], + tlist, slist, executor_kw) + if key: + SCons.Executor.AddBatchExecutor(key, executor) # Now set up the relevant information in the target Nodes themselves. for t in tlist: diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py index eeb3b3f1..91dd82a8 100644 --- a/src/engine/SCons/BuilderTests.py +++ b/src/engine/SCons/BuilderTests.py @@ -406,25 +406,6 @@ class BuilderTestCase(unittest.TestCase): """Test the get_name() method """ - def test_get_single_executor(self): - """Test the get_single_executor() method - """ - b = SCons.Builder.Builder(action='foo') - x = b.get_single_executor({}, [], [], {}) - assert not x is None, x - - def test_get_multi_executor(self): - """Test the get_multi_executor() method - """ - b = SCons.Builder.Builder(action='foo', multi=1) - t1 = MyNode('t1') - s1 = MyNode('s1') - s2 = MyNode('s2') - x1 = b.get_multi_executor({}, [t1], [s1], {}) - t1.executor = x1 - x2 = b.get_multi_executor({}, [t1], [s2], {}) - assert x1 is x2, "%s is not %s" % (repr(x1), repr(x2)) - def test_cmp(self): """Test simple comparisons of Builder objects """ @@ -1472,8 +1453,13 @@ class BuilderTestCase(unittest.TestCase): assert b5.get_name(None) == 'builder5', b5.get_name(None) assert b6.get_name(None) in b6_names, b6.get_name(None) - tgt = b4(env, target = 'moo', source='cow') - assert tgt[0].builder.get_name(env) == 'bldr4' + # This test worked before adding batch builders, but we must now + # be able to disambiguate a CompositeAction into a more specific + # action based on file suffix at call time. Leave this commented + # out (for now) in case this reflects a real-world use case that + # we must accomodate and we want to resurrect this test. + #tgt = b4(env, target = 'moo', source='cow') + #assert tgt[0].builder.get_name(env) == 'bldr4' class CompositeBuilderTestCase(unittest.TestCase): @@ -1517,12 +1503,11 @@ class CompositeBuilderTestCase(unittest.TestCase): builder = self.builder flag = 0 - tgt = builder(env, target='test3', source=['test2.bar', 'test1.foo'])[0] try: - tgt.build() + builder(env, target='test3', source=['test2.bar', 'test1.foo'])[0] except SCons.Errors.UserError, e: flag = 1 - assert flag, "UserError should be thrown when we build targets with files of different suffixes." + assert flag, "UserError should be thrown when we call a builder with files of different suffixes." expect = "While building `['test3']' from `test1.foo': Cannot build multiple sources with different extensions: .bar, .foo" assert str(e) == expect, e @@ -1558,12 +1543,11 @@ class CompositeBuilderTestCase(unittest.TestCase): env['FOO_SUFFIX'] = '.BAR2' builder.add_action('$NEW_SUFFIX', func_action) flag = 0 - tgt = builder(env, target='test5', source=['test5.BAR2'])[0] try: - tgt.build() + builder(env, target='test5', source=['test5.BAR2'])[0] except SCons.Errors.UserError: flag = 1 - assert flag, "UserError should be thrown when we build targets with ambigous suffixes." + assert flag, "UserError should be thrown when we call a builder with ambigous suffixes." def test_src_builder(self): """Test CompositeBuilder's use of a src_builder""" @@ -1603,52 +1587,47 @@ class CompositeBuilderTestCase(unittest.TestCase): assert isinstance(tgt.builder, SCons.Builder.BuilderBase) flag = 0 - tgt = builder(env, target='t5', source=['test5a.foo', 'test5b.inb'])[0] try: - tgt.build() + builder(env, target='t5', source=['test5a.foo', 'test5b.inb'])[0] except SCons.Errors.UserError, e: flag = 1 - assert flag, "UserError should be thrown when we build targets with files of different suffixes." + assert flag, "UserError should be thrown when we call a builder with files of different suffixes." expect = "While building `['t5']' from `test5b.bar': Cannot build multiple sources with different extensions: .foo, .bar" assert str(e) == expect, e flag = 0 - tgt = builder(env, target='t6', source=['test6a.bar', 'test6b.ina'])[0] try: - tgt.build() + builder(env, target='t6', source=['test6a.bar', 'test6b.ina'])[0] except SCons.Errors.UserError, e: flag = 1 - assert flag, "UserError should be thrown when we build targets with files of different suffixes." + assert flag, "UserError should be thrown when we call a builder with files of different suffixes." expect = "While building `['t6']' from `test6b.foo': Cannot build multiple sources with different extensions: .bar, .foo" assert str(e) == expect, e flag = 0 - tgt = builder(env, target='t4', source=['test4a.ina', 'test4b.inb'])[0] try: - tgt.build() + builder(env, target='t4', source=['test4a.ina', 'test4b.inb'])[0] except SCons.Errors.UserError, e: flag = 1 - assert flag, "UserError should be thrown when we build targets with files of different suffixes." + assert flag, "UserError should be thrown when we call a builder with files of different suffixes." expect = "While building `['t4']' from `test4b.bar': Cannot build multiple sources with different extensions: .foo, .bar" assert str(e) == expect, e flag = 0 - tgt = builder(env, target='t7', source=[env.fs.File('test7')])[0] try: - tgt.build() + builder(env, target='t7', source=[env.fs.File('test7')])[0] except SCons.Errors.UserError, e: flag = 1 - assert flag, "UserError should be thrown when we build targets with files of different suffixes." + assert flag, "UserError should be thrown when we call a builder with files of different suffixes." expect = "While building `['t7']': Cannot deduce file extension from source files: ['test7']" assert str(e) == expect, e flag = 0 - tgt = builder(env, target='t8', source=['test8.unknown'])[0] try: - tgt.build() + builder(env, target='t8', source=['test8.unknown'])[0] except SCons.Errors.UserError, e: flag = 1 - assert flag, "UserError should be thrown when we build a target with an unknown suffix." + assert flag, "UserError should be thrown when we call a builder target with an unknown suffix." expect = "While building `['t8']' from `['test8.unknown']': Don't know how to build from a source file with suffix `.unknown'. Expected a suffix in this list: ['.foo', '.bar']." assert str(e) == expect, e diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py index 338ed37f..a92a23d4 100644 --- a/src/engine/SCons/Environment.py +++ b/src/engine/SCons/Environment.py @@ -109,19 +109,18 @@ def apply_tools(env, tools, toolpath): # set or override them. This warning can optionally be turned off, # but scons will still ignore the illegal variable names even if it's off. reserved_construction_var_names = [ + 'CHANGED_SOURCES', + 'CHANGED_TARGETS', 'SOURCE', 'SOURCES', 'TARGET', 'TARGETS', -] - -future_reserved_construction_var_names = [ - 'CHANGED_SOURCES', - 'CHANGED_TARGETS', 'UNCHANGED_SOURCES', 'UNCHANGED_TARGETS', ] +future_reserved_construction_var_names = [] + def copy_non_reserved_keywords(dict): result = semi_deepcopy(dict) for k in result.keys(): @@ -490,7 +489,7 @@ class SubstitutionEnvironment: def lvars(self): return {} - def subst(self, string, raw=0, target=None, source=None, conv=None): + def subst(self, string, raw=0, target=None, source=None, conv=None, executor=None): """Recursively interpolates construction variables from the Environment into the specified string, returning the expanded result. Construction variables are specified by a $ prefix @@ -503,6 +502,8 @@ class SubstitutionEnvironment: gvars = self.gvars() lvars = self.lvars() lvars['__env__'] = self + if executor: + lvars.update(executor.get_lvars()) return SCons.Subst.scons_subst(string, self, raw, target, source, gvars, lvars, conv) def subst_kw(self, kw, raw=0, target=None, source=None): @@ -514,12 +515,14 @@ class SubstitutionEnvironment: nkw[k] = v return nkw - def subst_list(self, string, raw=0, target=None, source=None, conv=None): + def subst_list(self, string, raw=0, target=None, source=None, conv=None, executor=None): """Calls through to SCons.Subst.scons_subst_list(). See the documentation for that function.""" gvars = self.gvars() lvars = self.lvars() lvars['__env__'] = self + if executor: + lvars.update(executor.get_lvars()) return SCons.Subst.scons_subst_list(string, self, raw, target, source, gvars, lvars, conv) def subst_path(self, path, target=None, source=None): diff --git a/src/engine/SCons/Environment.xml b/src/engine/SCons/Environment.xml index a8611052..d63d20fd 100644 --- a/src/engine/SCons/Environment.xml +++ b/src/engine/SCons/Environment.xml @@ -115,6 +115,22 @@ below, for more information. + + +A reserved variable name +that may not be set or used in a construction environment. +(See "Variable Substitution," below.) + + + + + +A reserved variable name +that may not be set or used in a construction environment. +(See "Variable Substitution," below.) + + + A reserved variable name @@ -147,6 +163,22 @@ that may not be set or used in a construction environment. + + +A reserved variable name +that may not be set or used in a construction environment. +(See "Variable Substitution," below.) + + + + + +A reserved variable name +that may not be set or used in a construction environment. +(See "Variable Substitution," below.) + + + A list of the names of the Tool specifications diff --git a/src/engine/SCons/EnvironmentTests.py b/src/engine/SCons/EnvironmentTests.py index 0ea9dda1..77d46026 100644 --- a/src/engine/SCons/EnvironmentTests.py +++ b/src/engine/SCons/EnvironmentTests.py @@ -172,6 +172,7 @@ class TestEnvironmentFixture: suffix = '.o', single_source = 1) kw['BUILDERS'] = {'Object' : static_obj} + static_obj.add_action('.cpp', 'fake action') env = apply(Environment, args, kw) return env @@ -887,6 +888,17 @@ sys.exit(0) class BaseTestCase(unittest.TestCase,TestEnvironmentFixture): + reserved_variables = [ + 'CHANGED_SOURCES', + 'CHANGED_TARGETS', + 'SOURCE', + 'SOURCES', + 'TARGET', + 'TARGETS', + 'UNCHANGED_SOURCES', + 'UNCHANGED_TARGETS', + ] + def test___init__(self): """Test construction Environment creation @@ -1123,10 +1135,14 @@ env4.builder1.env, env3) """Test warning generation when reserved variable names are set""" reserved_variables = [ + 'CHANGED_SOURCES', + 'CHANGED_TARGETS', 'SOURCE', 'SOURCES', 'TARGET', 'TARGETS', + 'UNCHANGED_SOURCES', + 'UNCHANGED_TARGETS', ] warning = SCons.Warnings.ReservedVariableWarning @@ -1135,7 +1151,7 @@ env4.builder1.env, env3) try: env4 = Environment() - for kw in reserved_variables: + for kw in self.reserved_variables: exc_caught = None try: env4[kw] = 'xyzzy' @@ -1149,12 +1165,7 @@ env4.builder1.env, env3) def test_FutureReservedVariables(self): """Test warning generation when future reserved variable names are set""" - future_reserved_variables = [ - 'CHANGED_SOURCES', - 'CHANGED_TARGETS', - 'UNCHANGED_SOURCES', - 'UNCHANGED_TARGETS', - ] + future_reserved_variables = [] warning = SCons.Warnings.FutureReservedVariableWarning SCons.Warnings.enableWarningClass(warning) @@ -3365,19 +3376,22 @@ def generate(env): f = env.xxx('$FOO') assert f == 'foo', f - def test_bad_keywords(type): + def test_bad_keywords(self): """Test trying to use reserved keywords in an Environment""" - reserved = ['TARGETS','SOURCES', 'SOURCE','TARGET'] added = [] - env = type.TestEnvironment(TARGETS = 'targets', + env = self.TestEnvironment(TARGETS = 'targets', SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', INIT = 'init') bad_msg = '%s is not reserved, but got omitted; see Environment.construction_var_name_ok' added.append('INIT') - for x in reserved: + for x in self.reserved_variables: assert not env.has_key(x), env[x] for x in added: assert env.has_key(x), bad_msg % x @@ -3386,9 +3400,13 @@ def generate(env): SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', APPEND = 'append') added.append('APPEND') - for x in reserved: + for x in self.reserved_variables: assert not env.has_key(x), env[x] for x in added: assert env.has_key(x), bad_msg % x @@ -3397,9 +3415,13 @@ def generate(env): SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', APPENDUNIQUE = 'appendunique') added.append('APPENDUNIQUE') - for x in reserved: + for x in self.reserved_variables: assert not env.has_key(x), env[x] for x in added: assert env.has_key(x), bad_msg % x @@ -3408,9 +3430,13 @@ def generate(env): SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', PREPEND = 'prepend') added.append('PREPEND') - for x in reserved: + for x in self.reserved_variables: assert not env.has_key(x), env[x] for x in added: assert env.has_key(x), bad_msg % x @@ -3419,9 +3445,13 @@ def generate(env): SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', PREPENDUNIQUE = 'prependunique') added.append('PREPENDUNIQUE') - for x in reserved: + for x in self.reserved_variables: assert not env.has_key(x), env[x] for x in added: assert env.has_key(x), bad_msg % x @@ -3430,9 +3460,13 @@ def generate(env): SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', REPLACE = 'replace') added.append('REPLACE') - for x in reserved: + for x in self.reserved_variables: assert not env.has_key(x), env[x] for x in added: assert env.has_key(x), bad_msg % x @@ -3441,8 +3475,12 @@ def generate(env): SOURCES = 'sources', SOURCE = 'source', TARGET = 'target', + CHANGED_SOURCES = 'changed_sources', + CHANGED_TARGETS = 'changed_targets', + UNCHANGED_SOURCES = 'unchanged_sources', + UNCHANGED_TARGETS = 'unchanged_targets', COPY = 'copy') - for x in reserved: + for x in self.reserved_variables: assert not copy.has_key(x), env[x] for x in added + ['COPY']: assert copy.has_key(x), bad_msg % x @@ -3451,8 +3489,12 @@ def generate(env): 'SOURCES' : 'sources', 'SOURCE' : 'source', 'TARGET' : 'target', + 'CHANGED_SOURCES' : 'changed_sources', + 'CHANGED_TARGETS' : 'changed_targets', + 'UNCHANGED_SOURCES' : 'unchanged_sources', + 'UNCHANGED_TARGETS' : 'unchanged_targets', 'OVERRIDE' : 'override'}) - for x in reserved: + for x in self.reserved_variables: assert not over.has_key(x), over[x] for x in added + ['OVERRIDE']: assert over.has_key(x), bad_msg % x diff --git a/src/engine/SCons/Executor.py b/src/engine/SCons/Executor.py index 25db7711..7e831e33 100644 --- a/src/engine/SCons/Executor.py +++ b/src/engine/SCons/Executor.py @@ -31,12 +31,85 @@ Nodes. __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" import string +import UserList from SCons.Debug import logInstanceCreation import SCons.Errors import SCons.Memoize +class Batch: + """Remembers exact association between targets + and sources of executor.""" + def __init__(self, targets=[], sources=[]): + self.targets = targets + self.sources = sources + + + +class TSList(UserList.UserList): + """A class that implements $TARGETS or $SOURCES expansions by wrapping + an executor Method. This class is used in the Executor.lvars() + to delay creation of NodeList objects until they're needed. + + Note that we subclass UserList.UserList purely so that the + is_Sequence() function will identify an object of this class as + a list during variable expansion. We're not really using any + UserList.UserList methods in practice. + """ + def __init__(self, func): + self.func = func + def __getattr__(self, attr): + nl = self.func() + return getattr(nl, attr) + def __getitem__(self, i): + nl = self.func() + return nl[i] + def __getslice__(self, i, j): + nl = self.func() + i = max(i, 0); j = max(j, 0) + return nl[i:j] + def __str__(self): + nl = self.func() + return str(nl) + def __repr__(self): + nl = self.func() + return repr(nl) + +class TSObject: + """A class that implements $TARGET or $SOURCE expansions by wrapping + an Executor method. + """ + def __init__(self, func): + self.func = func + def __getattr__(self, attr): + n = self.func() + return getattr(n, attr) + def __str__(self): + n = self.func() + if n: + return str(n) + return '' + def __repr__(self): + n = self.func() + if n: + return repr(n) + return '' + +def rfile(node): + """ + A function to return the results of a Node's rfile() method, + if it exists, and the Node itself otherwise (if it's a Value + Node, e.g.). + """ + try: + rfile = node.rfile + except AttributeError: + return node + else: + return rfile() + + class Executor: """A class for controlling instances of executing an action. @@ -58,11 +131,96 @@ class Executor: self.post_actions = [] self.env = env self.overridelist = overridelist - self.targets = targets - self.sources = SCons.Util.UniqueList(sources[:]) + if targets or sources: + self.batches = [Batch(targets[:], sources[:])] + else: + self.batches = [] self.builder_kw = builder_kw self._memo = {} + def get_lvars(self): + try: + return self.lvars + except AttributeError: + self.lvars = { + 'CHANGED_SOURCES' : TSList(self._get_changed_sources), + 'CHANGED_TARGETS' : TSList(self._get_changed_targets), + 'SOURCE' : TSObject(self._get_source), + 'SOURCES' : TSList(self._get_sources), + 'TARGET' : TSObject(self._get_target), + 'TARGETS' : TSList(self._get_targets), + 'UNCHANGED_SOURCES' : TSList(self._get_unchanged_sources), + 'UNCHANGED_TARGETS' : TSList(self._get_unchanged_targets), + } + return self.lvars + + def _get_changes(self): + cs = [] + ct = [] + us = [] + ut = [] + for b in self.batches: + if b.targets[0].changed(): + cs.extend(map(rfile, b.sources)) + ct.extend(b.targets) + else: + us.extend(map(rfile, b.sources)) + ut.extend(b.targets) + self._changed_sources_list = SCons.Util.NodeList(cs) + self._changed_targets_list = SCons.Util.NodeList(ct) + self._unchanged_sources_list = SCons.Util.NodeList(us) + self._unchanged_targets_list = SCons.Util.NodeList(ut) + + def _get_changed_sources(self, *args, **kw): + try: + return self._changed_sources_list + except AttributeError: + self._get_changes() + return self._changed_sources_list + + def _get_changed_targets(self, *args, **kw): + try: + return self._changed_targets_list + except AttributeError: + self._get_changes() + return self._changed_targets_list + + def _get_source(self, *args, **kw): + #return SCons.Util.NodeList([rfile(self.batches[0].sources[0]).get_subst_proxy()]) + return rfile(self.batches[0].sources[0]).get_subst_proxy() + + def _get_sources(self, *args, **kw): + return SCons.Util.NodeList(map(lambda n: rfile(n).get_subst_proxy(), self.get_all_sources())) + + def _get_target(self, *args, **kw): + #return SCons.Util.NodeList([self.batches[0].targets[0].get_subst_proxy()]) + return self.batches[0].targets[0].get_subst_proxy() + + def _get_targets(self, *args, **kw): + return SCons.Util.NodeList(map(lambda n: n.get_subst_proxy(), self.get_all_targets())) + + def _get_unchanged_sources(self, *args, **kw): + try: + return self._unchanged_sources_list + except AttributeError: + self._get_changes() + return self._unchanged_sources_list + + def _get_unchanged_targets(self, *args, **kw): + try: + return self._unchanged_targets_list + except AttributeError: + self._get_changes() + return self._unchanged_targets_list + + def get_action_targets(self): + if not self.action_list: + return [] + targets_string = self.action_list[0].get_targets(self.env, self) + if targets_string[0] == '$': + targets_string = targets_string[1:] + return self.get_lvars()[targets_string] + def set_action_list(self, action): import SCons.Util if not SCons.Util.is_List(action): @@ -75,6 +233,58 @@ class Executor: def get_action_list(self): return self.pre_actions + self.action_list + self.post_actions + def get_all_targets(self): + """Returns all targets for all batches of this Executor.""" + result = [] + for batch in self.batches: + # TODO(1.5): remove the list() cast + result.extend(list(batch.targets)) + return result + + def get_all_sources(self): + """Returns all sources for all batches of this Executor.""" + result = [] + for batch in self.batches: + # TODO(1.5): remove the list() cast + result.extend(list(batch.sources)) + return result + + def get_all_children(self): + """Returns all unique children (dependencies) for all batches + of this Executor. + + The Taskmaster can recognize when it's already evaluated a + Node, so we don't have to make this list unique for its intended + canonical use case, but we expect there to be a lot of redundancy + (long lists of batched .cc files #including the same .h files + over and over), so removing the duplicates once up front should + save the Taskmaster a lot of work. + """ + result = SCons.Util.UniqueList([]) + for target in self.get_all_targets(): + result.extend(target.children()) + return result + + def get_all_prerequisites(self): + """Returns all unique (order-only) prerequisites for all batches + of this Executor. + """ + result = SCons.Util.UniqueList([]) + for target in self.get_all_targets(): + # TODO(1.5): remove the list() cast + result.extend(list(target.prerequisites)) + return result + + def get_action_side_effects(self): + + """Returns all side effects for all batches of this + Executor used by the underlying Action. + """ + result = SCons.Util.UniqueList([]) + for target in self.get_action_targets(): + result.extend(target.side_effects) + return result + memoizer_counters.append(SCons.Memoize.CountValue('get_build_env')) def get_build_env(self): @@ -108,14 +318,17 @@ class Executor: """ env = self.get_build_env() try: - cwd = self.targets[0].cwd + cwd = self.batches[0].targets[0].cwd except (IndexError, AttributeError): cwd = None - return scanner.path(env, cwd, self.targets, self.get_sources()) + return scanner.path(env, cwd, + self.get_all_targets(), + self.get_all_sources()) def get_kw(self, kw={}): result = self.builder_kw.copy() result.update(kw) + result['executor'] = self return result def do_nothing(self, target, kw): @@ -127,7 +340,9 @@ class Executor: kw = self.get_kw(kw) status = 0 for act in self.get_action_list(): - status = apply(act, (self.targets, self.get_sources(), env), kw) + #args = (self.get_all_targets(), self.get_all_sources(), env) + args = ([], [], env) + status = apply(act, args, kw) if isinstance(status, SCons.Errors.BuildError): status.executor = self raise status @@ -135,7 +350,7 @@ class Executor: msg = "Error %s" % status raise SCons.Errors.BuildError( errstr=msg, - node=self.targets, + node=self.batches[0].targets, executor=self, action=act) return status @@ -154,20 +369,32 @@ class Executor: """Add source files to this Executor's list. This is necessary for "multi" Builders that can be called repeatedly to build up a source file list for a given target.""" - self.sources.extend(sources) + # TODO(batch): extend to multiple batches + assert (len(self.batches) == 1) + # TODO(batch): remove duplicates? + #slist = filter(lambda x, s=self.batches[0].sources: x not in s, sources) + self.batches[0].sources.extend(sources) def get_sources(self): - return self.sources + return self.batches[0].sources + + def add_batch(self, targets, sources): + """Add pair of associated target and source to this Executor's list. + This is necessary for "batch" Builders that can be called repeatedly + to build up a list of matching target and source files that will be + used in order to update multiple target files at once from multiple + corresponding source files, for tools like MSVC that support it.""" + self.batches.append(Batch(targets, sources)) def prepare(self): """ Preparatory checks for whether this Executor can go ahead and (try to) build its targets. """ - for s in self.get_sources(): + for s in self.get_all_sources(): if s.missing(): msg = "Source `%s' not found, needed by target `%s'." - raise SCons.Errors.StopError, msg % (s, self.targets[0]) + raise SCons.Errors.StopError, msg % (s, self.batches[0].targets[0]) def add_pre_action(self, action): self.pre_actions.append(action) @@ -179,7 +406,7 @@ class Executor: def my_str(self): env = self.get_build_env() - get = lambda action, t=self.targets, s=self.get_sources(), e=env: \ + get = lambda action, t=self.get_all_targets(), s=self.get_all_sources(), e=env: \ action.genstring(t, s, e) return string.join(map(get, self.get_action_list()), "\n") @@ -204,7 +431,7 @@ class Executor: except KeyError: pass env = self.get_build_env() - get = lambda action, t=self.targets, s=self.get_sources(), e=env: \ + get = lambda action, t=self.get_all_targets(), s=self.get_all_sources(), e=env: \ action.get_contents(t, s, e) result = string.join(map(get, self.get_action_list()), "") self._memo['get_contents'] = result @@ -218,11 +445,13 @@ class Executor: return 0 def scan_targets(self, scanner): - self.scan(scanner, self.targets) + # TODO(batch): scan by batches + self.scan(scanner, self.get_all_targets()) def scan_sources(self, scanner): - if self.sources: - self.scan(scanner, self.get_sources()) + # TODO(batch): scan by batches + if self.batches[0].sources: + self.scan(scanner, self.get_all_sources()) def scan(self, scanner, node_list): """Scan a list of this Executor's files (targets or sources) for @@ -232,6 +461,7 @@ class Executor: """ env = self.get_build_env() + # TODO(batch): scan by batches) deps = [] if scanner: for node in node_list: @@ -256,16 +486,16 @@ class Executor: deps.extend(self.get_implicit_deps()) - for tgt in self.targets: + for tgt in self.get_all_targets(): tgt.add_to_implicit(deps) - def _get_unignored_sources_key(self, ignore=()): - return tuple(ignore) + def _get_unignored_sources_key(self, node, ignore=()): + return (node,) + tuple(ignore) memoizer_counters.append(SCons.Memoize.CountDict('get_unignored_sources', _get_unignored_sources_key)) - def get_unignored_sources(self, ignore=()): - ignore = tuple(ignore) + def get_unignored_sources(self, node, ignore=()): + key = (node,) + tuple(ignore) try: memo_dict = self._memo['get_unignored_sources'] except KeyError: @@ -273,56 +503,56 @@ class Executor: self._memo['get_unignored_sources'] = memo_dict else: try: - return memo_dict[ignore] + return memo_dict[key] except KeyError: pass - sourcelist = self.get_sources() + if node: + # TODO: better way to do this (it's a linear search, + # but it may not be critical path)? + sourcelist = [] + for b in self.batches: + if node in b.targets: + sourcelist = b.sources + break + else: + sourcelist = self.get_all_sources() if ignore: idict = {} for i in ignore: idict[i] = 1 sourcelist = filter(lambda s, i=idict: not i.has_key(s), sourcelist) - memo_dict[ignore] = sourcelist + memo_dict[key] = sourcelist return sourcelist - def _process_sources_key(self, func, ignore=()): - return (func, tuple(ignore)) - - memoizer_counters.append(SCons.Memoize.CountDict('process_sources', _process_sources_key)) - - def process_sources(self, func, ignore=()): - memo_key = (func, tuple(ignore)) - try: - memo_dict = self._memo['process_sources'] - except KeyError: - memo_dict = {} - self._memo['process_sources'] = memo_dict - else: - try: - return memo_dict[memo_key] - except KeyError: - pass - - result = map(func, self.get_unignored_sources(ignore)) - - memo_dict[memo_key] = result - - return result - def get_implicit_deps(self): """Return the executor's implicit dependencies, i.e. the nodes of the commands to be executed.""" result = [] build_env = self.get_build_env() for act in self.get_action_list(): - result.extend(act.get_implicit_deps(self.targets, self.get_sources(), build_env)) + deps = act.get_implicit_deps(self.get_all_targets(), + self.get_all_sources(), + build_env) + result.extend(deps) return result + + +_batch_executors = {} + +def GetBatchExecutor(key): + return _batch_executors[key] + +def AddBatchExecutor(key, executor): + assert not _batch_executors.has_key(key) + _batch_executors[key] = executor + nullenv = None + def get_NullEnvironment(): """Use singleton pattern for Null Environments.""" global nullenv @@ -349,7 +579,7 @@ class Null: """ def __init__(self, *args, **kw): if __debug__: logInstanceCreation(self, 'Executor.Null') - self.targets = kw['targets'] + self.batches = [Batch(kw['targets'][:], [])] def get_build_env(self): return get_NullEnvironment() def get_build_scanner_path(self): @@ -360,17 +590,30 @@ class Null: pass def get_unignored_sources(self, *args, **kw): return tuple(()) + def get_action_targets(self): + return [] def get_action_list(self): return [] + def get_all_targets(self): + return self.batches[0].targets + def get_all_sources(self): + return self.batches[0].targets[0].sources + def get_all_children(self): + return self.get_all_sources() + def get_all_prerequisites(self): + return [] + def get_action_side_effects(self): + return [] def __call__(self, *args, **kw): return 0 def get_contents(self): return '' - def _morph(self): """Morph this Null executor to a real Executor object.""" + batches = self.batches self.__class__ = Executor - self.__init__([], targets=self.targets) + self.__init__([]) + self.batches = batches # The following methods require morphing this Null Executor to a # real Executor object. diff --git a/src/engine/SCons/ExecutorTests.py b/src/engine/SCons/ExecutorTests.py index 0fd11af6..79e9d03b 100644 --- a/src/engine/SCons/ExecutorTests.py +++ b/src/engine/SCons/ExecutorTests.py @@ -110,9 +110,11 @@ class ExecutorTestCase(unittest.TestCase): assert x.action_list == ['a'], x.action_list assert x.env == 'e', x.env assert x.overridelist == ['o'], x.overridelist - assert x.targets == 't', x.targets + targets = x.get_all_targets() + assert targets == ['t'], targets source_list.append('s3') - assert x.sources == ['s1', 's2'], x.sources + sources = x.get_all_sources() + assert sources == ['s1', 's2'], sources try: x = SCons.Executor.Executor(None, 'e', ['o'], 't', source_list) except SCons.Errors.UserError: @@ -200,11 +202,11 @@ class ExecutorTestCase(unittest.TestCase): ['s1', 's2'], builder_kw={'X':1, 'Y':2}) kw = x.get_kw() - assert kw == {'X':1, 'Y':2}, kw + assert kw == {'X':1, 'Y':2, 'executor':x}, kw kw = x.get_kw({'Z':3}) - assert kw == {'X':1, 'Y':2, 'Z':3}, kw + assert kw == {'X':1, 'Y':2, 'Z':3, 'executor':x}, kw kw = x.get_kw({'X':4}) - assert kw == {'X':4, 'Y':2}, kw + assert kw == {'X':4, 'Y':2, 'executor':x}, kw def test__call__(self): """Test calling an Executor""" @@ -222,7 +224,7 @@ class ExecutorTestCase(unittest.TestCase): a = MyAction([action1, action2]) t = MyNode('t') - x = SCons.Executor.Executor(a, env, [], t, ['s1', 's2']) + x = SCons.Executor.Executor(a, env, [], [t], ['s1', 's2']) x.add_pre_action(pre) x.add_post_action(post) x(t) @@ -233,7 +235,7 @@ class ExecutorTestCase(unittest.TestCase): result.append('pre_err') return 1 - x = SCons.Executor.Executor(a, env, [], t, ['s1', 's2']) + x = SCons.Executor.Executor(a, env, [], [t], ['s1', 's2']) x.add_pre_action(pre_err) x.add_post_action(post) try: @@ -268,22 +270,30 @@ class ExecutorTestCase(unittest.TestCase): def test_add_sources(self): """Test adding sources to an Executor""" x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2']) - assert x.sources == ['s1', 's2'], x.sources + sources = x.get_all_sources() + assert sources == ['s1', 's2'], sources + x.add_sources(['s1', 's2']) - assert x.sources == ['s1', 's2'], x.sources + sources = x.get_all_sources() + assert sources == ['s1', 's2', 's1', 's2'], sources + x.add_sources(['s3', 's1', 's4']) - assert x.sources == ['s1', 's2', 's3', 's4'], x.sources + sources = x.get_all_sources() + assert sources == ['s1', 's2', 's1', 's2', 's3', 's1', 's4'], sources def test_get_sources(self): """Test getting sources from an Executor""" x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2']) - assert x.sources == ['s1', 's2'], x.sources + sources = x.get_sources() + assert sources == ['s1', 's2'], sources + x.add_sources(['s1', 's2']) - x.get_sources() - assert x.sources == ['s1', 's2'], x.sources + sources = x.get_sources() + assert sources == ['s1', 's2', 's1', 's2'], sources + x.add_sources(['s3', 's1', 's4']) - x.get_sources() - assert x.sources == ['s1', 's2', 's3', 's4'], x.sources + sources = x.get_sources() + assert sources == ['s1', 's2', 's1', 's2', 's3', 's1', 's4'], sources def test_prepare(self): """Test the Executor's prepare() method""" @@ -429,34 +439,15 @@ class ExecutorTestCase(unittest.TestCase): s3 = MyNode('s3') x = SCons.Executor.Executor('b', env, [{}], [], [s1, s2, s3]) - r = x.get_unignored_sources([]) + r = x.get_unignored_sources(None, []) assert r == [s1, s2, s3], map(str, r) - r = x.get_unignored_sources([s2]) + r = x.get_unignored_sources(None, [s2]) assert r == [s1, s3], map(str, r) - r = x.get_unignored_sources([s1, s3]) + r = x.get_unignored_sources(None, [s1, s3]) assert r == [s2], map(str, r) - def test_process_sources(self): - """Test processing the source list through a function""" - env = MyEnvironment() - s1 = MyNode('s1') - s2 = MyNode('s2') - s3 = MyNode('s3') - x = SCons.Executor.Executor('b', env, [{}], [], [s1, s2, s3]) - - r = x.process_sources(str) - assert r == ['s1', 's2', 's3'], r - - r = x.process_sources(str, [s2]) - assert r == ['s1', 's3'], r - - def xxx(x): - return 'xxx-' + str(x) - r = x.process_sources(xxx, [s1, s3]) - assert r == ['xxx-s2'], r - if __name__ == "__main__": diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index f8911b00..9da9d8e9 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -966,6 +966,9 @@ class Entry(Base): def _glob1(self, pattern, ondisk=True, source=False, strings=False): return self.disambiguate()._glob1(pattern, ondisk, source, strings) + def get_subst_proxy(self): + return self.disambiguate().get_subst_proxy() + # This is for later so we can differentiate between Entry the class and Entry # the method of the FS class. _classEntry = Entry diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 37dc465b..e2a1e9e0 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -304,7 +304,10 @@ class VariantDirTestCase(unittest.TestCase): class MkdirAction(Action): def __init__(self, dir_made): self.dir_made = dir_made - def __call__(self, target, source, env): + def __call__(self, target, source, env, executor=None): + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() self.dir_made.extend(target) save_Link = SCons.Node.FS.Link @@ -3060,7 +3063,10 @@ class prepareTestCase(unittest.TestCase): class MkdirAction(Action): def __init__(self, dir_made): self.dir_made = dir_made - def __call__(self, target, source, env): + def __call__(self, target, source, env, executor=None): + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() self.dir_made.extend(target) dir_made = [] @@ -3338,7 +3344,8 @@ class SpecialAttrTestCase(unittest.TestCase): assert s == os.path.normpath('baz/sub/file.suffix'), s assert f.srcpath.is_literal(), f.srcpath g = f.srcpath.get() - assert isinstance(g, SCons.Node.FS.Entry), g.__class__ + # Gets disambiguated to SCons.Node.FS.File by get_subst_proxy(). + assert isinstance(g, SCons.Node.FS.File), g.__class__ s = str(f.srcdir) assert s == os.path.normpath('baz/sub'), s @@ -3372,7 +3379,8 @@ class SpecialAttrTestCase(unittest.TestCase): try: fs.Entry('eee').get_subst_proxy().no_such_attr except AttributeError, e: - assert str(e) == "Entry instance 'eee' has no attribute 'no_such_attr'", e + # Gets disambiguated to File instance by get_subst_proxy(). + assert str(e) == "File instance 'eee' has no attribute 'no_such_attr'", e caught = 1 assert caught, "did not catch expected AttributeError" diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py index 8bceaf65..73e1e057 100644 --- a/src/engine/SCons/Node/NodeTests.py +++ b/src/engine/SCons/Node/NodeTests.py @@ -67,8 +67,11 @@ class MyAction(MyActionBase): def __init__(self): self.order = 0 - def __call__(self, target, source, env): + def __call__(self, target, source, env, executor=None): global built_it, built_target, built_source, built_args, built_order + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() built_it = 1 built_target = target built_source = source diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 871efff3..c44525f3 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -621,7 +621,7 @@ class Node: # essentially short-circuits an N*M scan of the # sources for each individual target, which is a hell # of a lot more efficient. - for tgt in executor.targets: + for tgt in executor.get_all_targets(): tgt.add_to_implicit(implicit) if implicit_deps_unchanged or self.is_up_to_date(): @@ -714,7 +714,7 @@ class Node: if s not in ignore_set: sources.append(s) else: - sources = executor.get_unignored_sources(self.ignore) + sources = executor.get_unignored_sources(self, self.ignore) seen = set() bsources = [] bsourcesigs = [] diff --git a/src/engine/SCons/SConfTests.py b/src/engine/SCons/SConfTests.py index 99744850..4fc657e3 100644 --- a/src/engine/SCons/SConfTests.py +++ b/src/engine/SCons/SConfTests.py @@ -219,10 +219,11 @@ class SConfTestCase(unittest.TestCase): pass def get_executor(self): class Executor: - pass - e = Executor() - e.targets = [self] - return e + def __init__(self, targets): + self.targets = targets + def get_all_targets(self): + return self.targets + return Executor([self]) return [MyNode('n1'), MyNode('n2')] try: self.scons_env.Append(BUILDERS = {'SConfActionBuilder' : MyBuilder()}) diff --git a/src/engine/SCons/Subst.py b/src/engine/SCons/Subst.py index fc9d7d96..6459eeca 100644 --- a/src/engine/SCons/Subst.py +++ b/src/engine/SCons/Subst.py @@ -256,6 +256,9 @@ class Target_or_Source: class NullNodeList(SCons.Util.NullSeq): def __call__(self, *args, **kwargs): return '' def __str__(self): return '' + # TODO(1.5): unneeded after new-style classes introduce iterators + def __getitem__(self, i): + raise IndexError NullNodesList = NullNodeList() @@ -285,6 +288,13 @@ def subst_dict(target, source): tnl = NLWrapper(target, get_tgt_subst_proxy) dict['TARGETS'] = Targets_or_Sources(tnl) dict['TARGET'] = Target_or_Source(tnl) + + # This is a total cheat, but hopefully this dictionary goes + # away soon anyway. We just let these expand to $TARGETS + # because that's "good enough" for the use of ToolSurrogates + # (see test/ToolSurrogate.py) to generate documentation. + dict['CHANGED_TARGETS'] = '$TARGETS' + dict['UNCHANGED_TARGETS'] = '$TARGETS' else: dict['TARGETS'] = NullNodesList dict['TARGET'] = NullNodesList @@ -304,6 +314,13 @@ def subst_dict(target, source): snl = NLWrapper(source, get_src_subst_proxy) dict['SOURCES'] = Targets_or_Sources(snl) dict['SOURCE'] = Target_or_Source(snl) + + # This is a total cheat, but hopefully this dictionary goes + # away soon anyway. We just let these expand to $TARGETS + # because that's "good enough" for the use of ToolSurrogates + # (see test/ToolSurrogate.py) to generate documentation. + dict['CHANGED_SOURCES'] = '$SOURCES' + dict['UNCHANGED_SOURCES'] = '$SOURCES' else: dict['SOURCES'] = NullNodesList dict['SOURCE'] = NullNodesList @@ -392,11 +409,9 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={ source with two methods (substitute() and expand()) that handle the expansion. """ - def __init__(self, env, mode, target, source, conv, gvars): + def __init__(self, env, mode, conv, gvars): self.env = env self.mode = mode - self.target = target - self.source = source self.conv = conv self.gvars = gvars @@ -433,14 +448,14 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={ except Exception, e: if e.__class__ in AllowableExceptions: return '' - raise_exception(e, self.target, s) + raise_exception(e, lvars['TARGETS'], s) else: if lvars.has_key(key): s = lvars[key] elif self.gvars.has_key(key): s = self.gvars[key] elif not NameError in AllowableExceptions: - raise_exception(NameError(key), self.target, s) + raise_exception(NameError(key), lvars['TARGETS'], s) else: return '' @@ -466,8 +481,8 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={ return map(func, s) elif callable(s): try: - s = s(target=self.target, - source=self.source, + s = s(target=lvars['TARGETS'], + source=lvars['SOURCES'], env=self.env, for_signature=(self.mode != SUBST_CMD)) except TypeError: @@ -525,10 +540,11 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={ # If we dropped that behavior (or found another way to cover it), # we could get rid of this call completely and just rely on the # Executor setting the variables. - d = subst_dict(target, source) - if d: - lvars = lvars.copy() - lvars.update(d) + if not lvars.has_key('TARGET'): + d = subst_dict(target, source) + if d: + lvars = lvars.copy() + lvars.update(d) # We're (most likely) going to eval() things. If Python doesn't # find a __builtins__ value in the global dictionary used for eval(), @@ -538,7 +554,7 @@ def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={ # for expansion. gvars['__builtins__'] = __builtins__ - ss = StringSubber(env, mode, target, source, conv, gvars) + ss = StringSubber(env, mode, conv, gvars) result = ss.substitute(strSubst, lvars) try: @@ -595,12 +611,10 @@ def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gv and the rest of the object takes care of doing the right thing internally. """ - def __init__(self, env, mode, target, source, conv, gvars): + def __init__(self, env, mode, conv, gvars): UserList.UserList.__init__(self, []) self.env = env self.mode = mode - self.target = target - self.source = source self.conv = conv self.gvars = gvars @@ -649,14 +663,14 @@ def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gv except Exception, e: if e.__class__ in AllowableExceptions: return - raise_exception(e, self.target, s) + raise_exception(e, lvars['TARGETS'], s) else: if lvars.has_key(key): s = lvars[key] elif self.gvars.has_key(key): s = self.gvars[key] elif not NameError in AllowableExceptions: - raise_exception(NameError(), self.target, s) + raise_exception(NameError(), lvars['TARGETS'], s) else: return @@ -676,8 +690,8 @@ def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gv self.next_word() elif callable(s): try: - s = s(target=self.target, - source=self.source, + s = s(target=lvars['TARGETS'], + source=lvars['SOURCES'], env=self.env, for_signature=(self.mode != SUBST_CMD)) except TypeError: @@ -820,10 +834,11 @@ def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gv # If we dropped that behavior (or found another way to cover it), # we could get rid of this call completely and just rely on the # Executor setting the variables. - d = subst_dict(target, source) - if d: - lvars = lvars.copy() - lvars.update(d) + if not lvars.has_key('TARGET'): + d = subst_dict(target, source) + if d: + lvars = lvars.copy() + lvars.update(d) # We're (most likely) going to eval() things. If Python doesn't # find a __builtins__ value in the global dictionary used for eval(), @@ -833,7 +848,7 @@ def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gv # for expansion. gvars['__builtins__'] = __builtins__ - ls = ListSubber(env, mode, target, source, conv, gvars) + ls = ListSubber(env, mode, conv, gvars) ls.substitute(strSubst, lvars, 0) try: diff --git a/src/engine/SCons/Taskmaster.py b/src/engine/SCons/Taskmaster.py index 934e28b8..42454801 100644 --- a/src/engine/SCons/Taskmaster.py +++ b/src/engine/SCons/Taskmaster.py @@ -186,8 +186,9 @@ class Task: # target t.prepare() methods check that each target's explicit # or implicit dependencies exists, and also initialize the # .sconsign info. - self.targets[0].get_executor().prepare() - for t in self.targets: + executor = self.targets[0].get_executor() + executor.prepare() + for t in executor.get_action_targets(): t.prepare() for s in t.side_effects: s.prepare() @@ -763,8 +764,10 @@ class Taskmaster: if T: T.write(self.trace_message(' already handled (executed)')) continue + executor = node.get_executor() + try: - children = node.children() + children = executor.get_all_children() except SystemExit: exc_value = sys.exc_info()[1] e = SCons.Errors.ExplicitExit(node, exc_value.code) @@ -786,7 +789,7 @@ class Taskmaster: children_not_ready = [] children_failed = False - for child in chain(children,node.prerequisites): + for child in chain(children, executor.get_all_prerequisites()): childstate = child.get_state() if T: T.write(self.trace_message(' ' + self.trace_node(child))) @@ -830,7 +833,8 @@ class Taskmaster: # added the other children to the list of candidate nodes # to keep on building (--keep-going). if children_failed: - node.set_state(NODE_FAILED) + for n in executor.get_action_targets(): + n.set_state(NODE_FAILED) if S: S.child_failed = S.child_failed + 1 if T: T.write(self.trace_message('****** %s\n' % self.trace_node(node))) @@ -861,7 +865,7 @@ class Taskmaster: # Skip this node if it has side-effects that are # currently being built: wait_side_effects = False - for se in node.side_effects: + for se in executor.get_action_side_effects(): if se.get_state() == NODE_EXECUTING: se.add_to_waiting_s_e(node) wait_side_effects = True @@ -900,7 +904,7 @@ class Taskmaster: if node is None: return None - tlist = node.get_executor().targets + tlist = node.get_executor().get_all_targets() task = self.tasker(self, tlist, node in self.original_top, node) try: diff --git a/src/engine/SCons/TaskmasterTests.py b/src/engine/SCons/TaskmasterTests.py index b36e4aab..c8bbdf46 100644 --- a/src/engine/SCons/TaskmasterTests.py +++ b/src/engine/SCons/TaskmasterTests.py @@ -166,8 +166,21 @@ class Node: class Executor: def prepare(self): pass + def get_action_targets(self): + return self.targets + def get_all_targets(self): + return self.targets + def get_all_children(self): + result = [] + for node in self.targets: + result.extend(node.children()) + return result + def get_all_prerequisites(self): + return [] + def get_action_side_effects(self): + return [] self.executor = Executor() - self.executor.targets = self.targets + self.executor.targets = self.targets return self.executor class OtherError(Exception): @@ -752,7 +765,7 @@ class TaskmasterTestCase(unittest.TestCase): # set it up by having something that approximates a real Builder # return this list--but that's more work than is probably # warranted right now. - t.targets = [n1, n2] + n1.get_executor().targets = [n1, n2] t.prepare() assert n1.prepared assert n2.prepared @@ -763,7 +776,7 @@ class TaskmasterTestCase(unittest.TestCase): t = tm.next_task() # More bogus reaching in and setting the targets. n3.set_state(SCons.Node.up_to_date) - t.targets = [n3, n4] + n3.get_executor().targets = [n3, n4] t.prepare() assert n3.prepared assert n4.prepared @@ -803,7 +816,7 @@ class TaskmasterTestCase(unittest.TestCase): tm = SCons.Taskmaster.Taskmaster([n6, n7]) t = tm.next_task() # More bogus reaching in and setting the targets. - t.targets = [n6, n7] + n6.get_executor().targets = [n6, n7] t.prepare() assert n6.prepared assert n7.prepared @@ -815,9 +828,21 @@ class TaskmasterTestCase(unittest.TestCase): class ExceptionExecutor: def prepare(self): raise Exception, "Executor.prepare() exception" + def get_all_targets(self): + return self.nodes + def get_all_children(self): + result = [] + for node in self.nodes: + result.extend(node.children()) + return result + def get_all_prerequisites(self): + return [] + def get_action_side_effects(self): + return [] n11 = Node("n11") n11.executor = ExceptionExecutor() + n11.executor.nodes = [n11] tm = SCons.Taskmaster.Taskmaster([n11]) t = tm.next_task() try: diff --git a/src/engine/SCons/Tool/javah.py b/src/engine/SCons/Tool/javah.py index 7eb4969e..9b3c8c22 100644 --- a/src/engine/SCons/Tool/javah.py +++ b/src/engine/SCons/Tool/javah.py @@ -103,7 +103,7 @@ def emit_java_headers(target, source, env): def JavaHOutFlagGenerator(target, source, env, for_signature): try: t = target[0] - except (AttributeError, TypeError): + except (AttributeError, IndexError, TypeError): t = target try: return '-d ' + str(t.attributes.java_lookupdir) diff --git a/src/engine/SCons/Tool/mslink.py b/src/engine/SCons/Tool/mslink.py index 4268b58f..53cab28c 100644 --- a/src/engine/SCons/Tool/mslink.py +++ b/src/engine/SCons/Tool/mslink.py @@ -65,7 +65,10 @@ def _dllSources(target, source, env, for_signature, paramtp): deffile = env.FindIxes(source, "WINDOWSDEFPREFIX", "WINDOWSDEFSUFFIX") for src in source: - if src == deffile: + # Check explicitly for a non-None deffile so that the __cmp__ + # method of the base SCons.Util.Proxy class used for some Node + # proxies doesn't try to use a non-existent __dict__ attribute. + if deffile and src == deffile: # Treat this source as a .def file. listCmd.append("/def:%s" % src.get_string(for_signature)) else: diff --git a/src/engine/SCons/Tool/msvc.py b/src/engine/SCons/Tool/msvc.py index b324a323..0898b91c 100644 --- a/src/engine/SCons/Tool/msvc.py +++ b/src/engine/SCons/Tool/msvc.py @@ -672,40 +672,103 @@ res_builder = SCons.Builder.Builder(action=res_action, src_builder=[], source_scanner=res_scanner) +def msvc_batch_key(action, env, target, source): + """ + Returns a key to identify unique batches of sources for compilation. + + If batching is enabled (via the $MSVC_BATCH setting), then all + target+source pairs that use the same action, defined by the same + environment, and have the same target and source directories, will + be batched. + + Returning None specifies that the specified target+source should not + be batched with other compilations. + """ + b = env.subst('$MSVC_BATCH') + if b in (None, '', '0'): + # We're not using batching; return no key. + return None + t = target[0] + s = source[0] + if os.path.splitext(t.name)[0] != os.path.splitext(s.name)[0]: + # The base names are different, so this *must* be compiled + # separately; return no key. + return None + return (id(action), id(env), t.dir, s.dir) + +def msvc_output_flag(target, source, env, for_signature): + """ + Returns the correct /Fo flag for batching. + + If batching is disabled or there's only one source file, then we + return an /Fo string that specifies the target explicitly. Otherwise, + we return an /Fo string that just specifies the first target's + directory (where the Visual C/C++ compiler will put the .obj files). + """ + b = env.subst('$MSVC_BATCH') + if b in (None, '', '0') or len(source) == 1: + return '/Fo$TARGET' + else: + # The Visual C/C++ compiler requires a \ at the end of the /Fo + # option to indicate an output directory. We use os.sep here so + # that the test(s) for this can be run on non-Windows systems + # without having a hard-coded backslash mess up command-line + # argument parsing. + return '/Fo${TARGET.dir}' + os.sep + +CAction = SCons.Action.Action("$CCCOM", "$CCCOMSTR", + batch_key=msvc_batch_key, + targets='$CHANGED_TARGETS') +ShCAction = SCons.Action.Action("$SHCCCOM", "$SHCCCOMSTR", + batch_key=msvc_batch_key, + targets='$CHANGED_TARGETS') +CXXAction = SCons.Action.Action("$CXXCOM", "$CXXCOMSTR", + batch_key=msvc_batch_key, + targets='$CHANGED_TARGETS') +ShCXXAction = SCons.Action.Action("$SHCXXCOM", "$SHCXXCOMSTR", + batch_key=msvc_batch_key, + targets='$CHANGED_TARGETS') def generate(env): """Add Builders and construction variables for MSVC++ to an Environment.""" static_obj, shared_obj = SCons.Tool.createObjBuilders(env) + # TODO(batch): shouldn't reach in to cmdgen this way; necessary + # for now to bypass the checks in Builder.DictCmdGenerator.__call__() + # and allow .cc and .cpp to be compiled in the same command line. + static_obj.cmdgen.source_ext_match = False + shared_obj.cmdgen.source_ext_match = False + for suffix in CSuffixes: - static_obj.add_action(suffix, SCons.Defaults.CAction) - shared_obj.add_action(suffix, SCons.Defaults.ShCAction) + static_obj.add_action(suffix, CAction) + shared_obj.add_action(suffix, ShCAction) static_obj.add_emitter(suffix, static_object_emitter) shared_obj.add_emitter(suffix, shared_object_emitter) for suffix in CXXSuffixes: - static_obj.add_action(suffix, SCons.Defaults.CXXAction) - shared_obj.add_action(suffix, SCons.Defaults.ShCXXAction) + static_obj.add_action(suffix, CXXAction) + shared_obj.add_action(suffix, ShCXXAction) static_obj.add_emitter(suffix, static_object_emitter) shared_obj.add_emitter(suffix, shared_object_emitter) env['CCPDBFLAGS'] = SCons.Util.CLVar(['${(PDB and "/Z7") or ""}']) env['CCPCHFLAGS'] = SCons.Util.CLVar(['${(PCH and "/Yu%s /Fp%s"%(PCHSTOP or "",File(PCH))) or ""}']) + env['_MSVC_OUTPUT_FLAG'] = msvc_output_flag env['_CCCOMCOM'] = '$CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS $CCPCHFLAGS $CCPDBFLAGS' env['CC'] = 'cl' env['CCFLAGS'] = SCons.Util.CLVar('/nologo') env['CFLAGS'] = SCons.Util.CLVar('') - env['CCCOM'] = '$CC /Fo$TARGET /c $SOURCES $CFLAGS $CCFLAGS $_CCCOMCOM' + env['CCCOM'] = '$CC $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $CFLAGS $CCFLAGS $_CCCOMCOM' env['SHCC'] = '$CC' env['SHCCFLAGS'] = SCons.Util.CLVar('$CCFLAGS') env['SHCFLAGS'] = SCons.Util.CLVar('$CFLAGS') - env['SHCCCOM'] = '$SHCC /Fo$TARGET /c $SOURCES $SHCFLAGS $SHCCFLAGS $_CCCOMCOM' + env['SHCCCOM'] = '$SHCC $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $SHCFLAGS $SHCCFLAGS $_CCCOMCOM' env['CXX'] = '$CC' env['CXXFLAGS'] = SCons.Util.CLVar('$CCFLAGS $( /TP $)') - env['CXXCOM'] = '$CXX /Fo$TARGET /c $SOURCES $CXXFLAGS $CCFLAGS $_CCCOMCOM' + env['CXXCOM'] = '$CXX $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $CXXFLAGS $CCFLAGS $_CCCOMCOM' env['SHCXX'] = '$CXX' env['SHCXXFLAGS'] = SCons.Util.CLVar('$CXXFLAGS') - env['SHCXXCOM'] = '$SHCXX /Fo$TARGET /c $SOURCES $SHCXXFLAGS $SHCCFLAGS $_CCCOMCOM' + env['SHCXXCOM'] = '$SHCXX $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $SHCXXFLAGS $SHCCFLAGS $_CCCOMCOM' env['CPPDEFPREFIX'] = '/D' env['CPPDEFSUFFIX'] = '' env['INCPREFIX'] = '/I' diff --git a/src/engine/SCons/Tool/msvc.xml b/src/engine/SCons/Tool/msvc.xml index a2fdd7ed..31dcdf16 100644 --- a/src/engine/SCons/Tool/msvc.xml +++ b/src/engine/SCons/Tool/msvc.xml @@ -140,6 +140,26 @@ env['CCPDBFLAGS'] = '/Zi /Fd${TARGET}.pdb' + + +When set to any true value, +specifies that &SCons; should batch +compilation of object files +when calling the Microsoft Visual C/C++ compiler. +All compilations of source files from the same source directory +that generate target files in a same output directory +and were configured in &SCons; using the same construction environment +will be built in a single call to the compiler. +Only source files that have changed since their +object files were built will be passed to each compiler invocation +(via the &cv-link-CHANGED_SOURCES; construction variable). +Any compilations where the object (target) file base name +(minus the .obj) +does not match the source file base name +will be compiled separately. + + + The Microsoft Visual C++ precompiled header that will be used when compiling diff --git a/src/engine/SCons/Util.py b/src/engine/SCons/Util.py index cbec5dde..a9f7b70b 100644 --- a/src/engine/SCons/Util.py +++ b/src/engine/SCons/Util.py @@ -1092,11 +1092,12 @@ class Selector(OrderedDict): """A callable ordered dictionary that maps file suffixes to dictionary values. We preserve the order in which items are added so that get_suffix() calls always return the first suffix added.""" - def __call__(self, env, source): - try: - ext = source[0].suffix - except IndexError: - ext = "" + def __call__(self, env, source, ext=None): + if ext is None: + try: + ext = source[0].suffix + except IndexError: + ext = "" try: return self[ext] except KeyError: @@ -1561,20 +1562,32 @@ class Null: #cls._inst = type.__new__(cls, *args, **kwargs) cls._inst = apply(type.__new__, (cls,) + args, kwargs) return cls._inst - def __init__(self, *args, **kwargs): pass - def __call__(self, *args, **kwargs): return self - def __repr__(self): return "Null(0x%08X)" % id(self) - def __nonzero__(self): return False - def __getattr__(self, mname): return self - def __setattr__(self, name, value): return self - def __delattr__(self, name): return self + def __init__(self, *args, **kwargs): + pass + def __call__(self, *args, **kwargs): + return self + def __repr__(self): + return "Null(0x%08X)" % id(self) + def __nonzero__(self): + return False + def __getattr__(self, name): + return self + def __setattr__(self, name, value): + return self + def __delattr__(self, name): + return self class NullSeq(Null): - def __len__(self): return 0 - def __iter__(self): return iter(()) - def __getitem__(self, i): return self - def __delitem__(self, i): return self - def __setitem__(self, i, v): return self + def __len__(self): + return 0 + def __iter__(self): + return iter(()) + def __getitem__(self, i): + return self + def __delitem__(self, i): + return self + def __setitem__(self, i, v): + return self del __revision__ diff --git a/test/Batch/Boolean.py b/test/Batch/Boolean.py new file mode 100644 index 00000000..e5f8d24a --- /dev/null +++ b/test/Batch/Boolean.py @@ -0,0 +1,73 @@ +#!/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__" + +""" +Verify basic use of batch_key to write a batch builder that handles +arbitrary pairs of target + source files. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +test.write('SConstruct', """ +def batch_build(target, source, env): + for t, s in zip(target, source): + open(str(t), 'wb').write(open(str(s), 'rb').read()) +env = Environment() +bb = Action(batch_build, batch_key=True) +env['BUILDERS']['Batch'] = Builder(action=bb) +env1 = env.Clone() +env1.Batch('f1a.out', 'f1a.in') +env1.Batch('f1b.out', 'f1b.in') +env2 = env.Clone() +env2.Batch('f2a.out', 'f2a.in') +env3 = env.Clone() +env3.Batch('f3a.out', 'f3a.in') +env3.Batch('f3b.out', 'f3b.in') +""") + +test.write('f1a.in', "f1a.in\n") +test.write('f1b.in', "f1b.in\n") +test.write('f2a.in', "f2a.in\n") +test.write('f3a.in', "f3a.in\n") +test.write('f3b.in', "f3b.in\n") + +expect = test.wrap_stdout("""\ +batch_build(["f1a.out", "f1b.out"], ["f1a.in", "f1b.in"]) +batch_build(["f2a.out"], ["f2a.in"]) +batch_build(["f3a.out", "f3b.out"], ["f3a.in", "f3b.in"]) +""") + +test.run(stdout = expect) + +test.must_match('f1a.out', "f1a.in\n") +test.must_match('f1b.out', "f1b.in\n") +test.must_match('f2a.out', "f2a.in\n") +test.must_match('f3a.out', "f3a.in\n") +test.must_match('f3b.out', "f3b.in\n") + +test.pass_test() diff --git a/test/Batch/CHANGED_SOURCES.py b/test/Batch/CHANGED_SOURCES.py new file mode 100644 index 00000000..9059afdd --- /dev/null +++ b/test/Batch/CHANGED_SOURCES.py @@ -0,0 +1,118 @@ +#!/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__" + +""" +Verify use of $CHANGED_SOURCES with batch builders correctly decides +to rebuild if any sources of changed, and specifies only the sources +on the rebuild. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +_python_ = TestSCons._python_ + +test.write('batch_build.py', """\ +import os +import sys +dir = sys.argv[1] +for infile in sys.argv[2:]: + inbase = os.path.splitext(os.path.split(infile)[1])[0] + outfile = os.path.join(dir, inbase+'.out') + open(outfile, 'wb').write(open(infile, 'rb').read()) +sys.exit(0) +""") + +test.write('SConstruct', """ +env = Environment() +env['BATCH_BUILD'] = 'batch_build.py' +env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $CHANGED_SOURCES' +bb = Action('$BATCHCOM', batch_key=True, targets='CHANGED_TARGETS') +env['BUILDERS']['Batch'] = Builder(action=bb) +env1 = env.Clone() +env1.Batch('out1/f1a.out', 'f1a.in') +env1.Batch('out1/f1b.out', 'f1b.in') +env2 = env.Clone() +env2.Batch('out2/f2a.out', 'f2a.in') +env3 = env.Clone() +env3.Batch('out3/f3a.out', 'f3a.in') +env3.Batch('out3/f3b.out', 'f3b.in') +""" % locals()) + +test.write('f1a.in', "f1a.in\n") +test.write('f1b.in', "f1b.in\n") +test.write('f2a.in', "f2a.in\n") +test.write('f3a.in', "f3a.in\n") +test.write('f3b.in', "f3b.in\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out1 f1a.in f1b.in +%(_python_)s batch_build.py out2 f2a.in +%(_python_)s batch_build.py out3 f3a.in f3b.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + + + +test.write('f1b.in', "f1b.in 2\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out1 f1b.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in 2\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + + + +test.write('f3a.in', "f3a.in 2\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out3 f3a.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in 2\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in 2\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + +test.pass_test() diff --git a/test/Batch/SOURCES.py b/test/Batch/SOURCES.py new file mode 100644 index 00000000..86b3b929 --- /dev/null +++ b/test/Batch/SOURCES.py @@ -0,0 +1,120 @@ +#!/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__" + +""" +Verify use of $SOURCES with batch builders correctly decides to +rebuild if any sources of changed, and specifies all the sources +on the rebuild. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +_python_ = TestSCons._python_ + +test.write('batch_build.py', """\ +import os +import sys +dir = sys.argv[1] +for infile in sys.argv[2:]: + inbase = os.path.splitext(os.path.split(infile)[1])[0] + outfile = os.path.join(dir, inbase+'.out') + open(outfile, 'wb').write(open(infile, 'rb').read()) +sys.exit(0) +""") + +test.write('SConstruct', """ +env = Environment() +env['BATCH_BUILD'] = 'batch_build.py' +env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $SOURCES' +bb = Action('$BATCHCOM', batch_key=True) +env['BUILDERS']['Batch'] = Builder(action=bb) +env1 = env.Clone() +env1.Batch('out1/f1a.out', 'f1a.in') +env1.Batch('out1/f1b.out', 'f1b.in') +env2 = env.Clone() +env2.Batch('out2/f2a.out', 'f2a.in') +env3 = env.Clone() +env3.Batch('out3/f3a.out', 'f3a.in') +env3.Batch('out3/f3b.out', 'f3b.in') +""" % locals()) + +test.write('f1a.in', "f1a.in\n") +test.write('f1b.in', "f1b.in\n") +test.write('f2a.in', "f2a.in\n") +test.write('f3a.in', "f3a.in\n") +test.write('f3b.in', "f3b.in\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out1 f1a.in f1b.in +%(_python_)s batch_build.py out2 f2a.in +%(_python_)s batch_build.py out3 f3a.in f3b.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + +test.up_to_date(options = '--debug=explain', arguments = '.') + + + + +test.write('f1b.in', "f1b.in 2\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out1 f1a.in f1b.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in 2\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + + +test.write('f3a.in', "f3a.in 2\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out3 f3a.in f3b.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in 2\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in 2\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + +test.pass_test() diff --git a/test/Batch/action-changed.py b/test/Batch/action-changed.py new file mode 100644 index 00000000..cc0cd415 --- /dev/null +++ b/test/Batch/action-changed.py @@ -0,0 +1,90 @@ +#!/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__" + +""" +Verify that targets in a batch builder are rebuilt when the +build action changes. +""" + +import os + +import TestSCons + +python = TestSCons.python + +test = TestSCons.TestSCons() + +build_py_contents = """\ +#!/usr/bin/env %s +import sys +sep = sys.argv.index('--') +targets = sys.argv[1:sep] +sources = sys.argv[sep+1:] +for t, s in zip(targets, sources): + fp = open(t, 'wb') + fp.write('%s\\n') + fp.write(open(s, 'rb').read()) + fp.close() +sys.exit(0) +""" + +test.write('build.py', build_py_contents % (python, 'one')) +os.chmod(test.workpath('build.py'), 0755) + +test.write('SConstruct', """ +env = Environment() +bb = Action('%s $CHANGED_TARGETS -- $CHANGED_SOURCES', + batch_key=True, + targets='CHANGED_TARGETS') +env['BUILDERS']['Batch'] = Builder(action=bb) +env.Batch('f1.out', 'f1.in') +env.Batch('f2.out', 'f2.in') +env.Batch('f3.out', 'f3.in') +""" % test.workpath('build.py')) + +test.write('f1.in', "f1.in\n") +test.write('f2.in', "f2.in\n") +test.write('f3.in', "f3.in\n") + +test.run(arguments = '.') + +test.must_match('f1.out', "one\nf1.in\n") +test.must_match('f2.out', "one\nf2.in\n") +test.must_match('f3.out', "one\nf3.in\n") + +test.up_to_date(arguments = '.') + +test.write('build.py', build_py_contents % (python, 'two')) +os.chmod(test.workpath('build.py'), 0755) + +#test.not_up_to_date(options = 'CALLER=1 --taskmastertrace=/dev/tty', arguments = '.') +test.not_up_to_date(arguments = '.') + +test.must_match('f1.out', "two\nf1.in\n") +test.must_match('f2.out', "two\nf2.in\n") +test.must_match('f3.out', "two\nf3.in\n") + +test.pass_test() diff --git a/test/Batch/callable.py b/test/Batch/callable.py new file mode 100644 index 00000000..fc96f15d --- /dev/null +++ b/test/Batch/callable.py @@ -0,0 +1,103 @@ +#!/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__" + +""" +Verify passing in a batch_key callable for more control over how +batch builders behave. +""" + +import os + +import TestSCons + +test = TestSCons.TestSCons() + +test.subdir('sub1', 'sub2') + +test.write('SConstruct', """ +def batch_build(target, source, env): + for t, s in zip(target, source): + open(str(t), 'wb').write(open(str(s), 'rb').read()) +if ARGUMENTS.get('BATCH_CALLABLE'): + def batch_key(action, env, target, source): + return (id(action), id(env), target[0].dir) +else: + batch_key=True +env = Environment() +bb = Action(batch_build, batch_key=batch_key) +env['BUILDERS']['Batch'] = Builder(action=bb) +env1 = env.Clone() +env1.Batch('sub1/f1a.out', 'f1a.in') +env1.Batch('sub2/f1b.out', 'f1b.in') +env2 = env.Clone() +env2.Batch('sub1/f2a.out', 'f2a.in') +env2.Batch('sub2/f2b.out', 'f2b.in') +""") + +test.write('f1a.in', "f1a.in\n") +test.write('f1b.in', "f1b.in\n") +test.write('f2a.in', "f2a.in\n") +test.write('f2b.in', "f2b.in\n") + +sub1_f1a_out = os.path.join('sub1', 'f1a.out') +sub2_f1b_out = os.path.join('sub2', 'f1b.out') +sub1_f2a_out = os.path.join('sub1', 'f2a.out') +sub2_f2b_out = os.path.join('sub2', 'f2b.out') + +expect = test.wrap_stdout("""\ +batch_build(["%(sub1_f1a_out)s", "%(sub2_f1b_out)s"], ["f1a.in", "f1b.in"]) +batch_build(["%(sub1_f2a_out)s", "%(sub2_f2b_out)s"], ["f2a.in", "f2b.in"]) +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['sub1', 'f1a.out'], "f1a.in\n") +test.must_match(['sub2', 'f1b.out'], "f1b.in\n") +test.must_match(['sub1', 'f2a.out'], "f2a.in\n") +test.must_match(['sub2', 'f2b.out'], "f2b.in\n") + +test.run(arguments = '-c') + +test.must_not_exist(['sub1', 'f1a.out']) +test.must_not_exist(['sub2', 'f1b.out']) +test.must_not_exist(['sub1', 'f2a.out']) +test.must_not_exist(['sub2', 'f2b.out']) + +expect = test.wrap_stdout("""\ +batch_build(["%(sub1_f1a_out)s"], ["f1a.in"]) +batch_build(["%(sub1_f2a_out)s"], ["f2a.in"]) +batch_build(["%(sub2_f1b_out)s"], ["f1b.in"]) +batch_build(["%(sub2_f2b_out)s"], ["f2b.in"]) +""" % locals()) + +test.run(arguments = 'BATCH_CALLABLE=1', stdout = expect) + +test.must_match(['sub1', 'f1a.out'], "f1a.in\n") +test.must_match(['sub2', 'f1b.out'], "f1b.in\n") +test.must_match(['sub1', 'f2a.out'], "f2a.in\n") +test.must_match(['sub2', 'f2b.out'], "f2b.in\n") + +test.pass_test() diff --git a/test/Batch/generated.py b/test/Batch/generated.py new file mode 100644 index 00000000..4f31a7ee --- /dev/null +++ b/test/Batch/generated.py @@ -0,0 +1,76 @@ +#!/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__" + +""" +Verify use of a batch builder when one of the later targets in the +list the list depends on a generated file. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +test.write('SConstruct', """ +def batch_build(target, source, env): + for t, s in zip(target, source): + fp = open(str(t), 'wb') + if str(t) == 'f3.out': + fp.write(open('f3.include', 'rb').read()) + fp.write(open(str(s), 'rb').read()) +env = Environment() +bb = Action(batch_build, batch_key=True) +env['BUILDERS']['Batch'] = Builder(action=bb) +env1 = env.Clone() +env1.Batch('f1.out', 'f1.in') +env1.Batch('f2.out', 'f2.mid') +f3_out = env1.Batch('f3.out', 'f3.in') + +env2 = env.Clone() +env2.Batch('f2.mid', 'f2.in') + +f3_include = env.Batch('f3.include', 'f3.include.in') +env.Depends(f3_out, f3_include) +""") + +test.write('f1.in', "f1.in\n") +test.write('f2.in', "f2.in\n") +test.write('f3.in', "f3.in\n") +test.write('f3.include.in', "f3.include.in\n") + +expect = test.wrap_stdout("""\ +batch_build(["f2.mid"], ["f2.in"]) +batch_build(["f3.include"], ["f3.include.in"]) +batch_build(["f1.out", "f2.out", "f3.out"], ["f1.in", "f2.mid", "f3.in"]) +""") + +test.run(stdout = expect) + +test.must_match('f1.out', "f1.in\n") +test.must_match('f2.out', "f2.in\n") + +test.up_to_date(arguments = '.') + +test.pass_test() diff --git a/test/Batch/up_to_date.py b/test/Batch/up_to_date.py new file mode 100644 index 00000000..9563c403 --- /dev/null +++ b/test/Batch/up_to_date.py @@ -0,0 +1,87 @@ +#!/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__" + +""" +Verify simple use of $SOURCES with batch builders correctly decide +that files are up to date on a rebuild. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +_python_ = TestSCons._python_ + +test.write('batch_build.py', """\ +import os +import sys +dir = sys.argv[1] +for infile in sys.argv[2:]: + inbase = os.path.splitext(os.path.split(infile)[1])[0] + outfile = os.path.join(dir, inbase+'.out') + open(outfile, 'wb').write(open(infile, 'rb').read()) +sys.exit(0) +""") + +test.write('SConstruct', """ +env = Environment() +env['BATCH_BUILD'] = 'batch_build.py' +env['BATCHCOM'] = r'%(_python_)s $BATCH_BUILD ${TARGET.dir} $SOURCES' +bb = Action('$BATCHCOM', batch_key=True) +env['BUILDERS']['Batch'] = Builder(action=bb) +env1 = env.Clone() +env1.Batch('out1/f1a.out', 'f1a.in') +env1.Batch('out1/f1b.out', 'f1b.in') +env2 = env.Clone() +env2.Batch('out2/f2a.out', 'f2a.in') +env3 = env.Clone() +env3.Batch('out3/f3a.out', 'f3a.in') +env3.Batch('out3/f3b.out', 'f3b.in') +""" % locals()) + +test.write('f1a.in', "f1a.in\n") +test.write('f1b.in', "f1b.in\n") +test.write('f2a.in', "f2a.in\n") +test.write('f3a.in', "f3a.in\n") +test.write('f3b.in', "f3b.in\n") + +expect = test.wrap_stdout("""\ +%(_python_)s batch_build.py out1 f1a.in f1b.in +%(_python_)s batch_build.py out2 f2a.in +%(_python_)s batch_build.py out3 f3a.in f3b.in +""" % locals()) + +test.run(stdout = expect) + +test.must_match(['out1', 'f1a.out'], "f1a.in\n") +test.must_match(['out1', 'f1b.out'], "f1b.in\n") +test.must_match(['out2', 'f2a.out'], "f2a.in\n") +test.must_match(['out3', 'f3a.out'], "f3a.in\n") +test.must_match(['out3', 'f3b.out'], "f3b.in\n") + +test.up_to_date(options = '--debug=explain', arguments = '.') + +test.pass_test() diff --git a/test/MSVC/batch.py b/test/MSVC/batch.py new file mode 100644 index 00000000..3776df7e --- /dev/null +++ b/test/MSVC/batch.py @@ -0,0 +1,155 @@ +#!/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__" + +""" +Verify operation of Visual C/C++ batch builds. + +This uses a fake compiler and linker script, fake command lines, and +explicit suffix settings so that the test should work when run on any +platform. +""" + +import string + +import TestSCons + +test = TestSCons.TestSCons() + +_python_ = TestSCons._python_ + +test.write('fake_cl.py', """\ +import os +import string +import sys +input_files = sys.argv[2:] +if sys.argv[1][-1] in (os.sep, '\\\\'): + # The output (/Fo) argument ends with a backslash, indicating an + # output directory. We accept ending with a slash as well so this + # test runs on non-Windows systems. Strip either character and + # record the directory name. + sys.argv[1] = sys.argv[1][:-1] + dir = sys.argv[1][3:] +else: + dir = None + output = sys.argv[1][3:] +# Delay writing the .log output until here so any trailing slash or +# backslash has been stripped, and the output comparisons later in this +# script don't have to account for the difference. +open('fake_cl.log', 'ab').write(string.join(sys.argv[1:]) + '\\n') +for infile in input_files: + if dir: + outfile = os.path.join(dir, string.replace(infile, '.c', '.obj')) + else: + outfile = output + open(outfile, 'wb').write(open(infile, 'rb').read()) +""") + +test.write('fake_link.py', """\ +import string +import sys +ofp = open(sys.argv[1], 'wb') +for infile in sys.argv[2:]: + ofp.write(open(infile, 'rb').read()) +""") + +test.write('SConstruct', """ +cccom = '%(_python_)s fake_cl.py $_MSVC_OUTPUT_FLAG $CHANGED_SOURCES' +linkcom = '%(_python_)s fake_link.py ${TARGET.windows} $SOURCES' +env = Environment(tools=['msvc', 'mslink'], + CCCOM=cccom, + LINKCOM=linkcom, + PROGSUFFIX='.exe', + OBJSUFFIX='.obj', + MSVC_BATCH=ARGUMENTS.get('MSVC_BATCH')) +p = env.Object('prog.c') +f1 = env.Object('f1.c') +f2 = env.Object('f2.c') +env.Program(p + f1 + f2) +""" % locals()) + +test.write('prog.c', "prog.c\n") +test.write('f1.c', "f1.c\n") +test.write('f2.c', "f2.c\n") + + + +test.run(arguments = 'MSVC_BATCH=1 .') + +test.must_match('prog.exe', "prog.c\nf1.c\nf2.c\n") +test.must_match('fake_cl.log', """\ +/Fo. prog.c f1.c f2.c +""") + +test.up_to_date(options = 'MSVC_BATCH=1', arguments = '.') + + + +test.write('f1.c', "f1.c 2\n") + +test.run(arguments = 'MSVC_BATCH=1 .') + +test.must_match('prog.exe', "prog.c\nf1.c 2\nf2.c\n") +test.must_match('fake_cl.log', """\ +/Fo. prog.c f1.c f2.c +/Fo. f1.c +""") + +test.up_to_date(options = 'MSVC_BATCH=1', arguments = '.') + + + +test.run(arguments = '-c .') + +test.unlink('fake_cl.log') + + + +test.run(arguments = '. MSVC_BATCH=0') + +test.must_match('prog.exe', "prog.c\nf1.c 2\nf2.c\n") +test.must_match('fake_cl.log', """\ +/Fof1.obj f1.c +/Fof2.obj f2.c +/Foprog.obj prog.c +""") + + + +test.write('f1.c', "f1.c 3\n") + +test.run(arguments = '. MSVC_BATCH=0') + +test.must_match('prog.exe', "prog.c\nf1.c 3\nf2.c\n") +test.must_match('fake_cl.log', """\ +/Fof1.obj f1.c +/Fof2.obj f2.c +/Foprog.obj prog.c +/Fof1.obj f1.c +""") + + + +test.pass_test() diff --git a/test/Scanner/generated.py b/test/Scanner/generated.py index eb66fd2f..507a3d3a 100644 --- a/test/Scanner/generated.py +++ b/test/Scanner/generated.py @@ -411,17 +411,14 @@ int g_3() test.run(stderr=TestSCons.noisy_ar, match=TestSCons.match_re_dotall) -# Note that the generated .h files still get scanned twice, -# but that's really once each as a child of libg_1.o and libg_2.o. - test.must_match("MyCScan.out", """\ libg_1.c: 1 libg_2.c: 1 libg_3.c: 1 -libg_gx.h: 2 +libg_gx.h: 1 libg_gy.h: 1 libg_gz.h: 1 -libg_w.h: 2 +libg_w.h: 1 """) test.pass_test() -- 2.26.2