# 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: