# Part of the A-A-P recipe executive: Add dependencies for a port recipe

# 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

#
# This file defines the code used for a port recipe.
#

import os.path
import string

from Action import action_run
from Depend import Depend
from Dictlist import str2dictlist, dictlist2str
from DoRead import goto_dir
from Message import *
from Process import recipe_error
from RecPos import RecPos
from Sign import check_md5
from Util import *
from VersCont import repl_file_name, handle_nodelist
from Work import getwork
import Global

# List of "*depend" target names that "dependcheck" needs to use.
depend_list = []

# Last target that will be used.
last_target = None

# Whether last_target was used, no further dependencies to be checked.
last_target_found = 0

cvs_done_file = "done/cvs-yes"	    # file created when CVS was used.
cvs_notdone_file = "done/cvs-no"    # file created when archives were used.

def check_port_dep(work, name):
    n = work.find_node(name)
    if n and n.get_dependencies():
	recipe_error([],
	    _('"%s" target already defined while $PORTNAME is present') % name)


def add_port_dep(work, rpstack, item, previtem):
    """Add one of the port dependencies "item".
       It will depend on "previtem"."""
#
# Note: all build commands have a hard coded minimal indent of two spaces.
#
    # Don't do anything when the "done/<name>" file exists.
    if item.done_check and os.path.exists("done/" + item.done_check):
	cmds = ''
    else:
	# Update "pre-name" when it was defined.
	n = work.find_node("pre-" + item.name)
	if n:
	    n.set_attributes({"virtual": 1})
	    cmds = "  :update pre-" + item.name + '\n'
	else:
	    cmds = ''

	# Update "do-name" when it was defined, use the default commands
	# otherwise.
	n = work.find_node("do-" + item.name)
	if n:
	    n.set_attributes({"virtual": 1})
	    cmds = cmds + ('  :update do-%s\n' % item.name)
	else:
	    cmds = cmds + ('  @port_%s(globals())\n' % item.name)

	# Update "post-name" when it was defined.
	n = work.find_node("post-" + item.name)
	if n:
	    n.set_attributes({"virtual": 1})
	    cmds = cmds + "  :update post-" + item.name + '\n'
	
	# Create the "done" file, except for "install" and "*depend".
	if item.done_create:
	    cmds = cmds + "  :mkdir {force} done\n"
	    cmds = cmds + "  :touch {force} done/" + item.name + '\n'

	# For "install" handle the runtime dependencies after installing the
	# package, reduces cyclic dependencies.  Then run the post-install
	# tests.
	# TODO: if this fails, uninstall the package?
	if item.name == "install":
	    cmds = cmds + "  :update rundepend\n"
	    cmds = cmds + "  :update installtest\n"

	#
	# Add "*depend" items to the list of dependencies to be satisfied.
	# This stops at the last target that will be build.
	#
	global last_target, last_target_found
	if item.name[-6:] == "depend" and not last_target_found:
	    global depend_list
	    depend_list.append(item.name)
	    if item.name == last_target:
		last_target_found = 1

    targets = [{"name": item.name, "virtual" : 1}]
    if previtem:
	sources = [{"name": previtem.name}]
    else:
	sources = []
    work.add_dependency(rpstack, Depend(targets, {}, sources, work,
					rpstack, cmds, recdict = work.recdict))


class port_step:
    def __init__(self, name, done_check, done_create):
	self.name = name
	self.done_check = done_check	# "done" file to check for existence
	self.done_create = done_create	# flag: create "done" file if success

def add_port_defaults(work):
    """Add the dependencies used for a port recipe."""
    # The list of virtual targets to be build for a port.
    deplist =  [port_step("dependcheck", "", 0),
		port_step("fetchdepend", "checksum", 0),
		port_step("fetch", "fetch", 1),
		port_step("checksum", "checksum", 1),
		port_step("extractdepend", "patch", 0),
		port_step("extract", "extract", 1),
		port_step("patch", "patch", 1),
		port_step("builddepend", "build", 0),
		port_step("configure", "configure", 1),
		port_step("build", "build", 1),
		port_step("testdepend", "test", 0),
		port_step("test", "test", 1),
		port_step("package", "package", 1),
		port_step("install", "", 0)]

    # Check if the user didn't accidentally overruled one of the targets.
    for item in deplist:
	check_port_dep(work, item.name)
    check_port_dep(work, "rundepend")
    check_port_dep(work, "installtest")

    for name in ["PORTVERSION", "PORTCOMMENT", "PORTDESCR"]:
	if not get_var_val(0, work.recdict, name):
	    if work.recdict.has_key(name):
		recipe_error([], _('Empty variable "%s"') % name)
	    else:
		recipe_error([], _('Missing variable "%s"') % name)

    msg_depend(work.recdict, _('Adding dependencies for port recipe'))

    #
    # Add the "all" target used for a port recipe.
    # This builds the port but doesn't test or install it.
    # Testing would be nice, but requires "rundepend" to be done.
    #
    rpstack = [RecPos("Default port target")]
    work.add_dependency(rpstack, Depend([{"name": "all"}], {},
	      [{"name" : "build"}], work, rpstack, '', recdict = work.recdict))

    #
    # Find which target is the last to be done.  Allows checking only the
    # dependencies that are actually used.
    #
    global last_target
    for item in deplist:
	if item.name in Global.cmd_args.targets:
	    last_target = item.name

    #
    # Add a dependency for each target.  It depends on the previous one.
    #
    previtem = None
    for item in deplist:
	add_port_dep(work, rpstack, item, previtem)
	previtem = item

    # "rundepend" and "installtest" are updated from inside "install", need to
    # create its dependency separately.
    add_port_dep(work, rpstack, port_step("rundepend", "", 0), None)
    add_port_dep(work, rpstack, port_step("installtest", "", 0), None)

    #
    # Add dependencies that are used individually.
    #
    indeplist = [port_step("clean", "", 0),
		 port_step("distclean", "", 0),
		 port_step("uninstall", "", 0),
		 port_step("makesum", "", 0),
		 port_step("srcpackage", "", 0)]
    for item in indeplist:
	add_port_dep(work, rpstack, item, None)

    #
    # Set default values for variables.
    #
    rd = work.recdict

    if use_cvs(rd):
	# Set $WRKSRC to the value used for CVS.
	dir = get_var_val(0, rd, "CVSWRKSRC")
	if dir:
	    msg_extra(rd, _("Using $CVSWRKSRC for $WRKSRC: %s") % dir)
	    rd["WRKSRC"] = dir
	else:
	    modules = str2dictlist([], get_var_val(0, rd, "CVSMODULES"))
	    s = modules[0]["name"]
	    msg_extra(rd, _("Using first CVS module for $WRKSRC: %s") % s)
	    rd["WRKSRC"] = s
	    rd["CVSWRKSRC"] = s
    else:
	if not get_var_val(0, rd, "WRKSRC"):
	    s = (get_var_val(0, rd, "PORTNAME") + "-"
					   + get_var_val(0, rd, "PORTVERSION"))
	    rd["WRKSRC"] = s
	    msg_extra(rd, _("Using $PORTNAME for $WRKSRC: %s") % s)


def port_clean(recdict, dist = 0):
    """Implementation of the "clean" target.
       Also used for "distclean", "dist" is non-zero then."""
    list = ["done",
		get_var_val(0, recdict, "WRKDIR"),
		get_var_val(0, recdict, "PKGDIR"),
		"pkg-plist", "pkg-comment", "pkg-descr"]
    if dist:
	list.extend([get_var_val(0, recdict, "DISTDIR"),
		get_var_val(0, recdict, "PATCHDISTDIR"),
		get_pkgname(recdict) + ".tgz",
		Global.aap_dirname])

    for dir in list:
	if os.path.exists(dir):
	    try:
		deltree(dir)
	    except StandardError, e:
		recipe_error([], (_('Cannot delete "%s": ') % dir) + str(e))


def port_distclean(recdict):
    """Implementation of the "distclean" target."""
    port_clean(recdict, 1)


def port_uninstall(recdict):
    """Implementation of the "uninstall" target."""
    msg_info(recdict, _("TODO: uninstall"))


def port_makesum(recdict):
    """Implementation of the "makesum" target."""
    # Get the recipe name.
    work = getwork(recdict)
    if not work.top_recipe:	# cannot happen?
	recipe_error([], _("No recipe specified to makesum for"))

    #
    # Get the total list of files and compute the checksum for each.
    #
    lines = []
    for name, dir in [("DISTFILES", "DISTDIR"), ("PATCHFILES", "PATCHDISTDIR")]:
	files = []
	# Get the normal list of files and then the CVS list (if defined).
	for varname in [name, "CVS" + name]:
	    f = get_var_val(0, recdict, varname)
	    if f:
		for i in str2dictlist([], f):
		    n = os.path.basename(i["name"])
		    if not n in files:
			files.append(n)

	# Make a recipe line for for each file, containing the md5 checksum.
	# TODO: use another kind of checksum when specified.
	for f in files:
	    n = os.path.join(get_var_val(0, recdict, dir), f)
	    if not os.path.exists(n):
		recipe_error([], _('File does not exists: "%s"') % n)
	    sum = check_md5(recdict, n)
	    if sum == "unknown":
		recipe_error([], _('Cannot compute checksum for "%s"') % n)
	    lines.append("\t:checksum $%s/%s {md5 = %s}\n" % (dir, f, sum))

    #
    # Replace or append the lines in/to the recipe
    #
    startline = '#>>> automatically inserted by "aap makesum" <<<\n'
    endline = '#>>> end <<<\n'

    # Open the original recipe file.
    try:
	fr = open(work.top_recipe)
    except StandardError, e:
	recipe_error([], (_('Cannot open recipe file "%s": ')
						   % work.top_recipe) + str(e))
    # Create a new file for the updated recipe.
    i = 1
    while 1:
	temp = work.top_recipe + str(i)
	if not os.path.exists(temp):
	    break
	i = i + 1
    try:
	fw = open(temp, "w")
    except StandardError, e:
	recipe_error([], (_('Cannot create temp file "%s": ') % temp) + str(e))

    def write_checksum_lines(fw, lines, endl):
	fw.write("do-checksum:\n")
	if lines:
	    fw.writelines(lines)
	else:
	    fw.write("\t@pass\n")
	fw.write(endl) 

    #
    # Copy original to updated recipe, replacing the makesum block.
    #
    try:
	added = 0
	while 1:
	    line = fr.readline()
	    if not line:
		break
	    fw.write(line)
	    if line == startline:
		if added:
		    recipe_error([], _("Duplicate makesum start marker"))
		added = 1
		write_checksum_lines(fw, lines, endline)
		while 1:
		    line = fr.readline()
		    if not line:
			recipe_error([], _("Missing makesum end marker"))
		    if line == endline:
			break
	if not added:
	    # makesum block not found, add it at the end.
	    fw.write(startline)
	    write_checksum_lines(fw, lines, endline)

	fr.close()
	fw.close()
    except (StandardError, UserError), e:
	try:
	    fw.close()
	except:
	    pass
	os.remove(temp)
	recipe_error([], _('Error while copying recipe file: ') + str(e))

    #
    # Rename original to backup and update to original recipe.
    # Most of the work is giving appropriate message when something goes wrong.
    # TODO: preserve protection bits.
    #
    bak = work.top_recipe + "~"
    if os.path.exists(bak):
	try:
	    os.remove(bak)
	except StandardError, e:
	    try:
		os.remove(temp)
	    except:
		pass
	    recipe_error([], (_('Cannot delete backup recipe "%s": ') % bak)
								      + str(e))
    try:
	os.rename(work.top_recipe, bak)
    except StandardError, e:
	try:
	    os.remove(temp)
	except:
	    pass
	recipe_error([], (_('Cannot rename recipe "%s" to "%s": ')
					    % (work.top_recipe, bak)) + str(e))
    try:
	os.rename(temp, work.top_recipe)
    except StandardError, e:
	recipe_error([], (_('Cannot rename recipe to "%s": ')
						   % work.top_recipe) + str(e))
	# renaming failed, try putting the original recipe back.
	try:
	    os.rename(bak, work.top_recipe)
	except StandardError, e:
	    recipe_error([], (_('Cannot rename recipe! It is now called "%s": ')
							       % bak) + str(e))
    try:
	os.remove(temp)
    except StandardError, e:
	msg_warning(recdict, (_('Cannot delete temp file "%s": ') % temp)
								      + str(e))


def port_srcpackage(recdict):
    """Implementation of the "srcpackage" target."""
    msg_info(recdict, _("TODO: srcpackage"))


# Cached list of packages.
# TODO: Needs to be flushed if a package is installed!
all_packages = None

def get_installed(recdict):
    """Return a list of all installed packages."""
    # TODO: this currently only works for systems with pkg_info.
    # TODO: pkg_info is slow, because it obtains descriptions.  Can we just get
    # the contents of /var/db/pkg?
    global all_packages
    if all_packages is None:
	ok, text = redir_system_int(recdict, "pkg_info -aI", use_tee = 0)
	if not ok:
	    msg_error(recdict, _("Could not obtain list of installed packages"))
	else:
	    # Get the package name from the line "name-9.9  description".
	    all_packages = []
	    lines = string.split(text, '\n')
	    for l in lines:
		items = string.split(l, None, 1)
		if items:
		    all_packages.append(items[0])
    return all_packages


def depend_matches(plist, pat):
    """Select the items from list "plist" that match pattern "pat"."""
    import fnmatch

    res = []
    for i in plist:
	# TODO: don't match "vimxx" when looking for "vim", do match "vim-1.1".
	if fnmatch.fnmatchcase(i, pat + "*"):
	    res.append(i)
    return res

def depend_item_match(name, op, pat):
    """Return non-zero if package "name" matches with operation "op" and
    pattern "pat"."""
    # Find the part of "name" until the version number.
    ns = 0
    while ns < len(name) - 1:
	ns = ns + 1
	if name[ns - 1] == '-' and name[ns] in string.digits:
	    break

    ps = 0
    while ns < len(name) and ps < len(pat):
	# Compare each dot separated part.
	# First get the digits.
	ne = ns
	while ne < len(name) and name[ne] in string.digits:
	    ne = ne + 1
	name_part = name[ns:ne]

	pe = ps
	while pe < len(pat) and pat[pe] in string.digits:
	    pe = pe + 1
	pat_part = pat[ps:pe]

	# Fill with zeros so that 9 is smaller than 10.
	while len(name_part) < len(pat_part):
	    name_part = '0' + name_part
	while len(name_part) > len(pat_part):
	    pat_part = '0' + pat_part
	
	# Add text until a dot.
	while ne < len(name) and name[ne] != ".":
	    name_part = name_part + name[ne]
	    ne = ne + 1
	while pe < len(pat) and pat[pe] != ".":
	    pat_part = pat_part + pat[pe]
	    pe = pe + 1

	# Compare.
	if op[0] == "<":
	    if name_part > pat_part:
		return 0
	    if name_part < pat_part:
		return 1
	elif op[0] == ">":
	    if name_part < pat_part:
		return 0
	    if name_part > pat_part:
		return 1
	
	# Advance to the next part, skip over the dot.
	if ne < len(name):
	    ne = ne + 1
	ns = ne
	if pe < len(pat):
	    pe = pe + 1
	ps = pe

    if op == "<" and name[ns:] >= pat[ps:]:
	return 0
    if op == ">" and name[ns:] <= pat[ps:]:
	return 0
    return 1

