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

# 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 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 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("config", "config", 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 overrule 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_int(work.recdict, name):
            if work.recdict["_no"].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.
        adir = get_var_val_int(rd, "CVSWRKSRC")
        if adir:
            msg_extra(rd, _("Using $CVSWRKSRC for $WRKSRC: %s") % adir)
            rd["WRKSRC"] = adir
        else:
            modules = str2dictlist([], get_var_val_int(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_int(rd, "WRKSRC"):
            s = (get_var_val_int(rd, "PORTNAME") + "-"
                                    + get_var_val_int(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."""
    alist = ["done",
                get_var_val_int(recdict, "WRKDIR"),
                get_var_val_int(recdict, "PKGDIR"),
                "pkg-plist", "pkg-comment", "pkg-descr"]
    if dist:
        alist.extend([get_var_val_int(recdict, "DISTDIR"),
                get_var_val_int(recdict, "PATCHDISTDIR"),
                get_pkgname(recdict) + ".tgz",
                Global.aap_dirname])

    for adir in alist:
        if os.path.exists(adir):
            try:
                deltree(adir)
            except StandardError, e:
                recipe_error([], (_('Cannot delete "%s": ') % adir) + 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, adir 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_int(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_int(recdict, adir), f)
            if not os.path.exists(n):
                recipe_error([], _('File does not exists: "%s"') % n)
            md5sum = check_md5(recdict, n)
            if md5sum == "unknown":
                recipe_error([], _('Cannot compute checksum for "%s"') % n)
            lines.append("\t:checksum $%s/%s {md5 = %s}\n" % (adir, f, md5sum))

    #
    # 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_delete(temp)
            recipe_error([], (_('Cannot delete backup recipe "%s": ') % bak)
                                                                      + str(e))
    try:
        os.rename(work.top_recipe, bak)
    except StandardError, e:
        try_delete(temp)
        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))
    # If the rename worked the remove fails, that's normal.
    try_delete(temp)


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_int(recdict, "AUTODEPEND") != "no":
        varname = "DEPEND_" + string.upper(name)
        depends = get_var_val_int(recdict, varname)
        if not depends and (name == "run" or name == "build"):
            varname = "DEPENDS"
            depends = get_var_val_int(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_int(recdict, "SKIPTEST") != "yes")
                and (n != "rundepend"
                   or get_var_val_int(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_int(recdict, "CVSMODULES")
                             and get_var_val_int(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()
        adir = get_var_val_int(recdict, "WRKDIR")
        if adir:
            # Do this in the $WRKDIR directory.
            assert_dir([], recdict, adir)
            goto_dir(recdict, adir)
        try:
            cvsroot = get_var_val_int(recdict, "CVSROOT")
            modules = str2dictlist([],
                                  get_var_val_int(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_int(recdict, "CVSDISTFILES")
        pfs = get_var_val_int(recdict, "CVSPATCHFILES")
    else:
        did_use_cvs = cvs_notdone_file
        dfs = get_var_val_int(recdict, "DISTFILES")
        if not dfs:
            msg_info(recdict, _("DISTFILES not set, no files fetched"))
        pfs = get_var_val_int(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_int(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_int(recdict, "EXTRACTFILES"):
        recdict["EXTRACTFILES"] = dfs

    # Obtain files: archives and patches
    for dl, sites, adir 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_int(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_int(recdict, adir)

        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_int(recdict, "EXTRACT_ONLY")
    if l:
        pass
    elif use_cvs(recdict):
        l = get_var_val_int(recdict, "CVSDISTFILES")
    else:
        l = get_var_val_int(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_int(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_int(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"):
                adir = os.path.join(wrkdir, f["extractdir"])
            else:
                adir = wrkdir
            assert_dir([], recdict, adir)
            goto_dir(recdict, adir)

            # 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_int(recdict, "CVSPATCHFILES")
    else:
        l = get_var_val_int(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_int(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_int(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_int(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"):
                adir = fd["patchdir"]
            else:
                adir = get_var_val_int(recdict, "PATCHDIR")
                if not adir:
                    adir = get_var_val_int(recdict, "WRKSRC")
            adir = os.path.join(wrkdir, adir)
            goto_dir(recdict, adir)

            # 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_config(recdict):
    """Configure the port."""
    cmd = get_var_val_int(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_int(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()
    adir = get_var_val_int(recdict, dirname)
    if not adir:
        adir = get_var_val_int(recdict, "WRKSRC")
    adir = os.path.join(get_var_val_int(recdict, "WRKDIR"), adir)
    try:
        goto_dir(recdict, adir)
    except StandardError, e:
        recipe_error([], (_('Cannot change to directory "%s": ') % adir)
                                                                      + 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_int(recdict, "SKIPTEST") != "yes":
        depend_do(recdict, "test", 0)


def port_test(recdict):
    """Do the tests."""
    # Skip this when not testing
    if get_var_val_int(recdict, "SKIPTEST") != "yes":
        # Decide what command to use for testing.
        cmd = get_var_val_int(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 = get_var_val_int(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_int(recdict, "PORTNAME") + "-"
                                     + get_var_val_int(recdict, "PORTVERSION"))
    rv = get_var_val_int(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.
    from RecPython import program_path

    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_int(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_int(recdict, "PACKFILES"):
        # Copy files mentioned in "PACKFILES" to $PKGDIR.
        prefix = ''
    else:
        # Execute the command in the test directory.
        cmd = get_var_val_int(recdict, "INSTALLCMD")
        if not cmd:
            # TODO: quoting
            cmd = "aap install DESTDIR=%s" % pkgdir
        port_exe_cmd(recdict, cmd, "INSTALLDIR")
        prefix = get_var_val_int(recdict, "PREFIX")
        if not prefix:
            prefix = "/usr/local"

    # 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
sus_err = 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, sus_err
    import popenerr
    import select

    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, _("Please enter the root password:"))
        sus_out, sus_in, sus_err = popenerr.popen3('su root -c %s'
                           % os.path.join(Global.aap_rootdir, "PkgInstall.py"))
        # Loop until logging in has been done.
        if sus_err:
            fdlist = [sus_out, sus_err]
        else:
            fdlist = [sus_out]
        try:
            while 1:
                # Read both from stdout and stderr until PkgInstall has been
                # started.  "su" may write to stderr.
                inp, outp, exc = select.select([], fdlist, [])
                m = outp[0].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, sus_err
    if sus_in:
        try:
            sus_in.write('Q\n')
            sus_in.flush()
            sus_out.close()
            sus_out = None
            sus_in.close()
            sus_in = None
            try:
                sus_err.close()     # see popenerr.py why this is necessary
            except:
                pass
            sus_err = 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_int(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 et sts=4 tw=79 fo+=l:
