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