# 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=foo.c@md5_c=012346...\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: .