def part_end(name, depends, idx):
    """Find the end of the next part in a depends item, up to the next "<",
    "!", etc.  Return the index of the following char."""
    e = idx
    while (e < len(depends)
	    and not is_white(depends[e])
	    and not depends[e] in "><!)("):
	e = e + 1
    if e == idx:
	recipe_error([], _("Syntax error in %s") % name)
    return e

def part_remove(name, depends, idx, matching, pname):
    """Handle following parts of an item "depends[idx:]".
       Remove packages from "matching" according to the parts.
       Stop when encountering white space or an unmatched ')'.
       Return the index of the next char and the remaining matches."""
    while (idx < len(depends)
	    and not is_white(depends[idx])
	    and not depends[idx] in '()'):

	braces = 0
	if depends[idx] == '!':
	    next_op = '!'
	    idx = idx + 1
	    if depends[idx] == '(':
		# !(>2.0<3.0): remove matching items
		# Call ourselves recursively!
		idx, notlist = part_remove(name, depends, idx + 1,
							    matching[:], pname)
		if idx >= len(depends) or depends[idx] != ')':
		    recipe_error([], _("Missing ) in %s") % name)
		idx = idx + 1
		braces = 1
	elif depends[idx] == '>':
	    idx = idx + 1
	    if idx < len(depends) and depends[idx] == '=':
		idx = idx + 1
		next_op = ">="
	    else:
		next_op = ">"
	elif depends[idx] == '<':
	    idx = idx + 1
	    if idx < len(depends) and depends[idx] == '=':
		idx = idx + 1
		next_op = "<="
	    else:
		next_op = "<"

	if not braces:
	    # Isolate the one part: package name and optional version spec.
	    e = part_end(name, depends, idx)
	    pat = depends[idx:e]
	    idx = e

	    # For "!version" find the list items that are to be excluded.
	    if next_op == '!':
		# Find the part of "pname" until the version number.
		i = 0
		while i < len(pname) - 1:
		    if pname[i] == '-' and pname[i + 1] in string.digits + '*[':
			break
		    i = i + 1
		patlead = pname[:i + 1]
		if patlead[-1] != '-':
		    patlead = patlead + '-'

		notlist = depend_matches(matching, patlead + pat)

	# Go over all currently matching items, deleting the ones that
	# don't meet the condition.
	i = 0
	while i < len(matching):
	    if next_op == '!':
		match = not matching[i] in notlist
	    else:
		match = depend_item_match(matching[i], next_op, pat)
	    if match:
		i = i + 1
	    else:
		del matching[i]

    return idx, matching


def depend_item(recdict, name, depends, idx):
    """Handle an item in a dependency spec.
       Return the index of the following character and a list of missing
       items"""
    # We start with all possible packages and remove the ones not matching.

    # First part: use name as a pattern and use all matches.
    e = part_end(name, depends, idx)
    pname = depends[idx:e]
    matching = depend_matches(get_installed(recdict), pname)

    # Following parts: Remove matches according to each part.
    idx, matching = part_remove(name, depends, e, matching, pname)

    if matching:
	missing = []
    else:
	# TODO: use first version number mentioned.
	missing = [pname]

    return skip_white(depends, idx), missing


