# Part of the A-A-P recipe executive: Store signatures

# 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

#
# This module handles remembering signatures of targets and sources.
#

import os
import os.path
import string

from Util import *
from Message import *

# Both "signatures" dictionaries are indexed by the name of the target Node
# (file or directory).
# For non-virtual nodes the absulute name is used.
# Each entry is a dictionary indexed by the source-name@check-name and has a
# string value.
# The "buildcheck" entry is used for the build commands.
# The "dir" entry is used to remember the sign file that stores the signatures
# for this target.
# "old_signatures" is for the signatures when we started.
# "upd_signatures" is for the signatures of items for which the build commands
# were successfully executed and are to be stored for the next time.
# Example:
# {"/aa/bb/file.o" : {	"dir" : "/aa/bb",
#			"/aa/bb/file.c@md5" : "13a445e5",
#			"buildcheck" : "-O2"},
#  "/aa/bb/bar.o"  : {	"dir" : "/aa/bb",
#			"/aa/bb/bar-debug.c@time" : "143234",
#			"aa/bb/bar.h@time" : "423421"}}
old_signatures = {}
upd_signatures = {}

# "new_signatures" caches the signatures we computed this invocation.  It is a
# dictionary of dictionaries.  The key for the toplevel dictionary is the Node
# name.  The key for the second level is the check name.  The target name isn't
# used here.
new_signatures = {}

# Name for the sign file relative to the directory of the target or the recipe.
sign_file_name = "aap/sign"

# Remember for which directories the sign file has been read.
# Also when the file couldn't actually be read, so that we remember to write
# this file when signs have been updated.
# An entry exists when the file has been read.  It's non-zero when the file
# should be written back.
sign_dirs = {}

def get_sign_file(target, update):
    """Get the sign file that is used for "target" if it wasn't done already.
       When "update" is non-zero, mark the file needs writing."""
    dir = target.get_sign_dir()
    if not sign_dirs.has_key(dir):
	sign_dirs[dir] = update
	sign_read(dir)
    elif update and not sign_dirs[dir]:
	sign_dirs[dir] = 1


# In the sign files, file names are stored with a leading "-" for a virtual
# node and "=" for a file name.  Expand to an absolute name for non-virtual
# nodes.
def sign_expand_name(dir, name):
    """Expand "name", which is used in a sign file in directory "dir"."""
    n = name[1:]
    if name[0] == '-':
	return n
    if os.path.isabs(n):
	return n
    return os.path.normpath(os.path.join(dir, n))

def sign_reduce_name(dir, name):
    """Reduce "name" to what is used in a sign file."""
    if os.path.isabs(name):
	return '=' + shorten_name(name, dir)
    return '-' + name

#
# A sign file stores the signatures for items (sources and targets) with the
# values they when they were computed in the past.
# The format of each line is:
#  	=foo.o<ESC>=foo.c@md5_c=012346<ESC>...<ESC>\n
# "md5_c" can be "md5", "time", etc.  Note that it's not always equal to
# the "check" attribute, both "time" and "older" use "time" here.

def sign_read(dir):
    """Read the signature file for directory "dir" into our dictionary of
    signatures."""
    fname = os.path.join(dir, sign_file_name)
    try:
	f = open(fname, "rb")
	for line in f.readlines():
	    e = string.find(line, "\033")
	    if e > 0:	# Only use lines with an ESC
		name = sign_expand_name(dir, line[:e])
		old_signatures[name] = {"dir" : dir}
		while 1:
		    s = e + 1
		    e = string.find(line, "\033", s)
		    if e < 1:
			break
		    i = string.rfind(line, "=", s, e)
		    if i < 1:
			break
		    old_signatures[name][sign_expand_name(dir, line[s:i])] \
								= line[i + 1:e]
	f.close()
    except StandardError, e:
	# TODO: handle errors?  It's not an error if the file does not exist.
	msg_warning((_('Cannot read sign file "%s": ')
					       % shorten_name(fname)) + str(e))


def sign_write_all():
    """Write all updated signature files from our dictionary of signatures."""

    # This assumes we are the only one updating this signature file, thus there
    # is no locking.  It wouldn't make sense sharing with others, since
    # building would fail as well.
    for dir in sign_dirs.keys():
	if sign_dirs[dir]:
	    # This sign file needs to be written.
	    sign_write(dir)


def sign_write(dir):
    """Write one updated signature file."""
    fname = os.path.join(dir, sign_file_name)

    sign_dir = os.path.dirname(fname)
    if not os.path.exists(sign_dir):
	try:
	    os.makedirs(sign_dir)
	except StandardError, e:
	    msg_warning((_('Cannot create directory for signature file "%s": ')
							     % fname) + str(e))
    try:
	f = open(fname, "wb")
    except StandardError, e:
	msg_warning((_('Cannot open signature file for writing: "%s": '),
							       fname) + str(e))
	return

    def write_sign_line(f, dir, s, old, new):
	"""Write a line to sign file "f" in directory "dir" for item "s", with
	checks from "old", using checks from "new" if they are present."""
	f.write(sign_reduce_name(dir, s) + "\033")

	# Go over all old checks, write all of them, using the new value
	# if it is available.
	for c in old.keys():
	    if c != "dir":
		if new and new.has_key(c):
		    val = new[c]
		else:
		    val = old[c]
		f.write("%s=%s\033" % (sign_reduce_name(dir, c), val))

	# Go over all new checks, write the ones for which there is no old
	# value.
	if new:
	    for c in new.keys():
		if c != "dir" and not old.has_key(c):
		    f.write("%s=%s\033" % (sign_reduce_name(dir, c), new[c]))

	f.write("\n")

    try:
	# Go over all old signatures, write all of them, using checks from
	# upd_signatures when they are present.
	# When the item is in upd_signatures, use the directory specified
	# there, otherwise use the directory of old_signatures.
	for s in old_signatures.keys():
	    if upd_signatures.has_key(s):
		if upd_signatures[s]["dir"] != dir:
		    continue
		new = upd_signatures[s]
	    else:
		if old_signatures[s]["dir"] != dir:
		    continue
		new = None
	    write_sign_line(f, dir, s, old_signatures[s], new)


	# Go over all new signatures, write only the ones for which there is no
	# old signature.
	for s in upd_signatures.keys():
	    if (not old_signatures.has_key(s)
					  and upd_signatures[s]["dir"] == dir):
		write_sign_line(f, dir, s, upd_signatures[s], None)

	f.close()
    except StandardError, e:
	msg_warning((_('Write error for signature file "%s": '),
							       fname) + str(e))

def hexdigest(m):
    """Turn an md5 object into a string of hex characters."""
    # NOTE:  This routine is a method in the Python 2.0 interface
    # of the native md5 module, not in Python 1.5.
    h = string.hexdigits
    r = ''
    for c in m.digest():
	i = ord(c)
	r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
    return r


def check_md5(fname):
    import md5
    try:
	f = open(fname, "rb")
	m = md5.new()
	while 1:
	    # Read big blocks at a time for speed, but don't read the whole
	    # file at once to reduce memory usage.
	    data = f.read(32768)
	    if not data:
		break
	    m.update(data)
	f.close()
	res = hexdigest(m)
    except:
	# Can't open a URL here.
	# TODO: error message?
	res = "unknown"
    return res


def buildcheckstr2sign(str):
    """Compute a signature from a string for the buildcheck."""
    import md5
    return hexdigest(md5.new(str))


def _sign_lookup(signatures, name, key):
    """Get the "key" signature for item "name" from dictionary "signatures"."""
    if not signatures.has_key(name):
	return ''
    s = signatures[name]
    if not s.has_key(key):
	return ''
    return s[key]


def sign_clear(name):
    """Clear the new signatures of an item.  Used when it has been build."""
    if new_signatures.has_key(name):
	new_signatures[name] = {}


def get_new_sign(globals, name, check):
    """Get the current "check" signature for the item "name".
       "name" is the absolute name for non-virtual nodes.
       This doesn't depend on the target.  "name" can be a URL.
       Returns a string (also for timestamps)."""
    key = check
    res = _sign_lookup(new_signatures, name, key)
    if not res:
	# Compute the signature now
	# TODO: other checks!  User defined?
	if check == "time":
	    from Remote import url_time
	    res = str(url_time(globals, name))
	elif check == "md5":
	    res = check_md5(name)
	elif check == "c_md5":
	    # TODO: filter out comments en spans of white space
	    res = check_md5(name)
	else:
	    res = "unknown"

	# Store the new signature to avoid recomputing it many times.
	if not new_signatures.has_key(name):
	    new_signatures[name] = {}
	new_signatures[name][key] = res

    return res

def sign_clear_target(target):
    """Called to clear old signatures after successfully executing build rules
       for "target".  sign_updated() should be called next for each source."""
    get_sign_file(target, 1)
    target_name = target.get_name()
    if old_signatures.has_key(target_name):
	del old_signatures[target_name]
    if upd_signatures.has_key(target_name):
	del upd_signatures[target_name]


def _sign_upd_sign(target, key, value):
    """Update signature for node "target" with "key" to "value"."""
    get_sign_file(target, 1)
    target_name = target.get_name()
    if not upd_signatures.has_key(target_name):
	upd_signatures[target_name] = {"dir": target.get_sign_dir()}
    upd_signatures[target_name][key] = value


def sign_updated(globals, name, check, target):
    """Called after successfully executing build rules for "target" from item
       "name", using "check"."""
    res = get_new_sign(globals, name, check)
    _sign_upd_sign(target, name + '@' + check, res)


def buildcheck_updated(target, value):
    """Called after successfully executing build rules for node "target" with
       the new buildcheck signature "value"."""
    _sign_upd_sign(target, '@buildcheck', value)


def get_old_sign(name, check, target):
    """Get the old "check" signature for item "name" and target node "target".
       If it doesn't exist an empty string is returned."""
    get_sign_file(target, 0)
    key = name + '@' + check
    return _sign_lookup(old_signatures, target.get_name(), key)

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