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