# Part of the A-A-P recipe executive: remember the work specified in the 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


# Currently a Work object contains these items:
#  recdict      -  Dictionary of global variables from the toplevel recipe.
#                  The "_work" variable points back to the Work object.
#  dependencies -  list of dependencies
#  rules        -  list of rules
#  nodes        -  dictionary of nodes from the recipes; index is node.name;
#                  (gone through fname_fold()). normally used to lookup virtual
#                  nodes 
#  absnodes     -  same contents as nodes, but key is the absolute name:
#                  node.absname (gone through fname_fold()).
#  aliasnodes   -  same as absnodes, but uses the alias name of nodes.
#  top_recipe   -  name of toplevel recipe used (None when not reading a recipe)

import os
import os.path
import string

from Node import Node
from RecPos import rpcopy
import Global
from Util import *
from Message import *
from Remote import is_url
from AapVersion import *
import Scope

def set_defaults(rd):
    """
    Set the default values for variables in dictionary "rd".
    This also adds dependencies and rules to the Work object associated with
    "rd".
    """
    from Dictlist import listitem2str, dictlist2str
    from DoRead import read_recipe, read_recipe_dir

    # $VERSIONSTR
    rd["VERSIONSTR"] = version_string

    # $HOME
    home = home_dir()
    if home:
        rd["HOME"] = listitem2str(os.path.basename(home))
    else:
        rd["HOME"] = ''
    
    # $CACHEPATH
    cache = []
    if os.path.exists("/var/aap/cache"):
        cache.append({"name" : "/var/aap/cache"})
    if home:
        cache.append({"name" : os.path.join(home, "cache")})
    cache.append({"name" : in_aap_dir("cache")})
    rd["CACHEPATH"] = dictlist2str(cache)

    # $BDIR
    if os.name == "posix":
        def fixname(n):
            """Change all non-letters, non-digits in "n" to underscores and
            return the result."""
            s = ''
            for c in n:
                if c in string.letters + string.digits:
                    s = s + c
                else:
                    s = s + '_'
            return s

        sysname, n, release, v, m = os.uname()
	rd["OSNAME"] = fixname(sysname) + fixname(release)
    else:
        rd["OSNAME"] = os.name
    rd["BDIR"] = "build-" + rd["OSNAME"]

    # $OSTYPE
    n = os.name
    if os.name == "dos":
        n = "msdos"
    elif os.name == "nt":
        n = "mswin"
    rd["OSTYPE"] = n

    # $MESSAGE
    msg_init(rd)

    # $SRCPATH (to be evaluated when used)
    rd["SRCPATH"] = ExpandVar(". $BDIR")

    # Standard variables.
    rd["bar"] = '|'
    rd["br"] = '\n'
    rd["BR"] = '\n'
    rd["empty"] = ''
    rd["gt"] = '>'
    rd["lt"] = '<'
    rd["pipe"] = '|'

    # Variables for a port recipe.
    rd["DISTDIR"] = "distfiles"
    rd["PATCHDISTDIR"] = "patches"
    rd["WRKDIR"] = "work"

    # $CACHEUPDATE
    rd["CACHEUPDATE"] = "12 hour"

    # $AAPVERSION
    rd["AAPVERSION"] = int(version_number)

    # Directory where our modules are.
    if Global.aap_rootdir:
        # Use rootdir if possible, when started as "../Exec/aap" __file__ will
        # be a relative file name, that doesn't work after chdir().
        dir = Global.aap_rootdir
    else:
        # Just in case rootdir wasn't set (never happens?).
        dir, tail = os.path.split(__file__)
        Global.set_aap_rootdir(dir)

    # $AAP: Execute aap, making sure the right version of Python is used.
    #       Use quotes where approriate, the path may have a space.
    rd["AAP"] = (listitem2str(sys.executable) + ' '
                                  + listitem2str(os.path.join(dir, "Main.py")))

    #
    # Read A-A-P default recipes.
    #
    read_recipe([], rd, os.path.join(dir, "default.aap"), 1, optional = 1)

    # Keep a copy of the result for the "_default" scope.
    rd["_default"] = Scope.RecipeDict(rd)

    # Read system and user default recipes.
    # Keep a copy of the result for the "_start" scope.
    for dir in default_dirs(rd):
        read_recipe_dir(rd, os.path.join(dir, "startup"))
    rd["_start"] = Scope.RecipeDict(rd)


class Work:
    def __init__(self, recdict = None):
        if recdict is None:
            self.topscope = Scope.create_topscope("toplevel")
            self.recdict = self.topscope.data
        else:
            # The ":execute" command may pass on a scope.
            self.topscope = recdict["_top"]
            self.recdict = recdict
        self.dependencies = []
        self.rules = []
        self.routes = {}
        self.nodes = {}
        self.absnodes = {}
        self.aliasnodes = {}
        self.top_recipe = None
        self.recipe_already_read = {}
        self.module_already_read = {}

        # This makes it possible to find "work" from the global variables.
        setwork(self.recdict, self)


    def add_dependency(self, rpstack, dep):
        """Add a new dependency.  This takes care of creating nodes for the
           sources and targets and setting the dependency for the target nodes
           if the dependency has commands."""
        self.dependencies.append(dep);

        # For each target let the Node know this Depend uses it.  If there are
        # commands also let it know this Depend builds it.
        for item in dep.targetlist:
            n = item["_node"]
            n.add_dependency(dep)
            if dep.commands:
                if (n.get_first_build_dependency()
                                    and not n.name in Global.virtual_targets):
                    from Process import recipe_error
                    recipe_error(rpstack,
                            _('Multiple build commands for target "%s"')
                                                                % item["name"])
                n.add_build_dependency(dep)


    def dictlist_nodes(self, dictlist):
        """Make sure there is a global node for each item in "dictlist" and
           add a reference to the node in the dictlist item.
           Carry over specific attributes to the node."""
        for item in dictlist:
            n = self.get_node(item["name"], 1)
            n.set_sticky_attributes(item)
            item["_node"] = n

    def add_dictlist_nodes(self, dictlist):
        """Add nodes for all items in "dictlist".  Also carry over attributes
        to the Node."""
        for item in dictlist:
            self.get_node(item["name"], 1, item)


    def add_node(self, node):
        """Add a Node to the global list of nodes.  The Node is the target
           and/or source in a dependency."""
        self.nodes[fname_fold(node.name)] = node
        self.absnodes[fname_fold(node.absname)] = node

    def del_node(self, node):
        """Remove a node from the global list of nodes."""
        del self.nodes[fname_fold(node.name)]
        del self.absnodes[fname_fold(node.absname)]

    def add_node_alias(self, node):
        """Add a Node to the global list of alias nodes."""
        self.aliasnodes[fname_fold(node.absalias)] = node


    def find_node(self, name, absname = None, use_alias = 1):
        """
        Find an existing Node by name or absname.
        For a virtual node "absname" should be an empty string (not None!).
        If "absname" is given it must have gone through expanduser() and
        abspath().
        """
        # First try the absolute name, it's more reliable.  Must not be used
        # for virtual nodes though.
        # Then check the short name, only for virtual nodes (may have been used
        # in another recipe).
        if absname is None:
            absname = os.path.abspath(os.path.expanduser(name))

        # Might try again with a folded name if fname_fold() may return a
        # different file name.  This is just to optimize the speed.
        if fname_fold_same():
            r = [0]
        else:
            r = [1, 0]

        if absname:
            findname = absname
            for i in r:
                if self.absnodes.has_key(findname):
                    return self.absnodes[findname]
                if i:
                    findname = fname_fold(absname)

        for i in r:
            findname = name
            if self.nodes.has_key(findname):
                n = self.nodes[findname]
                if n.attributes.get("virtual"):
                    return n
            if i:
                findname = fname_fold(name)

        # Now try again to find an alias.  Only use the absolute name, virtual
        # nodes don't have an alias.
        if absname and use_alias:
            findname = absname
            for i in r:
                if self.aliasnodes.has_key(findname):
                    return self.aliasnodes[findname]
                if i:
                    findname = fname_fold(absname)

        return None


    def get_node(self, name, add = 0, dict = {}, use_alias = 1):
        """Find a Node by name, create a new one if necessary.
           A new node is added to the global list if "add" is non-zero.
           When "dict" is given, check for attributes that apply to the
           Node."""
        absname = os.path.abspath(os.path.expanduser(name))
        n = self.find_node(name, absname, use_alias = use_alias)
        if n is None:
            n = Node(name, absname)
            if add:
                self.add_node(n)
        elif not n.name_relative and not (
                        name[0] == '~' or os.path.isabs(name) or is_url(name)):
            # Remember the relative name was used, this matters for where
            # signatures are stored.
            n.name_relative = 1

        if dict:
            n.set_attributes(dict)

        return n

    def node_set_alias(self, name, alias, dict = {}):
        """
        Define a node "name" with an alias "alias".
        """
        n = self.find_node(alias, use_alias = 0)
        if n:
            # If a node already exists by the alias name it may be that the
            # alias was used before it was defined, e.g.:
            #    all : foo
            #    :program foo : foo.c
            # But avoid adding an alias if it's a real node:
            #    :program foo : foo.c
            #    :lib foo : foo.c   # node = libfoo.a, don't use alias "foo"

            if n.alias:
                # Node already has an alias, thus it was not the situation that
                # the alias was used before it was defined.
                return

            if self.find_node(name, use_alias = 0):
                # New name also exists, don't set an alias.
                return

            # Node for alias name already exists, rename it.
            # Need to remove it from the global node list and add it back with
            # the new name.
            self.del_node(n)
            n.set_name(name)
            n.set_attributes(dict)
            self.add_node(n)
        else:
            # Create a new node.
            n = self.get_node(name, add = 1, dict = dict, use_alias = 0)
        n.set_alias(alias)
        self.add_node_alias(n)

    def add_node_attributes(self, dictlist):
        """Add attributes from existing nodes to the items in "dictlist".
           Used for sources and targets of executed dependencies and rules.
           Existing attributes are not overwritten."""
        for item in dictlist:
            node = self.find_node(item["name"])
            if node:
                for k in node.attributes.keys():
                    if not item.has_key(k):
                        item[k] = node.attributes[k]


    def add_rule(self, rule):
        self.rules.append(rule);

    def del_rule(self, rule):
        self.rules.remove(rule)

    def clearrules(self):
        self.rules = []

    def add_route(self, route):
        """
        Add a route to "routes".
        We actually add every possible route, since the input and output can be
        a list of filetype names.
        """
        for in_ftype in route.typelist[0]:
            for out_ftype in route.typelist[-1]:
                if not self.routes.has_key(in_ftype):
                    self.routes[in_ftype] = {}
                self.routes[in_ftype][out_ftype] = route

    def find_route(self, in_ftype, out_ftype, use_actions = 0):
        """
        Find a route for input filetype "in_ftype" to "out_ftype".
        Return the found route object or None.
        """
        l = self.routes.get(in_ftype)
        if l:
            l = l.get(out_ftype)
        if not l and use_actions:
            # No route specified, try using one or more actions.
            from Action import find_action_route
            l = find_action_route(in_ftype, out_ftype)
        return l


    def print_comments(self):
        """Print comments for all dependencies with a comment and for standard
        targets."""
        if not self.dependencies:
            msg_print(_("No dependencies in recipe"))
        else:
            # Collect the messages in toprint[].
            toprint = {}
            for d in self.dependencies:
                for t in d.targetlist:
                    comment = ''
                    if t.has_key("comment"):
                        comment = t["comment"]
                    else:
                        node = self.find_node(t["name"])
                        if node and node.attributes.has_key("comment"):
                            comment = node.attributes["comment"]
                    n = t["name"]
                    if comment:
                        toprint[n] = comment
                    elif n in Global.virtual_targets:
                        toprint[n] = _('standard target, no comment specified')

            # Sort the keys and print the messages.
            keys = toprint.keys()
            keys.sort()
            for n in keys:
                spaces = 12 - len(n)
                if spaces < 1:
                    spaces = 1
                msg_print(('target %s:' % n)
                                        + "            "[:spaces] + toprint[n])

def assert_attribute(recdict, dict, attrname):
    """Check if dictlist "dict" has an entry for attribute "attrname".
       If not, obtain it from any node that has this attribute."""
    if dict.has_key(attrname):
        return
    for node in getwork(recdict).nodes.values():
        if node.attributes.has_key(attrname):
            msg_extra(recdict, _('Using %s attribute from node "%s"')
                                                       % (attrname, node.name))
            dict[attrname] = node.attributes[attrname]
            return
    raise UserError, (_('Missing %s attribute for "%s"')
                                                    % (attrname, dict["name"]))


def setwork(recdict, work):
    """Set the Work object in 'recdict' to "work"."""
    recdict["_work"] = work

def getwork(recdict):
    """Return the Work object that contains 'recdict'.  Search scopes upto the
       toplevel."""
    return recdict["_no"].get("_work")

def setrpstack(recdict, rpstack):
    """Set the RecPos stack in 'recdict'.  This is separate for each scope."""
    recdict["_rpstack"] = rpstack

def getrpstack(recdict, line_nr = -1):
    """Return the RecPos stack in 'recdict'.
       When a line number is specified: Make a copy and set the line number for
       the item at the top of the stack."""
    rp = recdict["_rpstack"]
    if line_nr >= 0:
        rp = rpcopy(rp, line_nr)
    return rp


# A Route object contains:
#  recdict      - recdict of where the route was defined
#  rpstack      - rpstack of where the route was defined
#  default      - 1 when default route
#  typelist     - list of list of filetype names, input first
#  targetattr   - attributes for the target (e.g., "buildaction")
#  steplist     - list of steps to be taken, each step as a string.
#  lnumlist     - line number in recipe for each step
#
# Illustration:
#       :route intype1,intype2 xxtype outtype
#               oneaction $(source).xx
#               lastaction
#
# Too small to put in a separate file...
#

class Route:
    def __init__(self, recdict, rpstack, default, typelist,
                                               targetattr, steplist, lnumlist):
        self.recdict = recdict
        self.rpstack = rpstack
        self.default = default
        self.typelist = typelist
        self.targetattr = targetattr
        self.steplist = steplist
        self.lnumlist = lnumlist

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