X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=src%2Fengine%2FSCons%2FAction.py;h=9d497526856b40e17c2a2434697c0c1f362ea6c6;hb=704f6e2480ef60718f1aa42c266f04afc9c79580;hp=59ab2614e45dad054833b815899a1f40f5f2a81b;hpb=b45894291f16c1fb3584d95f67f278ba15b2407a;p=scons.git diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py index 59ab2614..9d497526 100644 --- a/src/engine/SCons/Action.py +++ b/src/engine/SCons/Action.py @@ -30,9 +30,9 @@ other modules: a pre-substitution command for debugging purposes. get_contents() - Fetches the "contents" of an Action for signature calculation. - This is what the Sig/*.py subsystem uses to decide if a target - needs to be rebuilt because its action changed. + Fetches the "contents" of an Action for signature calculation + plus the varlist. This is what gets MD5 checksummed to decide + if a target needs to be rebuilt because its action changed. genstring() Returns a string representation of the Action *without* @@ -49,7 +49,7 @@ this module: __str__() Returns a string approximation of the Action; no variable substitution is performed. - + execute() The internal method that really, truly, actually handles the execution of a command or Python function. This is used so @@ -57,6 +57,10 @@ this module: pre-substitution representations, and *then* execute an action without worrying about the specific Actions involved. + get_presig() + Fetches the "contents" of a subclass for signature calculation. + The varlist is added to this to produce the Action's contents. + strfunction() Returns a substituted string representation of the Action. This is used by the _ActionAction.show() command to display the @@ -72,7 +76,6 @@ way for wrapping up the functions. """ -# # __COPYRIGHT__ # # Permission is hereby granted, free of charge, to any person obtaining @@ -93,32 +96,34 @@ way for wrapping up the functions. # 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. -# +from __future__ import generators ### KEEP FOR COMPATIBILITY FIXERS __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" +import cPickle import dis import os -import os.path import re -import string import sys +import subprocess from SCons.Debug import logInstanceCreation import SCons.Errors +import SCons.Executor import SCons.Util +import SCons.Subst -class _Null: - pass +# we use these a lot, so try to optimize them +is_String = SCons.Util.is_String +is_List = SCons.Util.is_List -_null = _Null +class _null: + pass print_actions = 1 execute_actions = 1 print_actions_presub = 0 -default_ENV = None - def rfile(n): try: return n.rfile() @@ -148,7 +153,144 @@ else: else: result.append(c) i = i+1 - return string.join(result, '') + return ''.join(result) + +strip_quotes = re.compile('^[\'"](.*)[\'"]$') + + +def _callable_contents(obj): + """Return the signature contents of a callable Python object. + """ + try: + # Test if obj is a method. + return _function_contents(obj.im_func) + + except AttributeError: + try: + # Test if obj is a callable object. + return _function_contents(obj.__call__.im_func) + + except AttributeError: + try: + # Test if obj is a code object. + return _code_contents(obj) + + except AttributeError: + # Test if obj is a function object. + return _function_contents(obj) + + +def _object_contents(obj): + """Return the signature contents of any Python object. + + We have to handle the case where object contains a code object + since it can be pickled directly. + """ + try: + # Test if obj is a method. + return _function_contents(obj.im_func) + + except AttributeError: + try: + # Test if obj is a callable object. + return _function_contents(obj.__call__.im_func) + + except AttributeError: + try: + # Test if obj is a code object. + return _code_contents(obj) + + except AttributeError: + try: + # Test if obj is a function object. + return _function_contents(obj) + + except AttributeError: + # Should be a pickable Python object. + try: + return cPickle.dumps(obj) + except (cPickle.PicklingError, TypeError): + # This is weird, but it seems that nested classes + # are unpickable. The Python docs say it should + # always be a PicklingError, but some Python + # versions seem to return TypeError. Just do + # the best we can. + return str(obj) + + +def _code_contents(code): + """Return the signature contents of a code object. + + By providing direct access to the code object of the + function, Python makes this extremely easy. Hooray! + + Unfortunately, older versions of Python include line + number indications in the compiled byte code. Boo! + So we remove the line number byte codes to prevent + recompilations from moving a Python function. + """ + + contents = [] + + # The code contents depends on the number of local variables + # but not their actual names. + contents.append("%s,%s" % (code.co_argcount, len(code.co_varnames))) + try: + contents.append(",%s,%s" % (len(code.co_cellvars), len(code.co_freevars))) + except AttributeError: + # Older versions of Python do not support closures. + contents.append(",0,0") + + # The code contents depends on any constants accessed by the + # function. Note that we have to call _object_contents on each + # constants because the code object of nested functions can + # show-up among the constants. + # + # Note that we also always ignore the first entry of co_consts + # which contains the function doc string. We assume that the + # function does not access its doc string. + contents.append(',(' + ','.join(map(_object_contents,code.co_consts[1:])) + ')') + + # The code contents depends on the variable names used to + # accessed global variable, as changing the variable name changes + # the variable actually accessed and therefore changes the + # function result. + contents.append(',(' + ','.join(map(_object_contents,code.co_names)) + ')') + + + # The code contents depends on its actual code!!! + contents.append(',(' + str(remove_set_lineno_codes(code.co_code)) + ')') + + return ''.join(contents) + + +def _function_contents(func): + """Return the signature contents of a function.""" + + contents = [_code_contents(func.func_code)] + + # The function contents depends on the value of defaults arguments + if func.func_defaults: + contents.append(',(' + ','.join(map(_object_contents,func.func_defaults)) + ')') + else: + contents.append(',()') + + # The function contents depends on the closure captured cell values. + try: + closure = func.func_closure or [] + except AttributeError: + # Older versions of Python do not support closures. + closure = [] + + #xxx = [_object_contents(x.cell_contents) for x in closure] + try: + xxx = [_object_contents(x.cell_contents) for x in closure] + except AttributeError: + xxx = [] + contents.append(',(' + ','.join(xxx) + ')') + + return ''.join(contents) + def _actionAppend(act1, act2): # This function knows how to slap two actions together. @@ -169,7 +311,34 @@ def _actionAppend(act1, act2): else: return ListAction([ a1, a2 ]) -def _do_create_action(act, *args, **kw): +def _do_create_keywords(args, kw): + """This converts any arguments after the action argument into + their equivalent keywords and adds them to the kw argument. + """ + v = kw.get('varlist', ()) + # prevent varlist="FOO" from being interpreted as ['F', 'O', 'O'] + if is_String(v): v = (v,) + kw['varlist'] = tuple(v) + if args: + # turn positional args into equivalent keywords + cmdstrfunc = args[0] + if cmdstrfunc is None or is_String(cmdstrfunc): + kw['cmdstr'] = cmdstrfunc + elif callable(cmdstrfunc): + kw['strfunction'] = cmdstrfunc + else: + raise SCons.Errors.UserError( + 'Invalid command display variable type. ' + 'You must either pass a string or a callback which ' + 'accepts (target, source, env) as parameters.') + if len(args) > 1: + kw['varlist'] = args[1:] + kw['varlist'] + if kw.get('strfunction', _null) is not _null \ + and kw.get('cmdstr', _null) is not _null: + raise SCons.Errors.UserError( + 'Cannot have both strfunction and cmdstr args to Action()') + +def _do_create_action(act, kw): """This is the actual "implementation" for the Action factory method, below. This handles the fact that passing lists to Action() itself has @@ -182,8 +351,11 @@ def _do_create_action(act, *args, **kw): if isinstance(act, ActionBase): return act - if SCons.Util.is_List(act): - return apply(CommandAction, (act,)+args, kw) + + if is_List(act): + #TODO(1.5) return CommandAction(act, **kw) + return CommandAction(act, **kw) + if callable(act): try: gen = kw['generator'] @@ -194,8 +366,9 @@ def _do_create_action(act, *args, **kw): action_type = CommandGeneratorAction else: action_type = FunctionAction - return apply(action_type, (act,)+args, kw) - if SCons.Util.is_String(act): + return action_type(act, kw) + + if is_String(act): var=SCons.Util.get_environment_var(act) if var: # This looks like a string that is purely an Environment @@ -204,45 +377,65 @@ def _do_create_action(act, *args, **kw): # of that Environment variable, so a user could put something # like a function or a CommandGenerator in that variable # instead of a string. - return apply(LazyAction, (var,)+args, kw) - commands = string.split(str(act), '\n') + return LazyAction(var, kw) + commands = str(act).split('\n') if len(commands) == 1: - return apply(CommandAction, (commands[0],)+args, kw) - else: - listCmdActions = map(lambda x, args=args, kw=kw: - apply(CommandAction, (x,)+args, kw), - commands) - return ListAction(listCmdActions) + #TODO(1.5) return CommandAction(commands[0], **kw) + return CommandAction(commands[0], **kw) + # The list of string commands may include a LazyAction, so we + # reprocess them via _do_create_list_action. + return _do_create_list_action(commands, kw) return None +def _do_create_list_action(act, kw): + """A factory for list actions. Convert the input list into Actions + and then wrap them in a ListAction.""" + acts = [] + for a in act: + aa = _do_create_action(a, kw) + if aa is not None: acts.append(aa) + if not acts: + return ListAction([]) + elif len(acts) == 1: + return acts[0] + else: + return ListAction(acts) + def Action(act, *args, **kw): """A factory for action objects.""" - if SCons.Util.is_List(act): - acts = map(lambda a, args=args, kw=kw: - apply(_do_create_action, (a,)+args, kw), - act) - acts = filter(None, acts) - if len(acts) == 1: - return acts[0] - else: - return ListAction(acts) - else: - return apply(_do_create_action, (act,)+args, kw) + # Really simple: the _do_create_* routines do the heavy lifting. + _do_create_keywords(args, kw) + if is_List(act): + return _do_create_list_action(act, kw) + return _do_create_action(act, kw) class ActionBase: """Base class for all types of action objects that can be held by other objects (Builders, Executors, etc.) This provides the common methods for manipulating and combining those actions.""" - - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass 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) + def get_contents(self, target, source, env): + result = [ self.get_presig(target, source, env) ] + # This should never happen, as the Action() factory should wrap + # the varlist, but just in case an action is created directly, + # we duplicate this check here. + vl = self.get_varlist(target, source, env) + if is_String(vl): vl = (vl,) + for v in vl: + result.append(env.subst('${'+v+'}')) + return ''.join(result) + def __add__(self, other): return _actionAppend(self, other) @@ -257,37 +450,52 @@ class ActionBase: # and CommandGeneratorAction will use this env # when it calls its _generate method. self.presub_env = env - lines = string.split(str(self), '\n') + lines = str(self).split('\n') 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) - -if SCons.Memoize.use_old_memoization(): - _Base = ActionBase - class ActionBase(SCons.Memoize.Memoizer, _Base): - "Cache-backed version of ActionBase" - def __init__(self, *args, **kw): - apply(_Base.__init__, (self,)+args, kw) - SCons.Memoize.Memoizer.__init__(self) + def get_varlist(self, target, source, env, executor=None): + return self.varlist + 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, strfunction=_null, presub=_null, chdir=None, exitstatfunc=None, **kw): - if not strfunction is _null: - self.strfunction = strfunction - if presub is _null: - presub = print_actions_presub + 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: + if strfunction is None: + self.cmdstr = None + else: + self.strfunction = strfunction + self.varlist = varlist self.presub = presub self.chdir = chdir if not exitstatfunc: 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") @@ -296,13 +504,18 @@ class _ActionAction(ActionBase): presub=_null, show=_null, execute=_null, - chdir=_null): - if not SCons.Util.is_List(target): + chdir=_null, + executor=None): + if not is_List(target): target = [target] - if not SCons.Util.is_List(source): + if not is_List(source): source = [source] + + if presub is _null: + presub = self.presub + if presub is _null: + presub = print_actions_presub if exitstatfunc is _null: exitstatfunc = self.exitstatfunc - if presub is _null: presub = self.presub if show is _null: show = print_actions if execute is _null: execute = execute_actions if chdir is _null: chdir = self.chdir @@ -312,19 +525,31 @@ class _ActionAction(ActionBase): try: chdir = str(chdir.abspath) except AttributeError: - if not SCons.Util.is_String(chdir): - chdir = str(target[0].dir) + if not is_String(chdir): + if executor: + chdir = str(executor.batches[0].targets[0].dir) + else: + chdir = str(target[0].dir) if presub: - t = string.join(map(str, target), ' and ') - l = string.join(self.presub_lines(env), '\n ') + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() + t = ' and '.join(map(str, target)) + l = '\n '.join(self.presub_lines(env)) out = "Building %s with action:\n %s\n" % (t, l) sys.stdout.write(out) - s = None + cmd = None if show and self.strfunction: - s = self.strfunction(target, source, env) - if s: + 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: - s = ('os.chdir(%s)\n' % repr(chdir)) + s + cmd = ('os.chdir(%s)\n' % repr(chdir)) + cmd try: get = env.get except AttributeError: @@ -333,19 +558,27 @@ class _ActionAction(ActionBase): print_func = get('PRINT_CMD_LINE_FUNC') if not print_func: print_func = self.print_cmd_line - print_func(s, target, source, env) + print_func(cmd, target, source, env) stat = 0 if execute: if chdir: os.chdir(chdir) try: - stat = self.execute(target, source, env) - stat = exitstatfunc(stat) + stat = self.execute(target, source, env, executor=executor) + if isinstance(stat, SCons.Errors.BuildError): + s = exitstatfunc(stat.status) + if s: + stat.status = s + else: + stat = s + else: + stat = exitstatfunc(stat) finally: if save_cwd: os.chdir(save_cwd) - if s and save_cwd: + if cmd and save_cwd: print_func('os.chdir(%s)' % repr(save_cwd), target, source, env) + return stat @@ -357,11 +590,89 @@ def _string_from_cmd_list(cmd_list): if ' ' in arg or '\t' in arg: arg = '"' + arg + '"' cl.append(arg) - return string.join(cl) + return ' '.join(cl) + +# A fiddlin' little function that has an 'import SCons.Environment' which +# can't be moved to the top level without creating an import loop. Since +# this import creates a local variable named 'SCons', it blocks access to +# the global variable, so we move it here to prevent complaints about local +# variables being used uninitialized. +default_ENV = None +def get_default_ENV(env): + global default_ENV + try: + return env['ENV'] + except KeyError: + if not default_ENV: + import SCons.Environment + # This is a hideously expensive way to get a default shell + # environment. What it really should do is run the platform + # setup to get the default ENV. Fortunately, it's incredibly + # rare for an Environment not to have a shell environment, so + # we're not going to worry about it overmuch. + default_ENV = SCons.Environment.Environment()['ENV'] + return default_ENV + +# This function is still in draft mode. We're going to need something like +# it in the long run as more and more places use subprocess, but I'm sure +# it'll have to be tweaked to get the full desired functionality. +# one special arg (so far?), 'error', to tell what to do with exceptions. +def _subproc(env, cmd, error = 'ignore', **kw): + """Do common setup for a subprocess.Popen() call""" + # allow std{in,out,err} to be "'devnull'" + io = kw.get('stdin') + if is_String(io) and io == 'devnull': + kw['stdin'] = open(os.devnull) + io = kw.get('stdout') + if is_String(io) and io == 'devnull': + kw['stdout'] = open(os.devnull, 'w') + io = kw.get('stderr') + if is_String(io) and io == 'devnull': + kw['stderr'] = open(os.devnull, 'w') + + # Figure out what shell environment to use + ENV = kw.get('env', None) + if ENV is None: ENV = get_default_ENV(env) + + # Ensure that the ENV values are all strings: + new_env = {} + for key, value in ENV.items(): + if is_List(value): + # If the value is a list, then we assume it is a path list, + # because that's a pretty common list-like value to stick + # in an environment variable: + value = SCons.Util.flatten_sequence(value) + new_env[key] = os.pathsep.join(map(str, value)) + else: + # It's either a string or something else. If it's a string, + # we still want to call str() because it might be a *Unicode* + # string, which makes subprocess.Popen() gag. If it isn't a + # string or a list, then we just coerce it to a string, which + # is the proper way to handle Dir and File instances and will + # produce something reasonable for just about everything else: + new_env[key] = str(value) + kw['env'] = new_env + + try: + #FUTURE return subprocess.Popen(cmd, **kw) + return subprocess.Popen(cmd, **kw) + except EnvironmentError, e: + if error == 'raise': raise + # return a dummy Popen instance that only returns error + class dummyPopen: + def __init__(self, e): self.exception = e + def communicate(self): return ('','') + def wait(self): return -self.exception.errno + stdin = None + class f: + def read(self): return '' + def readline(self): return '' + stdout = stderr = f() + return dummyPopen(e) class CommandAction(_ActionAction): """Class for command-execution actions.""" - def __init__(self, cmd, cmdstr=None, *args, **kw): + def __init__(self, cmd, **kw): # Cmd can actually be a list or a single item; if it's a # single item it should be the command string to execute; if a # list then it should be the words of the command string to @@ -372,24 +683,28 @@ class CommandAction(_ActionAction): # Environment.subst_list() for substituting environment # variables. if __debug__: logInstanceCreation(self, 'Action.CommandAction') - apply(_ActionAction.__init__, (self,)+args, kw) - if SCons.Util.is_List(cmd): - if filter(SCons.Util.is_List, cmd): + + #TODO(1.5) _ActionAction.__init__(self, **kw) + _ActionAction.__init__(self, **kw) + if is_List(cmd): + if list(filter(is_List, cmd)): raise TypeError, "CommandAction should be given only " \ "a single command" self.cmd_list = cmd - self.cmdstr = cmdstr def __str__(self): - if SCons.Util.is_List(self.cmd_list): - return string.join(map(str, self.cmd_list), ' ') + if is_List(self.cmd_list): + return ' '.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: + while True: try: c = result[0][0][0] except IndexError: c = None if c == '@': silent = 1 @@ -403,17 +718,23 @@ class CommandAction(_ActionAction): pass return result, ignore, silent - def strfunction(self, target, source, env): - if not self.cmdstr is None: - c = env.subst(self.cmdstr, 0, target, source) + 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 + 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) - if silent: - return '' + 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, @@ -422,7 +743,8 @@ class CommandAction(_ActionAction): handle lists of commands, even though that's not how we use it externally. """ - from SCons.Util import is_String, is_List, flatten, escape_list + escape_list = SCons.Subst.escape_list + flatten_sequence = SCons.Util.flatten_sequence try: shell = env['SHELL'] @@ -433,17 +755,13 @@ class CommandAction(_ActionAction): spawn = env['SPAWN'] except KeyError: raise SCons.Errors.UserError('Missing SPAWN construction variable.') + else: + if is_String(spawn): + spawn = env.subst(spawn, raw=1, conv=lambda x: x) escape = env.get('ESCAPE', lambda x: x) - try: - ENV = env['ENV'] - except KeyError: - global default_ENV - if not default_ENV: - import SCons.Environment - default_ENV = SCons.Environment.Environment()['ENV'] - ENV = default_ENV + ENV = get_default_ENV(env) # Ensure that the ENV values are all strings: for key, value in ENV.items(): @@ -452,8 +770,8 @@ class CommandAction(_ActionAction): # If the value is a list, then we assume it is a # path list, because that's a pretty common list-like # value to stick in an environment variable: - value = flatten(value) - ENV[key] = string.join(map(str, value), os.pathsep) + value = flatten_sequence(value) + ENV[key] = os.pathsep.join(map(str, value)) else: # If it isn't a string or a list, then we just coerce # it to a string, which is the proper way to handle @@ -461,7 +779,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, list(map(rfile, source)), env, executor) # Use len() to filter out any "command" that's zero-length. for cmd_line in filter(len, cmd_list): @@ -469,65 +790,123 @@ class CommandAction(_ActionAction): cmd_line = escape_list(cmd_line, escape) result = spawn(shell, escape, cmd_line[0], cmd_line, ENV) if not ignore and result: - return result + msg = "Error %s" % result + return SCons.Errors.BuildError(errstr=msg, + status=result, + action=self, + command=cmd_line) return 0 - def get_contents(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. """ + from SCons.Subst import SUBST_SIG cmd = self.cmd_list - if SCons.Util.is_List(cmd): - cmd = string.join(map(str, cmd)) + if is_List(cmd): + cmd = ' '.join(map(str, cmd)) else: cmd = str(cmd) - return env.subst_target_source(cmd, SCons.Util.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, 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 + 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: + d = str(cmd_line[0]) + m = strip_quotes.match(d) + if m: + d = m.group(1) + d = env.WhereIs(d) + if d: + res.append(env.fs.File(d)) + return res class CommandGeneratorAction(ActionBase): """Class for command-generator actions.""" - def __init__(self, generator, *args, **kw): + def __init__(self, generator, kw): if __debug__: logInstanceCreation(self, 'Action.CommandGeneratorAction') 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 SCons.Util.is_List(target): + if not is_List(target): target = [target] - ret = self.generator(target=target, source=source, env=env, for_signature=for_signature) - gen_cmd = apply(Action, (ret,), self.gen_kw) + 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 = Action(ret, **self.gen_kw) if not gen_cmd: raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret)) return gen_cmd def __str__(self): try: - env = self.presub_env or {} + env = self.presub_env except AttributeError: - env = {} + env = None + if env is None: + env = SCons.Defaults.DefaultEnvironment() 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(list(map(str, target))), repr(list(map(str, source))))) return act(target, source, env, exitstatfunc, presub, - show, execute, chdir) + show, execute, chdir, executor) - def get_contents(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_contents(target, source, env) + return self._generate(target, source, env, 1, executor).get_presig(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_varlist(self, target, source, env, executor=None): + return self._generate(target, source, env, 1, executor).get_varlist(target, source, env, executor) + + def get_targets(self, env, executor): + return self._generate(None, None, env, 1, executor).get_targets(env, executor) @@ -550,58 +929,65 @@ class CommandGeneratorAction(ActionBase): class LazyAction(CommandGeneratorAction, CommandAction): - if SCons.Memoize.use_memoizer: - __metaclass__ = SCons.Memoize.Memoized_Metaclass - - def __init__(self, var, *args, **kw): + def __init__(self, var, kw): if __debug__: logInstanceCreation(self, 'Action.LazyAction') - apply(CommandAction.__init__, (self, '$'+var)+args, kw) + #FUTURE CommandAction.__init__(self, '${'+var+'}', **kw) + CommandAction.__init__(self, '${'+var+'}', **kw) self.var = SCons.Util.to_String(var) self.gen_kw = kw def get_parent_class(self, env): c = env.get(self.var) - if SCons.Util.is_String(c) and not '\n' in c: + if is_String(c) and not '\n' in c: return CommandAction return CommandGeneratorAction def _generate_cache(self, env): - """__cacheable__""" - c = env.get(self.var, '') - gen_cmd = apply(Action, (c,), self.gen_kw) + if env: + c = env.get(self.var, '') + else: + c = '' + #TODO(1.5) gen_cmd = Action(c, **self.gen_kw) + gen_cmd = 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): - args = (self, target, source, env) + args c = self.get_parent_class(env) - return apply(c.__call__, args, kw) + return c.__call__(self, target, source, env, *args, **kw) - def get_contents(self, target, source, env): + def get_presig(self, target, source, env): c = self.get_parent_class(env) - return c.get_contents(self, target, source, env) - -if not SCons.Memoize.has_metaclass: - _Base = LazyAction - class LazyAction(SCons.Memoize.Memoizer, _Base): - def __init__(self, *args, **kw): - SCons.Memoize.Memoizer.__init__(self) - apply(_Base.__init__, (self,)+args, kw) + return c.get_presig(self, target, source, env) + def get_varlist(self, target, source, env, executor=None): + c = self.get_parent_class(env) + return c.get_varlist(self, target, source, env, executor) class FunctionAction(_ActionAction): """Class for Python function actions.""" - def __init__(self, execfunction, *args, **kw): + def __init__(self, execfunction, kw): if __debug__: logInstanceCreation(self, 'Action.FunctionAction') + self.execfunction = execfunction - apply(_ActionAction.__init__, (self,)+args, kw) - self.varlist = kw.get('varlist', []) + try: + self.funccontents = _callable_contents(execfunction) + except AttributeError: + try: + # See if execfunction will do the heavy lifting for us. + self.gc = execfunction.get_contents + except AttributeError: + # This is weird, just do the best we can. + self.funccontents = _object_contents(execfunction) + + #TODO(1.5) _ActionAction.__init__(self, **kw) + _ActionAction.__init__(self, **kw) def function_name(self): try: @@ -612,11 +998,27 @@ 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 + 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): def quote(s): - return '"' + str(s) + '"' - return '[' + string.join(map(quote, a), ", ") + ']' + try: + str_for_display = s.str_for_display + except AttributeError: + s = repr(s) + else: + s = str_for_display() + return s + return '[' + ", ".join(map(quote, a)) + ']' try: strfunc = self.execfunction.strfunction except AttributeError: @@ -637,89 +1039,116 @@ class FunctionAction(_ActionAction): return str(self.execfunction) return "%s(target, source, env)" % name - def execute(self, target, source, env): - rsources = map(rfile, source) - try: - result = self.execfunction(target=target, source=rsources, env=env) - except EnvironmentError, e: - # If an IOError/OSError happens, raise a BuildError. - raise SCons.Errors.BuildError(node=target, errstr=e.strerror) - return result - - def get_contents(self, target, source, env): - """Return the signature contents of this callable action. - - By providing direct access to the code object of the - function, Python makes this extremely easy. Hooray! - - Unfortunately, older versions of Python include line - number indications in the compiled byte code. Boo! - So we remove the line number byte codes to prevent - recompilations from moving a Python function. - """ + def execute(self, target, source, env, executor=None): + exc_info = (None,None,None) try: - # "self.execfunction" is a function. - contents = str(self.execfunction.func_code.co_code) - except AttributeError: - # "self.execfunction" is a callable object. + if executor: + target = executor.get_all_targets() + source = executor.get_all_sources() + rsources = list(map(rfile, source)) try: - contents = str(self.execfunction.__call__.im_func.func_code.co_code) - except AttributeError: + result = self.execfunction(target=target, source=rsources, env=env) + except KeyboardInterrupt, e: + raise + except SystemExit, e: + raise + except Exception, e: + result = e + exc_info = sys.exc_info() + + if result: + result = SCons.Errors.convert_to_BuildError(result, exc_info) + result.node=target + result.action=self try: - # See if execfunction will do the heavy lifting for us. - gc = self.execfunction.get_contents - except AttributeError: - # This is weird, just do the best we can. - contents = str(self.execfunction) - else: - contents = gc(target, source, env) - contents = remove_set_lineno_codes(contents) - return contents + env.subst(string.join(map(lambda v: '${'+v+'}', - self.varlist))) + 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 + # exception and which ones were returned by value. It would + # probably be best to always return them by value here, but + # some codes do not check the return value of Actions and I do + # not have the time to modify them at this point. + if (exc_info[1] and + not isinstance(exc_info[1],EnvironmentError)): + raise result + + return result + finally: + # Break the cycle between the traceback object and this + # function stack frame. See the sys.exc_info() doc info for + # more information about this issue. + del exc_info + + + def get_presig(self, target, source, env): + """Return the signature contents of this callable action.""" + try: + return self.gc(target, source, env) + except AttributeError: + return self.funccontents + + def get_implicit_deps(self, target, source, env): + return [] class ListAction(ActionBase): """Class for lists of other actions.""" - def __init__(self, list): + def __init__(self, actionlist): if __debug__: logInstanceCreation(self, 'Action.ListAction') def list_of_actions(x): if isinstance(x, ActionBase): return x return Action(x) - self.list = map(list_of_actions, list) + self.list = list(map(list_of_actions, actionlist)) + # 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: - a.genstring(t, s, e), - self.list), - '\n') + return '\n'.join([a.genstring(target, source, env) for a in self.list]) def __str__(self): - return string.join(map(str, self.list), '\n') - + return '\n'.join(map(str, self.list)) + def presub_lines(self, env): - return SCons.Util.flatten(map(lambda a, env=env: - a.presub_lines(env), - self.list)) + return SCons.Util.flatten_sequence( + [a.presub_lines(env) for a in self.list]) - def get_contents(self, target, source, env): + def get_presig(self, target, source, env): """Return the signature contents of this action list. Simple concatenation of the signatures of the elements. """ - return string.join(map(lambda x, t=target, s=source, e=env: - x.get_contents(t, s, e), - self.list), - "") + return "".join([x.get_contents(target, source, env) for x in self.list]) 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 + def get_implicit_deps(self, target, source, env): + result = [] + for act in self.list: + result.extend(act.get_implicit_deps(target, source, env)) + return result + + def get_varlist(self, target, source, env, executor=None): + result = SCons.Util.OrderedDict() + for act in self.list: + for var in act.get_varlist(target, source, env, executor): + result[var] = True + return result.keys() + class ActionCaller: """A class for delaying calling an Action function with specific (positional and keyword) arguments until the Action is actually @@ -733,6 +1162,7 @@ class ActionCaller: self.parent = parent self.args = args self.kw = kw + def get_contents(self, target, source, env): actfunc = self.parent.actfunc try: @@ -748,33 +1178,49 @@ class ActionCaller: contents = str(actfunc) contents = remove_set_lineno_codes(contents) return contents + def subst(self, s, target, source, env): + # If s is a list, recursively apply subst() + # to every element in the list + if is_List(s): + result = [] + for elem in s: + result.append(self.subst(elem, target, source, env)) + return self.parent.convert(result) + # Special-case hack: Let a custom function wrapped in an # ActionCaller get at the environment through which the action # was called by using this hard-coded value as a special return. if s == '$__env__': return env - else: - return env.subst(s, 0, target, source) + elif is_String(s): + return env.subst(s, 1, target, source) + return self.parent.convert(s) + def subst_args(self, target, source, env): - return map(lambda x, self=self, t=target, s=source, e=env: - self.subst(x, t, s, e), - self.args) + return [self.subst(x, target, source, env) for x in self.args] + def subst_kw(self, target, source, env): kw = {} for key in self.kw.keys(): 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) - return apply(self.parent.actfunc, args, kw) + #TODO(1.5) return self.parent.actfunc(*args, **kw) + return self.parent.actfunc(*args, **kw) + def strfunction(self, target, source, env): args = self.subst_args(target, source, env) kw = self.subst_kw(target, source, env) - return apply(self.parent.strfunc, args, kw) + #TODO(1.5) return self.parent.strfunc(*args, **kw) + return self.parent.strfunc(*args, **kw) + def __str__(self): - return apply(self.parent.strfunc, self.args, self.kw) + #TODO(1.5) return self.parent.strfunc(*self.args, **self.kw) + return self.parent.strfunc(*self.args, **self.kw) class ActionFactory: """A factory class that will wrap up an arbitrary function @@ -785,10 +1231,18 @@ class ActionFactory: called with and give them to the ActionCaller object we create, so it can hang onto them until it needs them. """ - def __init__(self, actfunc, strfunc): + def __init__(self, actfunc, strfunc, convert=lambda x: x): self.actfunc = actfunc self.strfunc = strfunc + self.convert = convert + def __call__(self, *args, **kw): ac = ActionCaller(self, args, kw) action = Action(ac, strfunction=ac.strfunction) return action + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: