# Part of the A-A-P recipe executive: Node used in a recipe

# Copyright (C) 2002 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 *

#
# 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 "finally" 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.name = name

	self.attributes = {}		# dictionary of attributes; relevant
					# ones are "directory", "virtual" and
					# "cache_update"
	import Global
	from Remote import is_url

	if name in Global.virtual_targets:
	    self.attributes["virtual"] = 1

	# Remember the directory of the recipe where the node was first used.
	# "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) or is_url(name):
	    self.name_relative = 0
	    if absname is None:
		absname = os.path.normpath(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

	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

	# 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.auto_depend = None		# dictlist for automatic dependencies
	self.auto_depend_rec = 0	# auto_depend generated recursively
	self.did_auto_depend = 0	# used in dictlist_update()
	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 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)


    # 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"):
	    dir = os.path.abspath(os.path.expanduser(
					     self.attributes["signdirectory"]))
	elif self.name_relative and not self.attributes.get("virtual"):
	    dir = os.path.dirname(self.absname)
	else:
	    dir = self.sign_dir

	from Sign import sign_normal_fname
	return os.path.join(dir, 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):
	if self.build_dependencies:
	    return self.build_dependencies[0]
	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 sts=4 tw=79 fo+=l:
