# Part of the A-A-P recipe executive: Node used in a 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

import os

from Util import *
import Filetype

#
# A Node object is the source and/or target of a dependency.
# main items:
# name              name as used in the recipe (at first use, it's not changed
#                   when also used in a recipe in another directory)
# recipe_dir        directory in which "name" is valid
# absname           name with absolute, normalized path (meaningless for
#                   virtual targets, use get_name())
# attributes        dictionary for attributes, such as "virtual"
# dependencies      a list of references to the dependencies in which the node
#                   is a target.  It can be empty.
# build_dependencies  subset of "dependencies" for the ones that have build
#                   commands.  Only nodes like "finally", "clean" and "fetch"
#                   may have multiple entries.

class Node:

    # values used for "status"
    new = 1         # newly created
    busy = 2        # busy updating as a target
    updated = 3     # successfully updated
    builderror = 4  # attempted to update and failed.

    def __init__(self, name, absname = None):
        """Create a node for "name".  Caller must have already made sure "name"
        is normalized (but not made absolute)."""

        self.attributes = {}            # dictionary of attributes; relevant
                                        # ones are "directory", "virtual" and
                                        # "cache_update"
        self.set_name(name, absname)
        self.alias = None               # optional alias name
        self.absalias = None            # optional alias absolute name

        self._set_sign_dir(4)

        self.dependencies = []          # dependencies for which this node is a
                                        # target
        self.build_dependencies = []    # idem, with build commands

        self.status = Node.new          # current status

        self.scope_recdict = None       # Scope in which the node was used as a
                                        # target.  Used for finding rules that
                                        # may apply.

        # When status is "busy", either current_rule or current_dep indicates
        # which rule or dependency is being used, so that a clear error message
        # can be given for cyclic dependencies.
        self.current_rule = None
        self.current_dep = None

        self.autodep_dictlist = None    # dictlist for items this node depends
                                        # on, from automatic dependencies
        self.autodep_recursive = 0      # autodep_dictlist generated recursively
        self.autodep_busy = 0           # used in dictlist_update()
        self.did_add_clean = 0          # used by add_clean()
        self.recursive_level = 0        # depth of building recursively

    def copy(self):
        """Make a copy of a Node object.  This is a shallow copy of most
           things, but the attributes and dependencies are done an extra
           level."""
        import copy

        r = copy.copy(self)
        r.attributes = copy.copy(self.attributes)
        r.dependencies = copy.copy(self.dependencies)
        r.build_dependencies = copy.copy(self.build_dependencies)
        return r

    def rpstack(self):
        """
        Get a useful rpstack for when this node is used as a target (for error
        messages).
        """
        if self.current_rule:
            return self.current_rule.rpstack
        if self.current_dep:
            return self.current_dep.rpstack
        return []

    def set_name(self, name, absname = None):
        """Set the name of the node.  Used when creating a new node and when an
           alias is going to be used."""
        import Global
        from Remote import is_url

        self.name = name

        # Set "virtual" when it's a know virtual target.
        if name in Global.virtual_targets:
            self.attributes["virtual"] = 1

        # Remember the directory of the recipe where the node name was set.
        # "name" is relative to this directory unless it's virtual.
        self.recipe_dir = os.getcwd()

        # Remember the absolute path for the Node.  When it's virtual absname
        # should not be used!  Use get_name() instead.
        # A URL, "~/" and "~user/" are also absolute.
        if os.path.isabs(name):
            self.name_relative = 0
            if absname is None:
                absname = os.path.normpath(name)
        elif is_url(name):
            self.name_relative = 0
            if absname is None:
                absname = name
        elif name[0] == '~':
            self.name_relative = 0
            if absname is None:
                absname = os.path.abspath(os.path.expanduser(name))
        else:
            self.name_relative = 1
            if absname is None:
                absname = os.path.abspath(name)
        self.absname = absname

    def set_alias(self, name, absname = None):
        """Set the alias name for this node."""
        self.alias = name
        if absname:
            self.absalias = absname
        else:
            self.absalias = os.path.abspath(os.path.expanduser(name))

    def get_name(self):
        """Get the name to be used for this Node.  When the "virtual" attribute
           is given it's the unexpanded name, otherwise the absolute name."""
        if self.attributes.get("virtual"):
            return self.name
        return self.absname

    def short_name(self):
        """Get the shortest name that still makes clear what the name of the
           node is.  Used for messages."""
        if self.attributes.get("virtual"):
            return self.name
        return shorten_name(self.absname)

    def get_ftype(self, recdict):
        """Return the detected file type for this node."""
        ft = self.attributes.get("filetype")
        if ft:
            return ft
        # A ":program" command adds a lower priority filetype attribute.
        ft = self.attributes.get("filetypehint")
        if ft:
            return ft
        return Filetype.ft_detect(self.get_name(), recdict = recdict)


    # When the Node is used as a target, we must decide where the
    # signatures are stored.  The priority order is:
    # 1. When used with a relative path name, but no "virtual" attribute, use
    #    the directory of the target.
    # 2. When a dependency with build commands is defined with this Node as
    #    a target, use the directory of that recipe.
    # 3. When any dependency is defined with this node as a target, use the
    #    directory of that recipe.
    # 4. Use the directory of the recipe where this Node was first used.
    # This can be overruled with the "signdirectory" attribute.
    # CAREFUL: the "virtual" and "signdirectory" attributes may be changed!
    # When adding the "virtual" attribute level 1 is skipped, thus the choice
    # between level 2, 3 or 4 must be remembered separately.
    def _set_sign_dir(self, level):
        """Set the directory for the signatures to the directory of the target
        (for level 1) or the current directory (where the recipe is)."""
        self.sign_dir = os.getcwd()
        self.sign_level = level

    def get_sign_fname(self):
        """Get the file name to use for the signatures of this node.
           When using a directory, append "sign_fname"."""
        if self.attributes.has_key("signfile"):
            return os.path.abspath(os.path.expanduser(
                                                  self.attributes["signfile"]))
        if self.attributes.has_key("signdirectory"):
            adir = os.path.abspath(os.path.expanduser(
                                             self.attributes["signdirectory"]))
        elif self.name_relative and not self.attributes.get("virtual"):
            adir = os.path.dirname(self.absname)
        else:
            adir = self.sign_dir

        from Sign import sign_normal_fname
        return os.path.join(adir, sign_normal_fname)

    def relative_name(self):
        """This node has been used with a relative file name, which means the
           target directory is to be used for signatures, unless the "virtual"
           attribute is used (may be added later)."""
        self.name_relative = 1

    def add_dependency(self, dependency):
        self.dependencies.append(dependency)
        if self.sign_level > 3:
            self._set_sign_dir(3)

    def get_dependencies(self):
        return self.dependencies

    def add_build_dependency(self, dependency):
        self.build_dependencies.append(dependency)
        if self.sign_level > 2:
            self._set_sign_dir(2)

    def get_first_build_dependency(self):
        """Return the first build dependency, None if there isn't one."""
        if self.build_dependencies:
            return self.build_dependencies[0]
        return None

    def get_recipe_build_dependency(self, recdict):
        """Return the first build dependency for recipe with "recdict", None if
           there isn't one."""
        for dep in self.build_dependencies:
            if dep.buildrecdict is recdict:
                return dep
        return None

    def get_build_dependencies(self):
        return self.build_dependencies

    def set_attributes(self, dictlist):
        """Set attributes for a node from "dictlist".  Skip "name" and items
        that start with an underscore."""
        for k in dictlist.keys():
            if k == "virtual" and self.attributes.has_key(k):
                # The "virtual" attribute is never reset
                self.attributes[k] = (self.attributes[k] or dictlist[k])
            elif k != "name" and k[0] != '_':
                self.attributes[k] = dictlist[k]

    def set_sticky_attributes(self, dictlist):
        """Set only those attributes for the node from "dictlist" that can be
           carried over from a dependency to everywhere else the node is
           used."""
        for attr in ["virtual", "remember", "directory", "filetype", "force",
                       "constant", "fetch", "commit", "publish", "signfile",
                       "depdir"]:
            if dictlist.has_key(attr) and dictlist[attr]:
                self.attributes[attr] = dictlist[attr]

    def get_cache_update(self):
        """Get the cache_update attribute.  Return None if it's not set."""
        if self.attributes.has_key("cache_update"):
            return self.attributes["cache_update"]
        return None

    def isdir(self):
        """Return non-zero when we know this Node is a directory.  When
           specified with set_attributes() return the value used (mode value
           for creation)."""
        # A specified attribute overrules everything
        if self.attributes.has_key("directory"):
            return self.attributes["directory"]
        # A virtual target can't be a directory
        if self.attributes.get("virtual"):
            return 0
        # Check if the node exists and is a directory
        import os.path
        if os.path.isdir(self.get_name()):
            return 1
        # Must be a file then
        return 0

    def may_fetch(self):
        """Return non-zero if this node should be fetched when using the
           "fetch" target or ":fetch"."""
        # Never fetch a virtual node.
        # Fetching is skipped when the node has a "constant" attribute with
        # a non-empty non-zero value and the file exists.
        return (not self.attributes.get("virtual")
                and (not self.attributes.get("constant")
                    or not os.path.exists(self.get_name())))

    def __str__(self):
        return "Node " + self.get_name()



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