#!/usr/bin/env python # # Utility functions to access git repository info and # generate source files and binary objects using templates. # # (c) 2011, The OpenPilot Team, http://www.openpilot.org # See also: The GNU Public License (GPL) Version 3 # from subprocess import Popen, PIPE from re import search, MULTILINE from datetime import datetime from string import Template import argparse import hashlib import sys class Repo: """A simple git repository HEAD commit info class This simple class provides object notation to access the git repository current commit info. If one needs better access one can try the GitPython class available here: http://packages.python.org/GitPython It is not installed by default, so we cannot rely on it. Example: r = Repo('/path/to/git/repository') print "path: ", r.path() print "origin: ", r.origin() print "hash: ", r.hash() print "short hash: ", r.hash(8) print "Unix time: ", r.time() print "commit date:", r.time("%Y%m%d") print "commit tag: ", r.tag() print "branch: ", r.branch() print "release tag:", r.reltag() """ def _exec(self, cmd): """Execute git using cmd as arguments""" self._git = 'git' git = Popen(self._git + " " + cmd, cwd=self._path, shell=True, stdout=PIPE, stderr=PIPE) self._out, self._err = git.communicate() self._rc = git.poll() def _get_origin(self): """Get and store the repository origin URL""" self._origin = None self._exec('remote -v') if self._rc == 0: m = search(r"^origin\s+(.+)\s+\(fetch\)$", self._out, MULTILINE) if m: self._origin = m.group(1) def _get_time(self): """Get and store HEAD commit timestamp in Unix format We use commit timestamp rather than the build time, so it always is the same for the current commit or tag. """ self._time = None self._exec('log -n1 --no-color --format=format:%ct HEAD') if self._rc == 0: self._time = self._out def _get_tag(self): """Get and store git tag for the HEAD commit""" self._tag = None self._exec('describe --exact-match HEAD') if self._rc == 0: self._tag = self._out.strip(' \t\n\r') def _get_branch(self): """Get and store current branch containing the HEAD commit""" self._branch = None self._exec('branch --contains HEAD') if self._rc == 0: m = search(r"^\*\s+(.+)$", self._out, MULTILINE) if m: self._branch = m.group(1) def __init__(self, path = "."): """Initialize object instance and read repo info""" self._path = path self._exec('rev-parse --verify HEAD') if self._rc == 0: self._hash = self._out.strip(' \t\n\r') self._get_origin() self._get_time() self._get_tag() self._get_branch() else: self._hash = None self._origin = None self._time = None self._tag = None self._branch = None def path(self): """Return the repository path""" return self._path def origin(self, none = None): """Return fetch origin of the repository""" if self._origin == None: return none else: return self._origin def hash(self, n = 40, none = None): """Return hash of the HEAD commit""" if self._hash == None: return none else: return self._hash[:n] def time(self, format = None, none = None): """Return Unix or formatted time of the HEAD commit""" if self._time == None: return none else: if format == None: return self._time else: return datetime.utcfromtimestamp(float(self._time)).strftime(format) def tag(self, none = None): """Return git tag for the HEAD commit or given string if none""" if self._tag == None: return none else: return self._tag def branch(self, none = None): """Return git branch containing the HEAD or given string if none""" if self._branch == None: return none else: return self._branch def info(self): """Print some repository info""" print "path: ", self.path() print "origin: ", self.origin() print "Unix time: ", self.time() print "commit date:", self.time("%Y%m%d") print "hash: ", self.hash() print "short hash: ", self.hash(8) print "branch: ", self.branch() print "commit tag: ", self.tag() def file_from_template(tpl_name, out_name, dict): """Create or update file from template using dictionary This function reads the template, performs placeholder replacement using the dictionary and checks if output file with such content already exists. If no such file or file data is different from expected then it will be ovewritten with new data. Otherwise it will not be updated so make will not update dependent targets. Example: # template.c: # char source[] = "${OUTFILENAME}"; # uint32_t timestamp = ${UNIXTIME}; # uint32_t hash = 0x${HASH8}; r = Repo('/path/to/git/repository') tpl_name = "template.c" out_name = "output.c" dictionary = dict( HASH8 = r.hash(8), UNIXTIME = r.time(), OUTFILENAME = out_name, ) file_from_template(tpl_name, out_name, dictionary) """ # Read template first tf = open(tpl_name, "rb") tpl = tf.read() tf.close() # Replace placeholders using dictionary out = Template(tpl).substitute(dict) # Check if output file already exists try: of = open(out_name, "rb") except IOError: # No file - create new of = open(out_name, "wb") of.write(out) of.close() else: # File exists - overwite only if content is different inp = of.read() of.close() if inp != out: of = open(out_name, "wb") of.write(out) of.close() def sha1(file): """Provides C source representation of sha1 sum of file.""" if file == None: return "" else: sha1 = hashlib.sha1() with open(file, 'rb') as f: for chunk in iter(lambda: f.read(8192), ''): sha1.update(chunk) hex_stream = lambda s:",".join(['0x'+hex(ord(c))[2:].zfill(2) for c in s]) return hex_stream(sha1.digest()) def main(): """This utility uses git repository in the current working directory or from the given path to extract some info about it and HEAD commit. Then some variables in the form of ${VARIABLE} could be replaced by collected data. Optional board type, board revision and sha1 sum of given image file could be applied as well or will be replaced by empty strings if not defined. If --info option is given, some repository info will be printed to stdout. If --format option is given then utility prints the format string after substitution to the standard output. If --outfile option is given then the --template option should be defined too. In that case the utility reads a template file, performs variable substitution and writes the result into output file. Output file will be overwritten only if its content differs from expected. Otherwise it will not be touched, so make utility will not remake dependent targets. """ # Parse command line. parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description = "Performs variable substitution in template file or string.", epilog = main.__doc__); parser.add_argument('--path', default='.', help='path to the git repository'); parser.add_argument('--info', action='store_true', help='print repository info to stdout'); parser.add_argument('--format', help='format string to print to stdout'); parser.add_argument('--template', help='name of template file'); parser.add_argument('--outfile', help='name of output file'); parser.add_argument('--image', help='name of image file for sha1 calculation'); parser.add_argument('--type', default="", help='board type, for example, 0x04 for CopterControl'); parser.add_argument('--revision', default = "", help='board revision, for example, 0x01'); args = parser.parse_args() # Process arguments. No advanced error handling is here. # Any error will raise an exception and terminate process # with non-zero exit code. r = Repo(args.path) dictionary = dict( TEMPLATE = args.template, OUTFILENAME = args.outfile, ORIGIN = r.origin(), HASH = r.hash(), HASH8 = r.hash(8), TAG_OR_BRANCH = r.tag(r.branch('unreleased')), TAG_OR_HASH8 = r.tag(r.hash(8, 'untagged')), UNIXTIME = r.time(), DATE = r.time('%Y%m%d'), BOARD_TYPE = args.type, BOARD_REVISION = args.revision, SHA1 = sha1(args.image), ) if args.info: r.info() if args.format != None: print Template(args.format).substitute(dictionary) if args.outfile != None: file_from_template(args.template, args.outfile, dictionary) return 0 if __name__ == "__main__": sys.exit(main())