# Part of the A-A-P recipe executive: Parse and execute recipe commands

# Copyright (C) 2002 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

import string
import sys
import traceback

from Util import *
from Work import setrpstack
from Error import *
from RecPos import rpcopy
import Global

# ":" commands that only work at the toplevel
aap_cmd_toplevel = [
		"recipe",
		"rule",
		"variant",
		]

# All possible names of ":" commands in a recipe, including toplevel-only ones.
aap_cmd_names = [
		"action",
		"add",
		"addall",
		"attr",
		"attribute",
		"cat",
		"checkin",
		"checkinall",
		"checkout",
		"checkoutall",
		"checksum",
		"child",
		"chmod",
		"commit",
		"commitall",
		"copy",
		"del",
		"deldir",
		"delete",
		"do",
		"error",
		"eval",
		"execute",
		"exit",
		"export",
		"fetch",
		"fetchall",
		"filetype",
		"global",
		"include",
		"local",
		"mkdir",
		"mkdownload",
		"move",
		"print",
		"progsearch",
		"proxy",
		"publish",
		"publishall",
		"quit",
		"remove",
		"removeall",
		"reviseall",
		"start",
		"sys",
		"syseval",
		"syspath",
		"system",
		"tag",
		"tagall",
		"touch",
		"unlock",
		"unlockall",
		"update",
		"verscont",
		] + aap_cmd_toplevel

# marker for recipe line number in Python script
line_marker = '#@recipe='
line_marker_len = len(line_marker)


def assert_var_name(rpstack, name):
    """Check if "name" is a valid variable name.
       If it isn't, throw a user exception."""
    for c in name:
	if not varchar(c):
	    recipe_error(rpstack, _("Invalid character in variable name"))


def get_var_name(fp):
    """Get the name of a variable from the current position at "fp" and
    get the index of the next non-white after it.  Returns an empty string if
    there is no valid variable name."""
    idx = fp.idx
    while idx < fp.line_len and varchar(fp.line[idx]):
	idx = idx + 1
    return fp.line[fp.idx:idx], skip_white(fp.line, idx)


class ArgItem:
    """Object used as smallest part in the arglist."""
    def __init__(self, isexpr, str):
	self.isexpr = isexpr	    # 0 for string, 1 for Python expression
	self.str = str		    # the string itself


def getarg(fp, stop):
    """Get an argument starting at fp.line[fp.idx] and ending at a character in
       stop[].
       Quotes are used to include stop characters in the argument.
       Backticks are handled here.  `` is reduced to a single `.  A Python
       expression `python` is translated to '" + expr2str(python) + "'.
       Returns the resulting string and fp.idx is updated to the character
       after the argument (the stop character or past the end of line).
       """
    res = ''				# argument collected so far
    inquote = ''			# quote we're inside of
    inbraces = 0			# inside {} count
    while 1:
	if fp.idx >= fp.line_len:	# end of line
	    break

	# Python `expression`?
	if fp.line[fp.idx] == '`':
	    # `` isn't the start of an expression, reduce it to a single `.
	    if fp.idx + 1 < fp.line_len and fp.line[fp.idx + 1] == '`':
		res = res + '`'
		fp.idx = fp.idx + 2
		continue
	    # Append the Python expression.
	    res = res + '" + expr2str(' + get_py_expr(fp) + ') + "'
	    continue

	# End of quoted string?
	if inquote:
	    if fp.line[fp.idx] == inquote:
		inquote = ''

	# Start of quoted string?
	elif fp.line[fp.idx] == '"' or fp.line[fp.idx] == "'":
	    inquote = fp.line[fp.idx]

	else:
	    # start/end of {}?
	    if fp.line[fp.idx] == '{':
		inbraces = inbraces + 1
	    elif fp.line[fp.idx] == '}':
		inbraces = inbraces - 1
		if inbraces < 0:
		    # TODO: recipe_error(fp.rpstack, _("Unmatched }"))
		    inbraces = 0

	    # Stop character found?
	    # A ':' must be followed by white space to be recongized.
	    # A '=' must not be inside {}.
	    if string.find(stop, fp.line[fp.idx]) != -1 \
		    and (fp.line[fp.idx] != ':'
			    or fp.idx + 1 == fp.line_len
			    or fp.line[fp.idx + 1] == ' '
			    or fp.line[fp.idx + 1] == '\t') \
		    and (fp.line[fp.idx] != '=' or inbraces == 0):
		break

	# Need to escape backslash and double quote.
	c = fp.line[fp.idx]
	if c == '"' or c == '\\':
	    res = res + '\\'
	res = res + c
	fp.idx = fp.idx + 1

	# Skip over $$ and $#.
	if (c == '$' and fp.idx < fp.line_len
		       and (fp.line[fp.idx] == '$' or fp.line[fp.idx] == '#')):
	    res = res + fp.line[fp.idx]
	    fp.idx = fp.idx + 1

    # Remove trailing white space.
    e = len(res)
    while e > 0 and is_white(res[e - 1]):
	e = e - 1

    return res[:e]


