# Part of the A-A-P project: Action execution module
# Copyright (C) 2002-2003 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING
#
# This module selects commands to be executed for an action on a file
#
# EXTERNAL INTERFACE:
#
# action_run(recdict, action, dict, fname)
# Detects the type of file "fname", selects
# the application to do "action" and runs it.
# "dict" has attributes for customization.
#
# action_add(rpstack, recdict, args, commands)
# Add a connection between action-filetype to
# commands
#
# action_get_list() Return a dictionary that lists all available
# actions.
#
import string
import os
import os.path
from Util import *
from Dictlist import listitem2str,str2dictlist,dictlist2str
from ParsePos import ParsePos
from Process import Process,recipe_error
from Filetype import filetype_root,ft_known
from Commands import expand
from Scope import get_build_recdict
import RecPython
# All actions are stored in this dictionary.
# Key is the name of the action. Value is a list of Action objects.
# When multiple actions match, the last action in the list is preferred over
# earlier actions.
# Use this as:
# _action_dict[action].append(Action())
_action_dict = {}
class Action:
"""
Class to store the commands and rpstack for an action/filetype.
In a derived class get_in_types() and get_out_types() may be implemented
differently.
"""
def __init__(self, rpstack, recdict, attributes, commands,
outtypes = [], intypes = [],
defer_var_names = [],
outfilename = ''):
self.rpstack = rpstack
self.buildrecdict = recdict
self.attributes = attributes
self.commands = commands
self.outtypes = outtypes
self.intypes = intypes
self.primary = attributes.get("primary", 0)
self.defer_var_names = defer_var_names
self.outfilename = outfilename
def get_out_types(self, source = "", intype = None):
"""
Return list of supported output filetypes.
When "intype" is given, this is the required output type.
"""
aname = self.defer_action_name(source = source)
if aname:
return get_ftypes(aname, 0, intype = intype)
if intype and intype not in self.intypes:
return []
return self.outtypes
def get_in_types(self, source = "", outtype = None):
"""
Return list of supported input filetypes.
When "outtype" is given, this is the required output type.
"""
aname = self.defer_action_name(source = source)
if aname:
return get_ftypes(aname, 1, outtype = outtype)
if outtype and outtype not in self.outtypes:
return []
return self.intypes
def defer_action_name(self, source = ""):
"""Return name of action to defer the work to."""
for n in self.defer_var_names:
if len(n) > 2 and n[0] == '{' and n[-1] == '}':
aname = RecPython.get_var_attr(source, n[1:-1])
else:
aname = get_var_val(0, Global.globals, "_no", n)
if aname:
return aname
return None
def __str__(self):
return self.commands
def action_add(rpstack, recdict, arglist, commands):
"""
Add "commands" for an action-filetype combination defined by dictlist
"arglist".
"rpstack" is used for an error message when executing the commdands.
"""
if len(arglist) == 2:
outtypes = ["default"]
intypes = string.split(arglist[1]["name"], ',')
elif len(arglist) == 3:
outtypes = string.split(arglist[1]["name"], ',')
intypes = string.split(arglist[2]["name"], ',')
else:
recipe_error(rpstack, _(':action must have two or three arguments'))
# Check that the filetype names exist
for i in intypes + outtypes:
if i != "default" and not ft_known(i):
msg_warning(recdict, _('unknown filetype "%s" in :action command; recipe %s line %d') % (i, rpstack[-1].name, rpstack[-1].line_nr))
act = Action(rpstack, recdict, arglist[0], commands, outtypes, intypes)
for action in string.split(arglist[0]["name"], ','):
action_add_to_dict(action, act)
def action_add_to_dict(action, act):
"""
Add action object "act" to the dictionary of actions.
"""
global _action_dict
if not _action_dict.has_key(action):
_action_dict[action] = []
_action_dict[action].append(act)
def find_action(action, intype, outtype):
"""
Find the last action that supports intype and outtype.
"""
act = None
for a in _action_dict[action]:
if intype in a.get_in_types(outtype = outtype) and outtype in a.get_out_types(intype = intype):
act = a;
return act
def find_action_route(in_ftype, out_ftype):
"""
Find a route from "in_ftype" to "out_ftype" with primary actions.
Returns a route object or None.
"""
# Use a list of dictionaries. Each dictionary holds info about one
# incomplete route:
# type : input for the next action to be found
# actions : list of action lines of the steps found so far
# used_types : list of intermediate types used in the steps
trylist = [ {"type" : in_ftype, "actions" : [], "used_types" : [[in_ftype]]} ]
# Try up to ten steps deep before giving up.
depth = 0
while trylist and depth < 10:
new_trylist = []
for ent in trylist:
# Find actions that uses the type of this entry as input
for action in _action_dict.keys():
for act in _action_dict[action]:
if act.primary and ent["type"] in act.get_in_types():
for atype in act.get_out_types():
if atype == out_ftype:
# Found a route!
from Work import Route
from RecPos import RecPos
actions = ent["actions"] + [action]
return Route(Global.globals,
[RecPos("route from primary actions")], 0,
ent["used_types"] + [[out_ftype]],
{},
actions, map(lambda x: 0, actions))
# No direct route, may add to list for next try.
if atype != "default" and act.outfilename:
# Make the line look like what ":route" uses.
actions = ent["actions"] + [action + " " + act.outfilename]
new_trylist.append({"type" : atype,
"actions" : actions,
"used_types" : ent["used_types"] + [[atype]]})
depth = depth + 1
trylist = new_trylist
return None
def find_primary_action(action):
"""
Find the prinary action with name "action".
"""
act = None
for a in _action_dict[action]:
if a.primary:
act = a;
return act
def get_ftypes(action, in_types, intype = None, outtype = None):
"""
Return the list of filetypes supported by actions with name "action".
If "intype" given this input type must be supported.
If "outtype" given this output type must be supported.
"""
retval = []
if _action_dict.has_key(action):
for act in _action_dict[action]:
if in_types:
l = act.get_in_types(outtype = outtype)
else:
l = act.get_out_types(intype = intype)
for i in l:
if i not in retval:
retval.append(i)
return retval
def action_get_list():
"""
Return the dictionary that lists all actions.
This turns _action_dict[] into another kind of dictionary for backwards
compatibility.
The dictionary key is the action name, the item is the intype
dictionary.
The intype dictionary key is the input file type, the item is the outtype
dictionary.
The outtype dictionary key is the output file type, the item is an Action
object.
retval[action-name][input-filetype][output-filetype].
"""
retval = {}
for action in _action_dict.keys():
retval[action] = {}
for act in _action_dict[action]:
for intype in act.get_in_types():
if not retval[action].has_key(intype):
retval[action][intype] = {}
for outtype in act.get_out_types():
retval[action][intype][outtype] = act
return retval
def action_ftype(recdict, action, dict):
"""
Decide what filetype to use for "action" for the file specified with
dictionary "dict".
"""
if dict.has_key("filetype"):
return dict["filetype"]
from Work import getwork
fname = dict["name"]
node = getwork(recdict).find_node(fname)
if node and node.attributes.has_key("filetype"):
return node.attributes["filetype"]
# A ":program", ":produce" etc. adds a "filetypehint" attribute that has a
# lower priority than a "filetype" attribute the user specifies.
if dict.has_key("filetypehint"):
return dict["filetypehint"]
# For viewing a remote file the filetype doesn't really matter, need to
# use a browser anyway.
# TODO: expand list of methods
i = string.find(fname, "://")
if (i > 0 and fname[:i] in [ "http", "https", "ftp" ]
and action == "view"):
return "html"
# Detect the filetype
from Filetype import ft_detect
return ft_detect(fname, 1, recdict)
def has_action(recdict, action, dict):
"""
Return non-zero if "action" is defined for the file specified with "dict".
Does not use the default.
"""
if not _action_dict.has_key(action):
return 0
intype = action_ftype(recdict, action, dict)
for act in _action_dict[action]:
if intype in act.get_in_types():
return 1
return 0
def find_depend_action(ftype):
"""Find the depend action for a source file with type "ftype"."""
if not _action_dict.has_key("depend"):
return None
found_act = None
for intype in [ftype, filetype_root(ftype), "default"]:
for act in _action_dict["depend"]:
if intype in act.get_in_types():
found_act = act
if found_act:
break
# TODO: Should we check for the "default" outtype in the loop above?
if found_act and "default" in found_act.get_out_types():
return found_act
return None
def find_out_ftype(recdict, actlist, action, in_ftype, out_ftype, msg):
"""
Find the output filetype to be used for "action" and "in_ftype".
Use only the actions in the list "actlist".
Use the first one of "out_ftype", its root or "default" that is defined.
Return the output filetype and non-zero if found, otherwise anything and
zero.
"""
if out_ftype:
# Find an action that supports "out_ftype".
for act in actlist:
if out_ftype in act.get_out_types():
return out_ftype, 1
# Find an action that supports the root of "out_ftype".
root = filetype_root(out_ftype)
for act in actlist:
if root in act.get_out_types():
return root, 1
# Find an action that supports "default".
for act in actlist:
if "default" in act.get_out_types():
if msg:
msg_extra(recdict, _('Using default for %s %s from %s')
% (action, out_ftype, in_ftype))
return "default", 1
return out_ftype, 0
# For pychecker
find_type = None
def action_find(recdict, action, in_ftype, out_ftype, msg):
"""
Find the action to be used for "action", with detected input filetype
"in_ftype" and detected output filetype "out_ftype".
When "msg" is non-zero give a message about using a default type.
"""
# Try three input filetypes and three output filetypes for each of them.
# If no commands defined for the detected filetype, use the root filetype.
# If still no commands defined, use the default action.
use_in_ftype = in_ftype
use_out_ftype = out_ftype
found = 0
# Find_type must be global for this to work in Python 1.5.
global find_type
find_type = in_ftype
actlist = filter(lambda act: find_type in act.get_in_types(),
_action_dict[action])
if actlist:
use_out_ftype, found = find_out_ftype(recdict, actlist,
action, in_ftype, out_ftype, msg)
if not found:
find_type = filetype_root(in_ftype)
actlist = filter(lambda act: find_type in act.get_in_types(),
_action_dict[action])
if actlist:
use_in_ftype = find_type
use_out_ftype, found = find_out_ftype(recdict, actlist,
action, find_type, out_ftype, msg)
if not found:
actlist = filter(lambda act: "default" in act.get_in_types(),
_action_dict[action])
if actlist:
use_in_ftype = "default"
use_out_ftype, found = find_out_ftype(recdict, actlist,
action, use_in_ftype, out_ftype, msg)
if msg and found and use_out_ftype != "default":
msg_extra(recdict, _('Using default for %s from %s')
% (action, in_ftype))
if not found:
return None, None
return use_in_ftype, use_out_ftype
def get_vars_from_attr(fromdict, todict, savedict = None):
"""
Set variables from attributes:
- Take all the entries in "fromdict" that start with "var_" and copy the
value to "todict".
- Take all the entries in "fromdict" that start with "add_" and append the
value to a variable in "todict".
If "savedict" is given, save the old value in it.
"""
for k in fromdict.keys():
if len(k) > 4 and (k[:4] == "var_" or k[:4] == "add_"):
varname = k[4:]
val = fromdict[k]
if k[0] == 'a':
oldval = get_var_val(0, todict, "_no", varname)
if oldval:
# If the value already appears, don't append it again.
if string.find(' ' + oldval + ' ', ' ' + val + ' ') >= 0:
val = oldval
else:
val = oldval + ' ' + val
if savedict:
savedict[varname] = todict.get(varname)
todict[varname] = val
def action_run(recdict, args):
"""Run the associated program for action "args[0]" on a list of files
"args[1:]". "args" is a dictlist.
Use the attributes in "args[0]" to customize the action.
When the "filetype" attribute isn't specified, detect it from args[1].
Returns None for success, an error message for failure."""
action = args[0]["name"]
if not _action_dict.has_key(action):
return _("Unknown action: %s") % action
in_ftype = action_ftype(recdict, action, args[1])
if not in_ftype:
msg_note(recdict,
_('Warning; Filetype not recognized for "%s", using "default"')
% args[1]["name"])
in_ftype = "default"
out_ftype = args[0].get("filetype")
if not out_ftype:
if args[0].has_key("target"):
t = args[0]["target"]
elif recdict.has_key("target"):
t = recdict["target"]
else:
t = None
if t:
try:
out_ftype = action_ftype(recdict, action,
str2dictlist([], t)[0])
except UserError, e:
return _('Error parsing target attribute: ') + str(e)
use_in_ftype, use_out_ftype = action_find(recdict, action, in_ftype,
out_ftype, 1)
if not use_in_ftype:
return (_("No commands defined for %s %s from %s")
% (action, out_ftype, in_ftype))
msg_extra(recdict, _('Do %s %s -> %s')
% (action, use_in_ftype, use_out_ftype))
act = find_action(action, use_in_ftype, use_out_ftype)
if not act:
msg_error(recdict, _("Internal error: action_run() didn't find action"))
# Make a new scope for the the command block.
new_recdict = get_build_recdict(recdict, act.buildrecdict,
keep_current_scope = 1, rpstack = act.rpstack,
xscope = args[0].get("scope"))
# Take care of local and build command variables.
# $source is the whole list of filenames. Need to use quotes around items
# with white space.
# $fname is the first file name
new_recdict["source"] = dictlist2str(args[1:])
new_recdict["fname"] = listitem2str(args[1]["name"])
# $filetype is the detected input filetype
# $targettype is the detected output filetype
# $action is the name of the action
new_recdict["filetype"] = in_ftype
new_recdict["targettype"] = out_ftype
new_recdict["action"] = action
# If the action is to be deferred to another action, set $DEFER_ACTION_NAME
# to the name of that action.
new_recdict["DEFER_ACTION_NAME"] = act.defer_action_name(source = new_recdict["source"])
# Turn the attributes on the action into variables.
# Both the full name and "var_" names.
# Skip the "scope" attribute, it's handled above.
for k in args[0].keys():
if len(k) <= 4 or (k[:4] != "var_" and k[:4] != "add_"
and k not in ["scope", "filetype", "remove"]):
new_recdict[k] = args[0][k]
get_vars_from_attr(args[0], new_recdict)
from Work import getwork
# First use "var_" and "add_" attributes of the node.
# Turn the "var_" and "add_" attributes of the sources into variables.
for s in args[1:]:
node = getwork(recdict).find_node(s["name"])
if node:
get_vars_from_attr(node.attributes, new_recdict)
get_vars_from_attr(s, new_recdict)
# Also use "var_" and "add_" attributes from the target.
if new_recdict.has_key("target"):
for d in str2dictlist([], new_recdict["target"]):
get_vars_from_attr(d, new_recdict)
# Create a ParsePos object to contain the parse position in the string.
fp = ParsePos(act.rpstack, string = act.commands)
#
# Parse and execute the commands.
#
try:
Process(fp, new_recdict, 0)
except UserError, e:
return ((_('Error executing commands for %s %s: ')
% (action, use_in_ftype)) + str(e))
return None
def action_expand_do(recdict, commands, targetlist, sourcelist):
"""Expand ":do" commands in "commands" to the build commands they stand
for. Used for computing the checksum, not for executing the commands!.
The filetype is sometimes guessed."""
# Return quickly when there is no ":do" command.
if string.find(commands, ":do") < 0:
return commands
# Remember the end of an expanded action. It is not allowed to expand
# again before this position, it would mean an action invokes itself and
# causes an endless loop.
exp_action_end = {}
# Locate each ":do" command. When we can find the commands of the action,
# replace the ":do" command with them.
idx = 0
while 1:
# Find the next ":do" command.
do = string.find(commands, ":do", idx)
if do < 0:
break
# Get the arguments of the ":do" command; advance "idx" to the end.
i = skip_white(commands, do + 3)
idx = string.find(commands, '\n', i)
args = str2dictlist([], commands[i:idx])
if len(args) >= 2:
# The action is the first argument.
action = args[0]["name"]
if _action_dict.has_key(action):
# Guess the input filetype to be used:
# 1. an explicitly defined filetype after the action.
# 2. if the first filename doesn't contain a $, get the
# filetype from it.
# 3. use the filetype from the first source item
if args[1].has_key("filetype"):
in_ftype = args[1]["filetype"]
elif not '$' in args[1]["name"]:
in_ftype = action_ftype(recdict, action, args[1])
else:
in_ftype = None
if not in_ftype and sourcelist:
# Get the filetype of the first source item.
in_ftype = action_ftype(recdict, action, sourcelist[0])
if not in_ftype:
in_ftype = "default"
# Guess the output filetype to be used:
# 1. an explicitly defined filetype after the target.
# 2. if the first filename doesn't contain a $, get the
# filetype from it.
# 3. use the filetype from the first source item
tt = recdict.get("target")
try:
recdict["target"] = targetlist[0]["name"]
target = str2dictlist([], expand(0, recdict,
args[0]["target"], Expand(1, Expand.quote_aap)))[0]
except:
target = targetlist[0]
if recdict.has_key("target"):
if tt is None:
del recdict["target"]
else:
recdict["target"] = tt
if target.has_key("filetype"):
out_ftype = target["filetype"]
elif not '$' in target["name"]:
out_ftype = action_ftype(recdict, action, target)
else:
out_ftype = None
if not out_ftype:
out_ftype = "default"
# Find the action to be used for these filetypes, falling back
# to "default" when necessary.
in_ftype, out_ftype = action_find(recdict, action,
in_ftype, out_ftype, 0)
if in_ftype:
# Use the buildcheck attribute of the Action if present,
# use the commands otherwise.
act = find_action(action, in_ftype, out_ftype)
cmd = act.attributes.get("buildcheck")
if not cmd:
cmd = act.commands
# Check if this action isn't used recursively.
key = action + '@' + in_ftype + '@' + out_ftype
if (exp_action_end.has_key(key)
and exp_action_end[key] > do):
# :do command starts before expanded commands.
act = None
if act:
# Correct the end for the already expanded actions.
for k in exp_action_end.keys():
if exp_action_end[k] > idx:
exp_action_end[k] = (exp_action_end[k]
+ len(cmd) - (idx - do))
# Set the end of the currently expanded action.
exp_action_end[key] = do + len(cmd)
# Replace the ":do" command with the commands of
# the action
commands = commands[:do] + cmd + commands[idx:]
# Continue at the start of the replaced commands,
# so that it works recursively.
idx = do
return commands
# vim: set sw=4 et sts=4 tw=79 fo+=l:
|