From bf9a3ef7d36c108b46f53b20025e6b3aca73d7fc Mon Sep 17 00:00:00 2001 From: Oleg Semyonov Date: Tue, 17 May 2011 21:29:22 +0300 Subject: [PATCH] OP-483: add version-info.py script for software, firmware and release packaging --- make/scripts/version-info.py | 299 +++++++++++++++++++++++++++++++++++ release/Makefile | 7 +- 2 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 make/scripts/version-info.py diff --git a/make/scripts/version-info.py b/make/scripts/version-info.py new file mode 100644 index 000000000..ad747ce74 --- /dev/null +++ b/make/scripts/version-info.py @@ -0,0 +1,299 @@ +#!/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()) diff --git a/release/Makefile b/release/Makefile index 882070fb6..cfb131028 100644 --- a/release/Makefile +++ b/release/Makefile @@ -13,9 +13,8 @@ ROOT_DIR := $(realpath $(WHEREAMI)/../) # Set up some macros BUILD_DIR := $(ROOT_DIR)/build -RELEASE_DATE := $(shell date +%Y%m%d) -RELEASE_TAG := unreleased -RELEASE_LBL := $(RELEASE_DATE)-$(RELEASE_TAG) +VERSION_CMD := python $(ROOT_DIR)/make/scripts/version-info.py --path=$(ROOT_DIR) +RELEASE_LBL := $(shell $(VERSION_CMD) --format=\$${DATE}-\$${TAG_OR_HASH8}) RELEASE_DIR := $(BUILD_DIR)/release-$(RELEASE_LBL) FW_DIR := $(RELEASE_DIR)/firmware-$(RELEASE_LBL) BL_DIR := $(FW_DIR)/bootloaders @@ -89,7 +88,7 @@ $(eval $(call INSTALL_TEMPLATE,blupd_ahrs,all_bl,$(BLUPD_DIR),AHRS_,-$(RELEASE_L $(eval $(call INSTALL_TEMPLATE,blupd_openpilot,all_bl,$(BLUPD_DIR),OpenPilot_,-$(RELEASE_LBL),,,blupd_openpilot,install)) $(eval $(call INSTALL_TEMPLATE,blupd_pipxtreme,all_bl,$(BLUPD_DIR),PipXtreme_,-$(RELEASE_LBL),,,blupd_pipxtreme,install)) -# CopterControl flash eraser tool (change fw_spektrum to fw_ppm if fw_ppm is enabled in release_fw target below) +# CopterControl flash eraser tool $(eval $(call INSTALL_TEMPLATE,fw_tools,uavobjects,$(BLUPD_DIR),,-FlashEraser-$(RELEASE_LBL),ERASE_FLASH=YES,clean,$(FW_TARGETS_TOOLS),install)) # Order-only dependencies