def depend_top(recdict, name, depends, idx):
    """Handle a list of items in depends[idx:] with an "and" or "or" relation.
       Stop at the end of "depends" or when encountering an unmatched ")".
       Returns the index of the end or ")" and a list of missing packages."""
    missing = []
    had_op = None
    while idx < len(depends) and depends[idx] != ')':
	if depends[idx] == '(':
	    # Call ourselves recursively to handle items in parenthesis.
	    idx, res = depend_top(recdict, name, depends,
						   skip_white(depends,idx + 1))
	    if idx >= len(depends) or depends[idx] != ')':
		recipe_error([], _('Missing ")" in %s') % name)
	    idx = idx + 1
	else:
	    idx, res = depend_item(recdict, name, depends, idx)
	
	# Combine with previous results:
	# AND means adding more missing items.
	# OR means clearing missing items when there are none found now.
	# Otherwise it's the first item, set "missing".
	if had_op == "and":
	    missing.extend(res)
	elif had_op == "or":
	    if not res:
		missing = []
	else:
	    missing = res
	
	# Check for a following item:
	# "|" means an OR operation
	# end of string means it's done
	# something else means AND operation
	idx = skip_white(depends, idx)
	if idx < len(depends) and depends[idx] != ')':
	    if depends[idx] == '|':
		new_op = "or"
		idx = skip_white(depends, idx + 1)
		if idx >= len(depends):
		    recipe_error([], _("Trailing '|' in %s") % name)
		if depends[idx] == ')':
		    recipe_error([], _("'|' before ')' in %s") % name)
		if depends[idx] == '|':
		    recipe_error([], _("double '|' in %s") % name)
	    else:
		new_op = "and"
	    if had_op and had_op != new_op:
		recipe_error([], _("Illegal combination of AND and OR in %s")
									% name)
	    had_op = new_op

    return idx, missing

def depend_do(recdict, name, check_only):
    """Handle the "name" dependencies.  When "check_only" is non-zero only
       check if the items can be fulfilled, don't actually install them."""
    if get_var_val(0, recdict, "AUTODEPEND") != "no":
	varname = "DEPEND_" + string.upper(name)
	depends = get_var_val(0, recdict, varname)
	if not depends and (name == "run" or name == "build"):
	    varname = "DEPENDS"
	    depends = get_var_val(0, recdict, varname)
	if not depends:
	    return

	# Figure out which desired modules are not yet installed.
	idx, missing = depend_top(recdict, varname,
					       depends, skip_white(depends, 0))
	if idx < len(depends):
	    recipe_error([], _('Unmatched ")" in %s') % varname)

	if not missing:
	    msg_extra(recdict, _("All %s dependencies satisfied") % name)
	elif check_only:
	    msg_extra(recdict, 'Would check %s dependencies %s now'
							% (name, str(missing)))
	else:
	    msg_extra(recdict, 'Would install %s dependencies %s now'
							% (name, str(missing)))


def port_dependcheck(recdict):
    """Check if required items are present or can be installed.
       If not, exit with an error."""
    for n in depend_list:
	if ((n != "testdepend" or get_var_val(0, recdict, "SKIPTEST") != "yes")
		and (n != "rundepend"
			  or get_var_val(0, recdict, "SKIPRUNTIME") != "yes")):
	    depend_do(recdict, n[:-6], 1)


def use_cvs(recdict):
    """Return non-zero when CVS is to be used to obtain files."""
    # When CVS was already used we need to use it again.
    if os.path.exists(cvs_done_file):
	return 1
    # When archives were unpacked we don't use CVS.
    if os.path.exists(cvs_notdone_file):
	return 0
    return (get_var_val(0, recdict, "CVSMODULES")
				    and get_var_val(0, recdict, "CVS") != "no")


def port_fetchdepend(recdict):
    """Obtain required items for building."""
    depend_do(recdict, "fetch", 0)


