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