def get_func_args(fp, indent):
    """Get the arguments for an aap_ function or assignment from the recipe
       line(s).
       Stop at a line with an indent of "indent" or less.

       Input lines:   cmdname arg ` <python-expr> `arg
			        arg
       Result:        "arg " + expr2str(<python-expr>) + "arg arg"

       Return the argument string, advance fp to the following line."""

    res = ''
    fp.idx = skip_white(fp.line, fp.idx)

    while 1:
	if fp.idx >= fp.line_len or fp.line[fp.idx] == '#':
	    # Read the next line
	    fp.nextline()
	    if fp.line is None:
		break	# end of file
	    fp.idx = skip_white(fp.line, 0)
	    if get_indent(fp.line) > indent:
		continue
	    # A line with less indent finishes the list of arguments
	    break

	# Get the argument, stop at a comment, handle python expression.
	# A line break is changed into a space.
	# When a line ends in "$br" insert a line break.  But not when it ends
	# in "$$br".  But do it for "$$$br".  Etc.
	if res:
	    if len(res) >= 3 and res[-3:] == "$br":
		i = len(res) - 4
		escaped = 0
		while i >= 0 and res[i] == '$':
		    i = i - 1
		    escaped = not escaped
		if escaped:
		    res = res + ' '
		else:
		    res = res[:-3] + '\\n'
	    else:
		res = res + ' '
	res = res + getarg(fp, "#")

    return '"' + res + '"'


def esc_quote(s):
    """Escape double quotes and backslash with a backslash."""
    return string.replace(string.replace(s, '\\', '\\\\'), '"', '\\"')


def get_commands(fp, indent):
    """Read command lines for a dependency or a rule.
       Stop when the indent is at or below "indent".
       Separate the part at the start with more indent than the smallest indent
       in the block (this is considered continuation of the first line).
       Returns the head and the string of commands, each line ending in '\n'."""
    # First get all lines, until the indent drops to "indent" or lower.
    # Also remember the line markers, empty lines may be skipped thus we can't
    # compute the line numbers from the index.
    markers = []
    lines = []
    min_indent = 99999
    min_indent_idx = 0	    # first line with the smallest indent, this is
			    # where the build commands start.
    while 1:
	if fp.line is None:
	    break	    # end of commands reached
	idt = get_indent(fp.line)
	if idt <= indent:
	    break	    # end of commands reached
	if idt < min_indent:
	    min_indent = idt
	    min_indent_idx = len(lines)
	markers.append(' ' + ("%s%d" % (line_marker, fp.rpstack[-1].line_nr)))
	lines.append(fp.line)
	fp.nextline()

    # Make the string for the head (no line breaks).
    # Need to use a ParsePos here, so that getarg() can be called.  It
    # handles Python expressions that may continue over a line break.
    head = ''
    str = ''
    for i in range(0, min_indent_idx):
	str = str + lines[i] + '\n'
    if str:
	from ParsePos import ParsePos
	pp = ParsePos(fp.rpstack, string = str)
	pp.nextline()
	while 1:
	    if pp.line is None:
		break
	    pp.index = skip_white(pp.line, 0)
	    head = head + ' ' + getarg(pp, "#")
	    pp.nextline()

    # Make the string for the commands (with markers and line breaks).
    s = ''
    for i in range(min_indent_idx, len(lines)):
	s = s + markers[i] + '\n'
	s = s + lines[i] + '\n'

    return head, '"""' + esc_quote(s) + '"""'


