# Part of the A-A-P recipe executive: Add default rules and dependencies

# 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

import string
import os.path

from Action import find_primary_action
from Dictlist import varname2dictlist, dictlist2str
from RecPos import RecPos, rpcopy
from Util import *
from Depend import Depend
from Message import *
from Process import Process, recipe_error
from ParsePos import ParsePos
from Scope import get_build_recdict
from DoBuild import find_autodep_items
from RecPython import srcitem2obj, topdir


def doadddef(work, recdict, toplevel):
    """
    Add default dependencies, depending on what was defined in the
    recipe(s) read.
    """
    # Add a dependency from $SOURCE and $TARGET if appropriate.
    add_source_target(work, recdict)

    # Add a comment to the useful targets we add here.
    caddlist = []
    for n, c in [("install", _("install files to directory $PREFIX")),
                 ("uninstall", _("delete installed files from directory $PREFIX")),
                 ("clean", _("delete all generated files that are not distributed")),
                 ("cleanmore", _("delete all generated files")),
                 ("cleanALL", _("delete all generated files, AAPDIR and build-* directories"))]:
        if not work.find_node(n):
            caddlist.append((n, c))

    # Add "clean" and "cleanmore" target if appropriate.
    add_clean(work, recdict)

    # At the toplevel we add more things.
    if toplevel:

        # Add the stuff for ports if $PORTNAME is defined.
        if recdict.get("PORTNAME"):
            from Port import add_port_defaults
            add_port_defaults(work)

        # Add "cleanALL" when not defined.
        add_optional_target(work, recdict, "cleanALL", [ "clean" ],
                ":tree . {dirname = AAPDIR}\n"
                " :del {f}{r} $name\n"
                ":tree . {dirname = build-.*}\n"
                " :del {f}{r} $name\n")

        # Add "install" and friends when not defined.
        # Also does the # "uninstall" variant.
        add_install_target(work, recdict, "install",
                [ "install-platform",
                  "install-shared" ],
                "@if has_target('install-local'):\n"
                " :update install-local\n")
        add_install_target(work, recdict, "install-platform",
                [ "install-exec",
                  "install-sbin",
                  "install-lib",
                  "install-dll",
                  "install-ltlib",
                  "install-conf" ],
                "@if has_target('install-platform-local'):\n"
                " :update install-platform-local\n")
        add_install_target(work, recdict, "install-shared",
                ["install-data",
                "install-man",
                "install-info",
                "install-include"],
                "@if has_target('install-shared-local'):\n"
                " :update install-shared-local\n")

        # These targets are all alike.
        for t in ["exec", "sbin", "lib", "dll", "ltlib", "conf",
                                             "data", "man", "info", "include"]:
            tup = string.upper(t)
            add_install_target(work, recdict, "install-%s" % t, [],
                    "@if _top.get('INSTALL_%s'):\n"
                    " :update $_top.INSTALL_%s\n"
                    " :do install%s $_top.INSTALL_%s\n" % (tup, tup, t, tup))

        # Add the comments after adding the dependencies.
        for n, c in caddlist:
            work.get_node(n, 1).set_attributes({"comment" : c})


def add_source_target(work, recdict):
    """
    Add build dependencies for $SOURCE and $TARGET for backwards compatibility.
    """

    # If $SOURCE and $TARGET are defined at the toplevel in "recdict", $TARGET
    # is one item and there is no dependency with $TARGET as target that
    # includes build commands, create one from $SOURCE and $TARGET.  Can be
    # used for the main and a child recipe.
    if recdict.get("TARGET") and recdict.get("SOURCE"):
        targets = varname2dictlist(recdict, None, "TARGET")
        if len(targets) == 1:
            msg_warning(recdict, _("Warning: support for $SOURCE and $TARGET will be removed soon"))
            target = work.find_node(targets[0]["name"])
            if not target or not target.get_first_build_dependency():
                sources = varname2dictlist(recdict, None, "SOURCE")
                add_buildrule([RecPos("Default target")], work, recdict,
                                           "program", {}, targets, {}, sources)


def add_clean(work, recdict):
    """
    Add "clean" build dependency for the current recipe if there isn't one.
    Do the same for "cleanmore", but it also depends on "clean".
    """
    for (trg, TRG, srclist) in [("clean", "CLEAN", []),
                              ("cleanmore", "CLEANMORE", [{"name": "clean"}])]:
        target = work.find_node(trg)
        if not target or not target.get_recipe_build_dependency(recdict):
            cmd = (   ("@if _recipe.get('%sFILES'):\n" % TRG)
                    + (" :del {f} $_recipe.%sFILES\n" % TRG)
                    + ("@if _recipe.get('%sDIRS'):\n" % TRG)
                    + (" :del {f}{r} $_recipe.%sDIRS\n" % TRG))
            msg_depend(recdict, _('Adding default dependency for "%s"') % trg)
            rpstack = [RecPos("Default target")]
            dep = Depend([{"name" : trg}], {}, srclist,
                                         work, rpstack, cmd, recdict = recdict)
            work.add_dependency(rpstack, dep)

    # Add the generated recipes for automatic dependencies to $CLEANFILES.
    # Go through all the dependencies and handle the ones defined in this
    # recipe.
    # TODO: this doesn't find automatic dependencies from rules and assumes
    # dependencies are done recursively.
    for dep in work.dependencies:
        if (dep.buildrecdict
                        and dep.buildrecdict["_recipe"] is recdict["_recipe"]):
            for src in dep.sourcelist:
                node = work.find_node(src["name"])
                if node and not node.did_add_clean:
                    node.did_add_clean = 1
                    ftype = node.get_ftype(recdict)
                    if ftype and ftype != "ignore":
                        action, recipe = find_autodep_items(work, recdict,
                                                               ftype, node, {})
                        if action:
                            add_cleanfiles(recdict, recipe.get_name())

    # Remove duplicates from CLEANFILES.
    if recdict["_recipe"].get("CLEANFILES"):
        recdict["_recipe"]["CLEANFILES"] = rem_dup(
                                              recdict["_recipe"]["CLEANFILES"])


def add_install_target(work, recdict, targetname, sourcenames, cmd):
    """
    Call add_optional_target() twice, the second time with "install" changed to
    "uninstall".
    """
    add_optional_target(work, recdict, targetname, sourcenames, cmd)

    untargetname = re.sub("install", "uninstall", targetname)
    unsourcenames = map(lambda x: re.sub("install", "uninstall", x),
                                                                   sourcenames)
    # In the uninstall command change all "install" to "uninstall".
    # When using INSTALL.* remove the ":update" line and then double the
    # command to also use the UNINSTALL variable.
    uncmd = re.sub("install", "uninstall", cmd)
    if string.find(uncmd, "INSTALL") >= 0:
        uncmd = re.sub(":update.*\n", "", uncmd)
        uncmd = uncmd + re.sub("INSTALL", "UNINSTALL", uncmd)
    add_optional_target(work, recdict, untargetname, unsourcenames, uncmd)


def add_optional_target(work, recdict, targetname, sourcenames, cmd):
    """
    Add build dependency for "targetname" if there isn't one.
    To be used at the toplevel.
    """
    target = work.find_node(targetname)
    if target and target.get_dependencies():
        msg_depend(recdict, _('Not adding default dependency for "%s"')
                                                                  % targetname)
    else:
        rpstack = [RecPos("Default target")]
        if sourcenames:
            sourcelist = map(lambda x: {"name": x}, sourcenames)
        else:
            sourcelist = []
        dep = Depend([{"name" : targetname}], {}, sourcelist, work, rpstack,
                                                        cmd, recdict = recdict)
        work.add_dependency(rpstack, dep)


def add_buildrule(rpstack, work, recdict,
                           type, cmd_attr, targetlist, build_attr, sourcelist):
    """
    Add a buildrule.  Also used for ":totype".
    "type" is "program", "lib", "ltlib", "dll", "totype" or the first argument
    of ":produce".
    "cmd_attr" are extra attributes for the command itself.
    "targetlist" is a dictlist with the target(s).
    "build_attr" are extra attributes for the build commands.
    "sourcelist" is a dictlist with the sources.
    """
    # TODO: implement "cmd_attr".

    if type != "totype":
        target = work.find_node(targetlist[0]["name"])
        if target and target.get_first_build_dependency():
            msg_error(recdict, _("Both a build rule and a dependency with build commands defined for '%s'") % targetlist[0]["name"])
            return

    # Create a node for all items in sourcelist and targetlist, makes
    # sure the attributes are carried over.
    work.add_dictlist_nodes(sourcelist)
    if type != "totype":
        work.add_dictlist_nodes(targetlist)

    # When there is no direct build action for the source or there
    # are multiple sources, compile each source into an object and
    # build all the objects into the target.
    installvar = cmd_attr.get("installvar")
    onestep = cmd_attr.get("onestep")
    cleanfiles = []
    if type == "dll":
        if installvar is None:
            installvar = "INSTALL_DLL"
    elif type == "lib":
        if installvar is None:
            installvar = "INSTALL_LIB"
    elif type == "ltlib":
        if installvar is None:
            installvar = "INSTALL_LTLIB"
    elif type == "program":
        if installvar is None:
            installvar = "INSTALL_EXEC"

    if onestep and type == "ltlib":
        msg_warning(recdict, _("The 'onestep' attribute is not supported for the :ltlib target."), rpstack = rpstack)
        onestep = None

    if onestep:
        add_onestep_build(rpstack, work, recdict, type, cmd_attr, targetlist,
                            build_attr, sourcelist)
    else:
        a = cmd_attr.get("objecttype")
        if a:
            objtypes = string.split(a, ',')
        else:
            objtypes = None
        buildaction = cmd_attr.get("buildaction")
        if type == "dll":
            if not buildaction:
                buildaction = "builddll"
        elif type == "lib":
            if not buildaction:
                buildaction = "buildlib"
        elif type == "ltlib":
            if not buildaction:
                buildaction = "buildltlib"
        elif type == "program":
            if not buildaction:
                buildaction = "build"
        elif type == "totype":
            objtypes = [ targetlist[0]["name"] ]
        else:
            if not buildaction:
                recipe_error(rpstack, _("Missing buildaction attribute"))
            if installvar is None:
                installvar = "INSTALL_EXEC"

        if not objtypes:
            # Obtain the supported object types from the buildaction.
            act = find_primary_action(buildaction)
            if not act:
                if type not in ["dll", "lib", "ltlib", "program"]:
                    recipe_error(rpstack, _("Missing objecttype attribute"))
                else:
                    recipe_error(rpstack, _("No primary %s action defined")
                                                                 % buildaction)
            objtypes = act.get_in_types()

        objectsuffix = cmd_attr.get("objectsuffix")
        if type == "totype" and targetlist[0].get("suffix"):
            objectsuffix = targetlist[0].get("suffix")
        objectprefix = cmd_attr.get("objectprefix")
        if type == "totype" and targetlist[0].get("prefix"):
            objectprefix = targetlist[0].get("prefix")

        # Go over all source files and figure out a way to turn each one
        # into an object file.
        build_obj = []
        for si in sourcelist:
            node = work.get_node(si["name"], 1, si)
            in_ftype = node.get_ftype(recdict)

            if in_ftype in objtypes:
                # The source is supported by the build action: add it directly.
                build_obj.append(si)
            else:
                # Loop over the types that the build action supports, turn
                # the source in the first one that is possible.
                names = ''

                for objtype in objtypes:
                    if objtype == "default":
                        continue
                    objname = add_build_one(rpstack, work, recdict, node, si, in_ftype, objtype, objectprefix, objectsuffix, build_attr, cleanfiles)
                    if objname:
                        break
                    if names:
                        names = names + '/'
                    names = names + objtype

                if not objname:
                    recipe_error(rpstack, _('Do not know how to make an %s out of "%s"') % (names, si["name"]))

                build_obj.append({"name" : objname})


    if type != "totype":
        targets_str = dictlist2str(targetlist)
        if not onestep:
            # Add a dependency to build the program or library in the form:
            #   targetlist : {buildcheck = xxx} sourcelist
            #       do buildaction {target = $target} $source
            build_obj_str = dictlist2str(build_obj)
            cmd = ("  :do %s {target = $+target} $source" % buildaction)

            msg_depend(recdict, _('Adding dependency:\n\t%s : %s\n\t%s')
                                           % (targets_str, build_obj_str, cmd))
            work.add_dependency(rpstack,
                       Depend(targetlist, build_attr, build_obj, work,
                                       rpstack, cmd + '\n', recdict = recdict))

        cleanfiles.extend(targetlist)

        # Add target to INSTALL_EXEC, INSTALL_LIB or INSTALL_DLL.
        # do that relative to the top directory.
        if installvar:
            append2var(recdict, "_top", installvar, topdir(targets_str))

    # Add files to be cleaned to _recipe.CLEANFILES.
    add_cleanfiles(recdict, dictlist2str(cleanfiles, Expand(0)))

    # Add files to be distributed to _recipe.DISTFILES.  Skip the ones with a
    # {nodist} attribute.
    l = filter(lambda x: not x.get("nodist"), sourcelist)
    add_distfiles(recdict, dictlist2str(l, Expand(0)))


def add_build_one(rpstack, work, recdict, node, si, in_ftype,
                  objtype, objectprefix, objectsuffix, build_attr, cleanfiles):
    """
    Add build rule to turn one source node into a file of type "objtype".
    If successful return name of target file, None otherwise.
    """
    sufname = None
    if not objectsuffix:
        if objtype == "dllobject":
            sufname = "DLLOBJSUF"
        elif objtype == "libobject":
            sufname = "LIBOBJSUF"
        elif objtype == "ltobject":
            sufname = "LTOBJSUF"
        elif objtype == "object":
            sufname = "OBJSUF"

    # Make the object name by prepending $BDIR and changing the
    # extension to $OBJSUF/$LIBOBJSUF/$DLLOBJSUF/nothing.
    n = srcitem2obj(recdict, si["name"], attrdict = si, sufname = sufname)
    if objectprefix:
        n = os.path.join(os.path.dirname(n), objectprefix + os.path.basename(n))
    if objectsuffix:
        n = n + objectsuffix

    # If there is no build rule for the object file, add one.
    target = work.find_node(n)
    if target and target.get_first_build_dependency():
        # There already is a dependency for this target.
        if len(si) > 1:
            msg_warning(recdict, _('%s: Ignoring attributes for "%s", a build rule already exists') % (str(rpstack[-1]), si["name"]))
    else:

        # Find the route to the object file.
        route = work.find_route(in_ftype, objtype, use_actions = 1)
        if not route:
            return None

        # Add attributes from ":route" to the target node.
        work.get_node(n, add = 1, dict = route.targetattr)

        # Inits to avoid a warning from pychecker.
        trg = None
        out_ftype = None

        # Loop over all steps in the route.
        for i in range(len(route.steplist)):
            # Get the action and filename by processing the line.
            # TODO: catch errors
            route.rpstack[-1].line_nr = route.lnumlist[i] - 1
            fp = ParsePos(route.rpstack, string = "_x = "
                                                    + route.steplist[i] + '\n')
            rd = get_build_recdict(recdict, route.recdict,
                                         route.rpstack, keep_current_scope = 1,
                                              xscope = build_attr.get("scope"))
            rd["source"] = si["name"]
            Process(fp, rd, 0)
            l = string.split(rd["_x"], None, 1)
            action = l[0]

            if i == 0:
                src_dict = si.copy()
                src = si["name"]
                dnode = node
            else:
                src_dict = {"name" : trg}
                src = trg
                in_ftype = out_ftype
                dnode = work.find_node(src)

            # Set "depdir" for the auto-depend recipe.  Required if the source
            # file is built twice with different $BDIR.  Also add the recipe to
            # the files to be cleaned now, because $BDIR may change afterwards.
            if not src_dict.get("depdir"):
                src_dict["depdir"] = get_var_val_int(recdict, "BDIR")
            depaction, recipe = find_autodep_items(work, recdict,
                                                     in_ftype, dnode, src_dict)
            if depaction:
                add_cleanfiles(recdict, recipe.get_name())

            if i == len(route.steplist) - 1:
                trg = n
                out_ftype = objtype
            else:
                trg = l[1]
                out_ftype = route.typelist[i + 1][0]
                if (not os.path.isabs(trg) and string.find(trg, "build-") != 0):
                    trg = os.path.join(get_var_val_int(recdict, "BDIR"), trg)

            # Add a dependency to execute the action.
            cmd = "  :do %s {target = %s} %s" % (action, trg, src)
            msg_depend(recdict, _('Adding dependency:\n\t%s : %s\n\t%s')
                                        % (trg, dictlist2str([src_dict]), cmd))
            src_dict["filetype"] = in_ftype
            trg_dict = {"name": trg, "filetype" : out_ftype}
            work.add_dependency(rpstack,
                   Depend([ trg_dict ], build_attr, [ src_dict ], work,
                                      rpcopy(route.rpstack, route.lnumlist[i]),
                                                cmd + '\n', recdict = recdict))
            cleanfiles.append({"name": trg})

    return n


def add_onestep_build(rpstack, work, recdict,
                           type, cmd_attr, targetlist, build_attr, sourcelist):
    """Adds an onestep build"""

    buildaction = cmd_attr.get("buildaction")
    if type == "dll":
        if not buildaction:
            buildaction = "builddllonestep"
    elif type == "lib":
        if not buildaction:
            buildaction = "buildlibonestep"
    elif type == "program":
        if not buildaction:
            buildaction = "buildonestep"
    cmd = ("   :do %s {target =$+target} $source" % buildaction)
    work.add_dependency(rpstack,
            Depend(targetlist, build_attr, sourcelist, work,
                rpstack, cmd + '\n', recdict = recdict))

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