def port_fetch(recdict):
    """Obtain the files for the port."""
    work = getwork(recdict)

    if use_cvs(recdict):
	# Obtain stuff through CVS.
	# TODO: support a list of cvsroots
	cwd = os.getcwd()
	dir = get_var_val(0, recdict, "WRKDIR")
	if dir:
	    # Do this in the $WRKDIR directory.
	    assert_dir([], recdict, dir)
	    goto_dir(recdict, dir)
	try:
	    cvsroot = get_var_val(0, recdict, "CVSROOT")
	    modules = str2dictlist([], get_var_val(0, recdict, "CVSMODULES"))

	    # Get each module from CVS.
	    for f in modules:
		if f.has_key("cvsroot"):
		    root = f["cvsroot"]
		else:
		    root = cvsroot
		n = work.get_node(f["name"], 0, f)
		n.set_attributes({"fetch" : "cvs://" + root})
		l = handle_nodelist([], recdict, [n], 1, "fetch", ["fetch"])
		if l:
		    recipe_error([], _('CVS checkout of %s failed') % f["name"])
	finally:
	    goto_dir(recdict, cwd)

	did_use_cvs = cvs_done_file
	dfs = get_var_val(0, recdict, "CVSDISTFILES")
	pfs = get_var_val(0, recdict, "CVSPATCHFILES")
    else:
	did_use_cvs = cvs_notdone_file
	dfs = get_var_val(0, recdict, "DISTFILES")
	if not dfs:
	    msg_info(recdict, _("DISTFILES not set, no files fetched"))
	pfs = get_var_val(0, recdict, "PATCHFILES")

    # Check this first, to avoid getting an error after spending a lot of time
    # downloading the distfiles.
    if pfs and not get_var_val(0, recdict, "PATCH_SITES"):
	recipe_error([], _('Patch files defined but PATCH_SITES not defined'))

    # remember which files need to be extracted
    if not get_var_val(0, recdict, "EXTRACTFILES"):
	recdict["EXTRACTFILES"] = dfs

    # Obtain files: archives and patches
    for dl, sites, dir in [(str2dictlist([], dfs), "MASTER_SITES", "DISTDIR"),
			   (str2dictlist([], pfs), "PATCH_SITES", "PATCHDISTDIR")]:
	# For each item in $MASTER_SITES or $PATCH_SITES append "/%file%", so
	# that it can be used as a fetch attribute.
	msl = str2dictlist([], get_var_val(0, recdict, sites))
	for i in msl:
	    i["name"] = os.path.join(i["name"], "%file%")
	master_fetch = dictlist2str(msl, Expand(0, Expand.quote_aap))

	destdir = get_var_val(0, recdict, dir)

	for f in dl:
	    # Download each file into $DISTDIR or $PATCHDISTDIR.

	    # Let the "distdir" attribute overrule the default directory.
	    fn = os.path.basename(f["name"])
	    if f.has_key("distdir"):
		fname = os.path.join(f["distdir"], fn)
	    else:
		fname = os.path.join(destdir, fn)

	    # Skip fetching if the file already exists.
	    if os.path.exists(fname):
		msg_extra(recdict, _('file already exists: "%s"') % fname)
		continue

	    # Make sure the destination directory exists.
	    assert_dir([], recdict, os.path.dirname(fname))

	    # Make the fetch attribute include the full path of the file to
	    # download.
	    n = work.get_node(fname, 0, f)
	    if f.has_key("fetch"):
		rf = f["fetch"]
	    else:
		rf = master_fetch

	    # replace %file% with the file name
	    rf = repl_file_name(rf, f["name"])
	    n.set_attributes({"fetch" : rf})
	    l = handle_nodelist([], recdict, [n], 1, "fetch", ["fetch"])
	    if l:
		recipe_error([], _('Obtaining "%s" failed') % f["name"])

    # When checkout from CVS was successful, remember that CVS is to be
    # used until "aap distclean" is done.
    try:
	from Commands import touch_file
	aap_checkdir([], recdict, did_use_cvs)
	touch_file(did_use_cvs, 0644);
    except StandardError, e:
	recipe_error([], (_('Cannot create "%s": ') % did_use_cvs) + str(e))


def port_checksum(recdict):
    """Check the checksums of obtained files for the port."""
    msg_extra(recdict, 'No do-checksum target defined; checking checksums skipped')


def port_extractdepend(recdict):
    """Obtain required items for building."""
    depend_do(recdict, "extract", 0)


def port_extract(recdict):
    """Obtain the files for the port."""
    l = get_var_val(0, recdict, "EXTRACT_ONLY")
    if l:
	pass
    elif use_cvs(recdict):
	l = get_var_val(0, recdict, "CVSDISTFILES")
    else:
	l = get_var_val(0, recdict, "DISTFILES")
    if not l:
	return
    archlist = str2dictlist([], l)

    # Change the names to be in $DISTDIR and make them absolute (changing
    # directory below).
    distdir = os.path.abspath(get_var_val(0, recdict, "DISTDIR"))
    for x in archlist:
	fn = os.path.basename(x["name"])
	if x.has_key("distdir"):
	    x["name"] = os.path.abspath(os.path.join(x["distdir"], fn))
	else:
	    x["name"] = os.path.join(distdir, fn)

    # extract each file
    wrkdir = os.path.abspath(get_var_val(0, recdict, "WRKDIR"))
    cwd = os.getcwd()
    try:
	for f in archlist:
	    # change to "extractdir"
	    # Make path absolute now, before changing directories.
	    if f.has_key("extractdir"):
		dir = os.path.join(wrkdir, f["extractdir"])
	    else:
		dir = wrkdir
	    assert_dir([], recdict, dir)
	    goto_dir(recdict, dir)

	    # Invoke the extract action.
	    msg = action_run(recdict, [{"name" : "extract"}, f])
	    if msg:
		recipe_error([], msg)
    finally:
	# return from $WRKDIR
	goto_dir(recdict, cwd)


def port_patch(recdict):
    """Apply the patch files."""
    if use_cvs(recdict):
	l = get_var_val(0, recdict, "CVSPATCHFILES")
    else:
	l = get_var_val(0, recdict, "PATCHFILES")
    if not l:
	return
    patchlist = str2dictlist([], l)

    # Change the names to be in $PATCHDISTDIR and make them absolute (changing
    # directory below).
    patchdistdir = os.path.abspath(get_var_val(0, recdict, "PATCHDISTDIR"))
    for x in patchlist:
	fn = os.path.basename(x["name"])
	if x.has_key("distdir"):
	    x["name"] = os.path.abspath(os.path.join(x["distdir"], fn))
	else:
	    x["name"] = os.path.join(patchdistdir, fn)


    # Handle each patch file separately, it may have attributes for a
    # different patch command or directory.
    wrkdir = os.path.abspath(get_var_val(0, recdict, "WRKDIR"))
    cwd = os.getcwd()
    try:
	for fd in patchlist:
	    # Decide what command to use for patching.
	    if fd.get("patchcmd"):
		cmd = fd["patchcmd"]
	    else:
		cmd = get_var_val(0, recdict, "PATCHCMD")
		if not cmd:
		    cmd = "patch -p -f -s < "

	    # TODO: shell quoting
	    i = string.find(cmd, "%s")
	    if i >= 0:
		cmd = cmd[:i] + fd["name"] + cmd[i + 2:]
	    else:
		cmd = cmd + fd["name"]

	    # Go to the directory for patching.
	    if fd.get("patchdir"):
		dir = fd["patchdir"]
	    else:
		dir = get_var_val(0, recdict, "PATCHDIR")
		if not dir:
		    dir = get_var_val(0, recdict, "WRKSRC")
	    dir = os.path.join(wrkdir, dir)
	    goto_dir(recdict, dir)

	    # Execute the patch command.
	    n = logged_system(recdict, cmd)
	    if n:
		recipe_error([],
			  _("Shell returned %d when patching:\n%s") % (n, cmd))
    finally:
	goto_dir(recdict, cwd)


def port_builddepend(recdict):
    """Obtain required items for building."""
    depend_do(recdict, "build", 0)


def port_configure(recdict):
    """Configure the port."""
    cmd = get_var_val(0, recdict, "CONFIGURECMD")
    if cmd:
	port_exe_cmd(recdict, cmd, "BUILDDIR")
    else:
	msg_extra(recdict, 'No CONFIGURECMD specified')


def port_build(recdict):
    """Build the port."""
    # Decide what command to use for building.
    cmd = get_var_val(0, recdict, "BUILDCMD")
    if not cmd:
	cmd = "aap"

    port_exe_cmd(recdict, cmd, "BUILDDIR")


def port_exe_cmd(recdict, cmd, dirname):
    """Execute "cmd" in directory possibly specified with "dirname"."""
    # Go to the build/test directory.
    cwd = os.getcwd()
    dir = get_var_val(0, recdict, dirname)
    if not dir:
	dir = get_var_val(0, recdict, "WRKSRC")
    dir = os.path.join(get_var_val(0, recdict, "WRKDIR"), dir)
    try:
	goto_dir(recdict, dir)
    except StandardError, e:
	recipe_error([], (_('Cannot change to directory "%s": ') % dir)
								      + str(e))

    try:
	# Execute the build command.
	# When it's the default, avoid starting another instance of ourselves.
	# It would redirect (and echo) the output twice.
	if ((len(cmd) == 3 and cmd == "aap")
				     or (len(cmd) >= 4 and cmd[:4] == "aap ")):
	    from Commands import aap_execute
	    from Work import setrpstack, getrpstack

	    # Don't copy our recdict, it would cause things like "PORTNAME" to
	    # be defined, which isn't appropriate for the build recipe.
	    rd = {}
	    setrpstack(rd, getrpstack(recdict))
	    rd["_work"] = recdict["_work"]
	    aap_execute(0, rd, "main.aap" + cmd[3:])
	else:
	    n = logged_system(recdict, cmd)
	    if n:
		recipe_error([],
			  _("Shell returned %d when executing:\n%s") % (n, cmd))
    finally:
	goto_dir(recdict, cwd)


def port_testdepend(recdict):
    """Obtain required items for testing the port."""
    
    if get_var_val(0, recdict, "SKIPTEST") != "yes":
	depend_do(recdict, "test", 0)


def port_test(recdict):
    """Do the tests."""
    # Skip this when not testing
    if get_var_val(0, recdict, "SKIPTEST") != "yes":
	# Decide what command to use for testing.
	cmd = get_var_val(0, recdict, "TESTCMD")
	if not cmd:
	    cmd = "aap test"

	# Execute the command in the test directory.
	port_exe_cmd(recdict, cmd, "TESTDIR")


def _write_var2file(recdict, varname, fname):
    # Skip when not actually building.
    if skip_commands():
	msg_info(recdict, _('skip writing to "%s"') % fname)
	return

    try:
	f = open(fname, "w")
    except IOError, e:
	recipe_error([], (_('Cannot open "%s" for writing') % fname) + str(e))
    try:
	s = recdict[varname]
	f.write(s)
	if s[-1] != '\n':
	    f.write('\n')
	f.close()
    except IOError, e:
	recipe_error([], (_('Cannot write to "%s"') % fname) + str(e))
	

def get_pkgname(recdict):
    n = (get_var_val(0, recdict, "PORTNAME") + "-"
				     + get_var_val(0, recdict, "PORTVERSION"))
    rv = get_var_val(0, recdict, "PORTREVISION")
    if rv:
	n = n + '_' + rv
    return n