def get_py_expr(fp):
    """Get a Python expression from ` to matching `.
       Reduce `` to `.
       fp.idx points to the ` and is advanced to after the matching `.
       Returns the expression excluding the ` before and after."""
    # Remember the RecPos where the first ` was found; need to make a copy,
    # because fp.nextline() will change it.
    rpstack = rpcopy(fp.rpstack, fp.rpstack[-1].line_nr)

    res = ''
    fp.idx = fp.idx + 1
    while 1:
	if fp.idx >= fp.line_len:
	    # Python expression continues in the next line.
	    fp.nextline()
	    if fp.line is None:
		recipe_error(rpstack, _("Missing `"))
	    res = res + '\n'
	if fp.line[fp.idx] == '`':
	    # Either the matching ` or `` that stands for a single `.
	    fp.idx = fp.idx + 1
	    if fp.idx >= fp.line_len or fp.line[fp.idx] != '`':
		break	    # found matching `
	    res = res + '`'
	else:
	    # Append a character to the Python expression.
	    res = res + fp.line[fp.idx]
	fp.idx = fp.idx + 1

    return res


def get_error_msg(rpstack, msg):
    # Note: This messages is not translated, so that a parser for the
    # messages isn't confused by various languages.
    if not rpstack:
	return 'Error in recipe: %s' % msg
    return 'Error in recipe "%s" line %d: %s' \
				 % (rpstack[-1].name, rpstack[-1].line_nr, msg)


def recipe_error(rpstack, msg):
    """Throw an exception for an error in a recipe:
	    Error: Unknown command
	    in recipe "main.aap" line 88: :foobar asdf
	    included from "main.aap" line 33
       When "rpstack" is empty it's not mentioned, useful for errors
       not related to a specific line."""
    e = get_error_msg(rpstack, msg) + '\n'
    if len(rpstack) > 1:
	for i in range(len(rpstack) - 2, 0, -1):
	    e = e + 'included from "%s" line %d\n' \
					% (rpstack[i].name, rpstack[i].line_nr)
    e = e[:-1]	    # remove trailing \n

    raise UserError, e


def option_error(rpstack, dict, cmd):
    """Give an error message for options in dictionary "dict"."""
    if len(dict.keys()) > 1:
	recipe_error(rpstack, _("Illegal options for %s command: %s")
						     % (cmd, str(dict.keys())))
    else:
	recipe_error(rpstack, _("Illegal option for %s command: %s")
						       % (cmd, dict.keys()[0]))

def get_line_marker(s):
    """Check for the line marker comment at string "s".
       Return the line number if found, None otherwise."""
    if s[:line_marker_len] == line_marker:
	j = line_marker_len
	try:
	    while s[j] in string.digits:
		j = j + 1
	except:
	    pass
	return int(s[line_marker_len:j])
    return None


def get_block_term(fp, cmd):
    """Get the argument used to terminate a block of lines.
       Used for ":python EOF" and "var << EOF"."""
    fp.idx = skip_white(fp.line, fp.idx)
    n = skip_to_white(fp.line, fp.idx)
    if n == fp.idx:
	recipe_error(fp.rpstack, _("Missing item for %s") % cmd)
    term = fp.line[fp.idx:n]
    n = skip_white(fp.line, n)
    if n < fp.line_len and fp.line[n] != '#':
	recipe_error(fp.rpstack, _("Too many arguments for %s") % cmd)
    return term

def get_block_lines(fp, term, indent, cmd):
    """Get the lines for a block of lines followed by terminator "term".
       Stop when "term" is empty and the indent drops below "indent".
       Used for ":python EOF" and "var << EOF"."""
    lines = ''
    start_line_nr = fp.rpstack[-1].line_nr
    term_len = len(term)
    first_indent = -1

    while 1:
	fp.nextline()
	if fp.line is None:
	    if not term:
		break
	    fp.rpstack[-1].line_nr = start_line_nr
	    recipe_error(fp.rpstack, _("Unterminated %s block") % cmd)

	# Need to know the indent of the first line.
	if first_indent == -1:
	    first_indent = get_indent(fp.line)

	if not term:
	    # No terminator defined: end when indent is smaller.
	    if get_indent(fp.line) <= indent:
		break
	else:
	    # Terminator defined: end when it's found.
	    n = skip_white(fp.line, 0)
	    if n < fp.line_len and fp.line[n:n + term_len] == term:
		n = skip_white(fp.line, n + term_len)
		if n >= fp.line_len or fp.line[n] == "#":
		    fp.nextline()
		    break

	if cmd == ":python":
	    # Append the recipe line number, used for error messages.
	    lines = lines + ("%s%d\n" % (line_marker, fp.rpstack[-1].line_nr))
	    lines = lines + fp.line + '\n'
	else:
	    # Remove the indent by as much as the indent of the first line.
	    ind = 0
	    i = 0
	    while ind < first_indent and i < len(fp.line):
		if fp.line[i] == ' ':
		    ind = ind + 1
		elif fp.line[i] == '\t':
		    ind = ind + 8 - ind % 8
		else:
		    break
		i = i + 1
	    if ind < first_indent:
		recipe_error(fp.rpstack, _("Not enough indent in %s block")
									 % cmd)
	    line = (' ' * (ind - first_indent)) + fp.line[i:]
	    lines = lines + line + '\n'

    return lines, first_indent


def script_error(rpstack, recdict, script):
    """Handle an error raised while executing the Python script produced for a
    recipe.  The error is probably in the recipe, try to give a useful error
    message."""
    etype, evalue, tb = sys.exc_info()

    # A SyntaxError is special: it's not the last frame in the traceback but
    # only in the "etype" and "evalue".  When there is a filename it must have
    # been an internal error, otherwise it's an error in the converted recipe.
    py_line_nr = -1
    if etype is SyntaxError:
	try:
	    msg, (filename, py_line_nr, offset, line) = evalue
	    if not filename is None:
		py_line_nr = -2
	except:
	    pass

    if py_line_nr < 0:
	# Find the line number in the last traceback frame.
	while tb.tb_next:
	    tb = tb.tb_next
	fname = tb.tb_frame.f_code.co_filename
	if py_line_nr == -2 or (fname and not fname == "<string>"):
	    # If there is a filename, it's not an error in the script.
	    from Main import error_msg
	    error_msg(recdict, _("Internal Error"))
	    traceback.print_exc()
	    sys.exit(1)

	py_line_nr = traceback.tb_lineno(tb)

    # Translate the line number in the Python script to the line number
    # in the recipe.
    i = 0
    script_len = len(script)
    rec_line_nr = 1
    while 1:
	if py_line_nr == 1:
	    break
	while i < script_len:
	    if script[i] == '\n':
		break
	    i = i + 1
	i = i + 1
	if i >= script_len:
	    break
	n = get_line_marker(script[i:])
	if not n is None:
	    rec_line_nr = n
	py_line_nr = py_line_nr - 1

    # Give the exception error with the line number in the recipe.
    recipe_py_error(rpcopy(rpstack, rec_line_nr), '')