def port_package(recdict):
    """Turn the port into a package."""
    # This only works for "pkg_create" for now.
    if not program_path("pkg_create"):
	recipe_error([], _('"pkg_create" command not found'))

    # Copy or install the files from "work" to the "pack" directory.
    pkgdir = os.path.abspath(get_var_val(0, recdict, "PKGDIR"))
    # Make sure the pack directory exists and is empty.
    if os.path.exists(pkgdir):
	try:
	    deltree(pkgdir)
	except StandardError, e:
	    recipe_error([], (_('Cannot make "%s" directory empty: ') % pkgdir)
								      + str(e))
    assert_dir([], recdict, pkgdir)

    # TODO
    if 0 and get_var_val(0, recdict, "PACKFILES"):
	# Copy files mentioned in "PACKFILES" to $PKGDIR.
	prefix = ''
    else:
	# Execute the command in the test directory.
	cmd = get_var_val(0, recdict, "INSTALLCMD")
	if not cmd:
	    # TODO: quoting
	    cmd = "aap install DESTDIR=%s" % pkgdir
	port_exe_cmd(recdict, cmd, "INSTALLDIR")
	prefix = get_var_val(0, recdict, "PREFIX")
	if not prefix:
	    prefix = ""

    # Remove a leading slash, join() would do the wrong thing.
    if prefix and prefix[0] == '/':
	prefix = prefix[1:]

    # Write packing list in pkg-plist
    filesdir = os.path.join(pkgdir, prefix)
    try:
	filelist = dir_contents(filesdir, 1, 0)
    except StandardError, e:
	recipe_error([], (_('Could not list files in "%s": ') % filesdir)
								      + str(e))

    if skip_commands():
	msg_info(recdict, _('skip writing to pkg-plist'))
    else:
	try:
	    f = open("pkg-plist", "w")
	except StandardError, e:
	    recipe_error([], _('Could not open pkg-plist for writing: ')
								      + str(e))
	try:
	    f.write("@cwd /usr/local\n")
	    f.write("@srcdir %s\n" % filesdir)
	    f.writelines(map(lambda x: x + '\n', filelist))
	    f.close()
	except StandardError, e:
	    recipe_error([], _('Could not write to pkg-plist') + str(e))

    # Write description in pkg-descr
    _write_var2file(recdict, "PORTDESCR", "pkg-descr")

    # Write comment in pkg-comment
    _write_var2file(recdict, "PORTCOMMENT", "pkg-comment")

    # Create the package
    pkgname = get_pkgname(recdict)
    cmd = ("pkg_create -f pkg-plist -c pkg-comment -d pkg-descr %s" % pkgname)
    n = logged_system(recdict, cmd)
    if n:
	recipe_error([], _("Shell returned %d when packaging:\n%s") % (n, cmd))


sus_out = None
sus_in = None

def port_install(recdict):
    """Install the package."""
    # Open a shell to install the package.
    # TODO: skip this if user is root or installing in user home dir.
    open_sushell(recdict)
    pkgname = get_pkgname(recdict) + ".tgz"
    sus_in.write('I' + pkgname + '\n')
    sus_in.flush()
    while 1:
	m = sus_out.readline()
	if not m:
	    recipe_error([], 'Installing %s aborted' % pkgname)
	if m[:10] == "PkgInstall":
	    msg_extra(recdict, m)
	    if m[11:13] != "OK":
		recipe_error([], 'Installing %s failed' % pkgname)
	    break
	msg_info(recdict, m)


def open_sushell(recdict):
    """Open a connection to a su-shell to install packages under root
    permissions."""
    global sus_out, sus_in
    import popen2

    if sus_out is None and sus_in is None:
	# Run the shell and get the input and output files.
	msg_info(recdict, _("Starting a separate shell to run pkg_add"))
	msg_info(recdict, _("You will be asked to enter the root password"))
	sus_out, sus_in = popen2.popen2('su root -c %s'
			   % os.path.join(Global.aap_rootdir, "PkgInstall.py"))
	# Loop until logging in has been done.
	try:
	    while 1:
		m = sus_out.readline()
		if not m:
		    recipe_error([], _('Starting super-user shell failed'))
		if m[:16] == "PkgInstall ready":
		    break
		msg_info(recdict, m)
	except StandardError, e:
	    recipe_error([], _('Error while installing package: ') + str(e))

    if sus_out is None or sus_in is None:
	recipe_error([], _('Failed to start a shell to install packages'))

def close_sushell(recdict):
    """Close the su-shell."""
    global sus_out, sus_in
    if sus_in:
	try:
	    sus_in.write('Q\n')
	    sus_in.flush()
	    sus_out = None
	    sus_in = None
	except:
	    msg_info(recdict, 'could not close shell for installing packages')
	else:
	    msg_extra(recdict, 'closed shell for installing packages')


def port_rundepend(recdict):
    """Obtain required items for running the port."""
    # TODO: add the code to do the work.
    if get_var_val(0, recdict, "SKIPRUNTIME") != "yes":
	depend_do(recdict, "run", 0)


def port_installtest(recdict):
    """Post-install test: defaults to nothing."""
    msg_extra(recdict, _("Default installtest: do nothing"))


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