def recipe_py_error(rpstack, msg):
    """Turn the list from format_exception_only() into a simple string and pass
    it to recipe_error()."""
    etype, evalue, tb = sys.exc_info()

    lines = traceback.format_exception_only(etype, evalue)

    # For a syntax error remove the "<string>" and line number that the 
    # Python script causes.
    if etype is SyntaxError:
	try:
	    emsg, (filename, lineno, offset, line) = evalue
	    if filename is None:
		lines[0] = '\n'
	except:
	    pass

    str = msg
    for line in lines[:-1]:
	str = str + line + ' '
    str = str + lines[-1]
    recipe_error(rpstack, str)


def Process(fp, recdict):
    """Read all the lines in ParsePos "fp", convert it into a Python script and
       execute it.
       When "fp.string" is empty, the source is a recipe file, otherwise it is
       a string (commands from a dependency or rule)."""

    # Need to be able to find the RecPos stack in recdict.
    setrpstack(recdict, fp.rpstack)

    class Variant:
	"""Class used to remember nested ":variant" commands in
	   variant_stack."""
	def __init__(self, name, indent, line_nr):
	    self.name = name
	    self.min_indent = indent
	    self.line_nr = line_nr
	    self.val_indent = 0
	    self.had_star = 0	    # encountered * item

    #
    # At the start of the loop "fp.line" contains the next line to be
    # processsed.  "fp.rpstack[-1].line_nr" is the number of this line in the
    # recipe.
    #
    script = ""
    shell_cmd = ""	    # shell command collected so far
    shell_cmd_indent = 0    # avoid PyChecker warning
    shell_cmd_line_nr = 0   # avoid PyChecker warning
    variant_stack = []	    # nested ":variant" commands
    had_recipe_cmd = 0	    # encountered ":recipe" command
    fp.nextline()	    # read the first line

    while 1:

	# Skip leading white space (unless at end of file).
	if not fp.line is None:
	    indent = get_indent(fp.line)
	    fp.idx = skip_white(fp.line, 0)

	# If it's not a shell command and the previous line was, generate the
	# collected shell commands now.
	if shell_cmd:
	    if (fp.line is None \
		    or indent < shell_cmd_indent \
		    or fp.line[fp.idx:fp.idx + 4] != ":sys"):
		script = script + (' ' * shell_cmd_indent) \
			      + ('aap_shell(%d, globals(), "%s")\n'
					      % (shell_cmd_line_nr, shell_cmd))
		shell_cmd = ''
	elif not fp.line is None:
	    # Append the recipe line number, used for error messages.
	    script = script + ("%s%d\n" % (line_marker, fp.rpstack[-1].line_nr))


	#
	# Handle the end of commands in a variant or the end of a variant
	#
	if len(variant_stack) > 0:
	    v = variant_stack[-1]
	    if fp.line is None or indent <= v.min_indent:
		# End of the :variant command.
		if v.val_indent == 0:
		    recipe_error(fp.rpstack,
				  _("Exepected list of values after :variant"))
		if not v.had_star:
		    script = script + (' ' * v.min_indent) + "else:\n"
		    script = (script + (' ' * v.min_indent)
			      + "  aap_error(%d, globals(), 'invalid value for %s: %%s' %% %s)\n"
				  % (v.line_nr, v.name, v.name))

		del variant_stack[-1]
		if len(variant_stack) > 0:
		    continue    # another may end here as well
	    else:
		if v.val_indent == 0:
		    v.val_indent = indent
		    first = 1
		else:
		    first = 0
		if indent <= v.val_indent:
		    # Start of a variant value: "debug [ condition ]"
		    # We simply ignore the condition here.
		    if v.had_star:
			recipe_error(fp.rpstack,
					  _("Variant item * must be last one"))
		    if fp.idx < fp.line_len and fp.line[fp.idx] == '*':
			if (fp.idx + 1 < fp.line_len
					and not is_white(fp.line[fp.idx + 1])):
			    recipe_error(fp.rpstack, _("* must be by itself"))
			if not first:
			    script = script + (' ' * v.min_indent) + "else:\n"
			v.had_star = 1
		    else:
			val, n = get_var_name(fp)
			if val == '':
			    recipe_error(fp.rpstack,
						  _("Exepected variant value"))
			if first:
			    # Specify the default value and change $BDIR.
			    script = script + (' ' * v.min_indent) + (
				  'if not globals().has_key("%s"):\n' % v.name)
			    script = script + (' ' * v.min_indent) + (
					       '  %s = "%s"\n' % (v.name, val))
			    script = script + (' ' * v.min_indent) + (
					   "BDIR = BDIR + '-' + %s\n" % v.name)

			script = script + (' ' * v.min_indent)
			if not first:
			    script = script + "el"
			script = script + ("if %s == '%s':\n" % (v.name, val))
		    fp.nextline()
		    if fp.line is None or get_indent(fp.line) <= v.val_indent:
			script = script + (' ' * v.min_indent) + "  pass\n"
		    continue

	#
	# Stop at the end of the file.
	#
	if fp.line is None:
	    break

	#
	# A Python block
	#
	#  recipe:    :python EOF
	#		    command
	#		    command
	#		  EOF
	#  Python:	  if 1:
	#		     command
	#		     command
	#
	if fp.line[fp.idx:fp.idx + 7] == ":python":
	    fp.idx = skip_white(fp.line, fp.idx + 7)
	    if fp.idx >= fp.line_len or fp.line[fp.idx] == '#':
		term = ''
	    else:
		term = get_block_term(fp, ":python")

	    # Get the lines until the terminator and append them to the script.
	    # If the indent of the Python block is more than the current
	    # indent, insert an ":if 1".
	    lines, first_indent = get_block_lines(fp, term, indent, ":python")
	    if first_indent > indent:
		script = script + (indent * ' ') + "if 1:" + '\n'
	    script = script + lines


	    continue

	#
	# An A-A-P command
	#
	#  recipe:  :cmd arg arg
	#                  arg
	#  Python:  aap_cmd(123, globals(), "arg arg arg")
	#
	if fp.line[fp.idx] == ":":
	    s = fp.idx
	    fp.idx = fp.idx + 1
	    e = skip_to_white(fp.line, fp.idx)
	    cmd_name = fp.line[fp.idx:e]
	    fp.idx = skip_white(fp.line, e)

	    # Check if this is a valid command name.  The error is postponed
	    # until executing the line, so that "@if aapversion > nr" can be
	    # used before it.
	    if cmd_name not in aap_cmd_names:
		cmd_name = "unknown"
		fp.idx = s
	    if fp.string and cmd_name in aap_cmd_toplevel:
		cmd_name = "nothere"
		fp.idx = s

	    #
	    # To avoid starting a shell for every single command, collect
	    # system commands until encountering another command.
	    #
	    # recipe:       :system one
	    #		    :sys  two
	    #			    three
	    # Python:	    aap_shell(123, globals(), "one\ntwo three\n")
	    #
	    if cmd_name == "system" or cmd_name == "sys":
		if not shell_cmd:
		    shell_cmd_line_nr = fp.rpstack[-1].line_nr
		    shell_cmd_indent = indent
		shell_cmd = shell_cmd + getarg(fp, "#")

		# get the next line and continue until the indent is
		while 1:
		    fp.nextline()
		    if fp.line is None or get_indent(fp.line) <= indent:
			break	    # end of commands reached
		    fp.idx = skip_white(fp.line, 0)
		    shell_cmd = shell_cmd + ' ' + getarg(fp, "#")

		shell_cmd = shell_cmd + '\\n'
		continue

	    # recipe:   :variant VAR
	    #               foo  [ condition ]
	    #                  cmds
	    #               *    [ condition ]
	    #                  cmds
	    # Python:   if VAR == "foo":
	    #                  cmds
	    #           else:
	    #                  cmds
	    #           BDIR = BDIR + '-' + VAR
	    # This is complicated, because "cmds" can be any command, and
	    # variants may nest.  Store the info about the variant in
	    # variant_stack and continue, the rest is handled above.
	    if cmd_name == "variant":
		var_name, n = get_var_name(fp)
		if var_name == '' or (n < fp.line_len and fp.line[n] != '#'):
		    recipe_error(fp.rpstack,
				   _("Expected variable name after :variant"))
		variant_stack.append(Variant(var_name, indent,
						       fp.rpstack[-1].line_nr))

		# get the next line
		fp.nextline()
		continue

	    # Generate a call to the Python function for this command.
	    script = script + (indent * ' ') + ('aap_%s(%d, globals(), '
					  % (cmd_name, fp.rpstack[-1].line_nr))

	    if cmd_name == "rule":
		# recipe:   :rule  target : {attr} source
		#                 commands
		# Python:   aap_rule(123, globals(), "target", "source",
		#                      124, """commands""")
		target = getarg(fp, ":#")
		if fp.idx >= fp.line_len or fp.line[fp.idx] != ':':
		    recipe_error(fp.rpstack, _("Missing ':' after :%s")
								% cmd_name)
		fp.idx = fp.idx + 1
		script = script + ('"%s", ' % target)

		source = getarg(fp, "#")

		cmd_line_nr = fp.rpstack[-1].line_nr
		fp.nextline()
		head, cmds = get_commands(fp, indent)

		script = script + ('"%s", %d, %s)\n'
					  % (source + head, cmd_line_nr, cmds))

	    elif cmd_name in [ "filetype", "action" ]:
		# recipe:   :filetype [filename]
		#		detection-lines
		#           :action action ftype command
		#           :action action ftype
		#		commands
		# Python:   aap_command(123, globals(), "arg",
		#				    """detection-lines""")
		#
		arg = getarg(fp, "#")
		cmd_line_nr = fp.rpstack[-1].line_nr
		fp.nextline()
		head, cmds = get_commands(fp, indent)

		script = (script + '"%s", %d, %s)\n'
					     % (arg + head, cmd_line_nr, cmds))

	    else:
		# get arguments that may continue on the next line
		script = script + get_func_args(fp, indent) + ")\n"

		# When a ":recipe" command is encountered that will probably
		# be executed, make a copy of the recdict at the start, so that
		# this can be restored when executing the updated recipe.
		# This is a "heavy" command, only do it when needed.
		from Commands import maydo_recipe_cmd
		if (cmd_name == "recipe" and not had_recipe_cmd
					     and maydo_recipe_cmd(fp.rpstack)):
		    had_recipe_cmd = 1
		    script = ('globals()["_start_recdict"] = globals().copy()\n'
			    + script)

	    continue

	#
	# A Python command
	#
	#  recipe:    @command args
	#  Python:	  command args
	#
	if fp.line[fp.idx] == "@":
	    if fp.idx + 1 < fp.line_len:
		if fp.line[fp.idx + 1] == ' ' \
					    or fp.line[fp.idx + 1] == '\t':
		    # followed by white space: replace @ with a space
		    script = script \
			      + string.replace(fp.line, '@', ' ', 1) + '\n'
		else:
		    # followed by text: remove the @
		    script = script \
			       + string.replace(fp.line, '@', '', 1) + '\n'

	    # get the next line
	    fp.nextline()
	    continue

	#
	# Assignment
	#
	#  recipe:   name = $VAR {attr=val} ` glob("*.c") `
	#                     two
	#  Python:	 name = aap_assign(123, globals(),
	#			"$VAR {attr=val} " + glob("*.c") + " two", 1)
	#
	#  var = value	    assign
	#  var += value	    append (assign if not set yet)
	#  var ?= value	    only assign when not set yet
	#  var $= value	    evaluate when used
	#  var $+= value    append, evaluate when used
	#  var $?= value    only when not set, evaluate when used
	#  var $+<< EOF	    assign multiple lines
	#       value1
	#	value2
	#      EOF
	var_name, n = get_var_name(fp)

	if var_name != '' and n < fp.line_len:
	    nc = fp.line[n]
	    ec = nc
	    if ec == '$' and n + 1 < fp.line_len:
		ne = n + 1
		ec = fp.line[ne]
	    else:
		ne = n
	    if (ec == '+' or ec == '?') and ne + 1 < fp.line_len:
		lc = ec
		ne = ne + 1
		ec = fp.line[ne]
	    else:
		lc = ''
	    if ec == '=' or ec == '<':
		if ec == '<':
		    # Get the term string from "var << term".
		    if ne + 1 >= fp.line_len or fp.line[ne + 1] != '<':
			recipe_error(fp.rpstack, _("Single < not allowed"))
		    fp.idx = ne + 2
		    term = get_block_term(fp, "<<")

		# When changing $CACHE need to flush the cache and reload it.
		if var_name == "CACHE":
		    script = (script + (indent * ' ')
						  + "flush_cache(globals())\n")
		fp.idx = skip_white(fp.line, ne + 1)
		script = script + (indent * ' ') + (
				     "aap_assign(%d, globals(), '%s', "
					  % (fp.rpstack[-1].line_nr, var_name))
		if ec == '<':
		    args, dummy = get_block_lines(fp, term, 0, "assignment")
		    args = '"""' + args + '"""'
		else:
		    args = get_func_args(fp, indent)
		script = script + args + (", '%s', '%s')\n" % (nc, lc))
		continue

	#
	# If there is no ":" following we don't know what it is.
	#
	targets = getarg(fp, ":#")
	if fp.idx >= fp.line_len or fp.line[fp.idx] != ':':
	    recipe_error(fp.rpstack, _("No recognized item"))

	if fp.string:
	    recipe_error(fp.rpstack, _("Dependency not allowed here"))

	#
	# Dependency
	#
	#  recipe:     target target : source source
	#                    commands
	#  Python:     aap_depend(123, globals(), list-of-targets,
	#			      list-of-sources, "commands")
	#
	else:
	    # Skip the ':' and get the list of sources.
	    fp.idx = skip_white(fp.line, fp.idx + 1)
	    sources = getarg(fp, '#')
	    nr = fp.rpstack[-1].line_nr
	    fp.nextline()

	    # get the commands and the following line
	    head, cmds = get_commands(fp, indent)
	    sources = sources + head

	    script = (script + (indent * ' ')
			+ ('aap_depend(%d, globals(), "%s", "%s", %d, '
			      % (nr, targets, sources, fp.rpstack[-1].line_nr))
			+ cmds + ')\n')

	#
	# End of loop over all lines in recipe.
	#


    if fp.string:
	# When parsing a string need to take care of the indent.
	if is_white(fp.string[0]):
	    script = "if 1:\n" + script
    else:
	# Close the file before executing the script, so that ":recipe" can
	# overwrite the file.
	fp.file.close()

    # Prepend the default imports.
    script = "from Commands import *\n" \
		+ "from Port import *\n" \
		+ "from glob import glob\n" \
		+ script

    # Need to be able to find the globals from invoked functions.
    save_globals = Global.globals
    Global.globals = recdict
    
    # DEBUG
    #msg_warning(recdict, script)

    #
    # Execute the resulting Python script.
    # Give a useful error message when something is wrong.
    #
    try:
	exec script in recdict, recdict
    except StandardError, e:
	script_error(fp.rpstack, recdict, script)

    # Restore the old globals.
    Global.globals = save_globals

    # DEBUG
    #except UserError:
	#msg_warning(recdict, script)
	#raise


# vim: set sw=4 sts=4 tw=79 fo+=l:
