OP-1483 Added velocity filter to correct EKF's velocity estimate for static velocity...
[librepilot.git] / make / scripts / version-info.py
blob79cf9590423192ce959aac54c52059d163afd43b
1 #!/usr/bin/env python
3 # Utility functions to access git repository info and
4 # generate source files and binary objects using templates.
6 # (c) 2011, The OpenPilot Team, http://www.openpilot.org
7 # See also: The GNU Public License (GPL) Version 3
10 from subprocess import Popen, PIPE
11 from re import search, MULTILINE
12 from datetime import datetime
13 from string import Template
14 import optparse
15 import hashlib
16 import sys
18 class Repo:
19 """A simple git repository HEAD commit info class
21 This simple class provides object notation to access
22 the git repository current commit info. If one needs
23 better access one can try the GitPython class available
24 here:
25 http://packages.python.org/GitPython
26 It is not installed by default, so we cannot rely on it.
28 Example:
29 r = Repo('/path/to/git/repository')
30 print "path: ", r.path()
31 print "origin: ", r.origin()
32 print "hash: ", r.hash()
33 print "short hash: ", r.hash(8)
34 print "Unix time: ", r.time()
35 print "commit date:", r.time("%Y%m%d")
36 print "commit tag: ", r.tag()
37 print "branch: ", r.branch()
38 print "release tag:", r.reltag()
39 """
41 def _exec(self, cmd):
42 """Execute git using cmd as arguments"""
43 self._git = 'git'
44 git = Popen(self._git + " " + cmd, cwd = self._path,
45 shell = True, stdout = PIPE, stderr = PIPE)
46 self._out, self._err = git.communicate()
47 self._rc = git.poll()
49 def _get_origin(self):
50 """Get and store the repository fetch origin path"""
51 self._origin = None
52 self._exec('remote -v')
53 if self._rc == 0:
54 m = search(r"^origin\s+(.+)\s+\(fetch\)$", self._out, MULTILINE)
55 if m:
56 self._origin = m.group(1)
58 def _get_time(self):
59 """Get and store HEAD commit timestamp in Unix format
61 We use commit timestamp rather than the build time,
62 so it always is the same for the current commit or tag.
63 """
64 self._time = None
65 self._exec('log -n1 --no-color --format=format:%ct HEAD')
66 if self._rc == 0:
67 self._time = self._out
69 def _get_tag(self):
70 """Get and store git tag for the HEAD commit"""
71 self._tag = None
72 self._exec('describe --tags --exact-match HEAD')
73 if self._rc == 0:
74 self._tag = self._out.strip(' \t\n\r')
76 def _get_branch(self):
77 """Get and store current branch containing the HEAD commit"""
78 self._branch = None
79 self._exec('branch --contains HEAD')
80 if self._rc == 0:
81 m = search(r"^\*\s+(.+)$", self._out, MULTILINE)
82 if m:
83 self._branch = m.group(1)
85 def _get_dirty(self):
86 """Check for dirty state of repository"""
87 self._dirty = False
88 self._exec('update-index --refresh --unmerged')
89 self._exec('diff-index --name-only --exit-code --quiet HEAD')
90 if self._rc:
91 self._dirty = True
93 def __init__(self, path = "."):
94 """Initialize object instance and read repo info"""
95 self._path = path
96 self._exec('rev-parse --verify HEAD')
97 if self._rc == 0:
98 self._hash = self._out.strip(' \t\n\r')
99 self._get_origin()
100 self._get_time()
101 self._get_tag()
102 self._get_branch()
103 self._get_dirty()
104 else:
105 self._hash = None
106 self._origin = None
107 self._time = None
108 self._tag = None
109 self._branch = None
110 self._dirty = None
112 def path(self):
113 """Return the repository path"""
114 return self._path
116 def origin(self, none = None):
117 """Return fetch origin of the repository"""
118 if self._origin == None:
119 return none
120 else:
121 return self._origin
123 def hash(self, n = 40, none = None):
124 """Return hash of the HEAD commit"""
125 if self._hash == None:
126 return none
127 else:
128 return self._hash[:n]
130 def time(self, format = None, none = None):
131 """Return Unix or formatted time of the HEAD commit"""
132 if self._time == None:
133 return none
134 else:
135 if format == None:
136 return self._time
137 else:
138 return datetime.utcfromtimestamp(float(self._time)).strftime(format)
140 def tag(self, none = None):
141 """Return git tag for the HEAD commit or given string if none"""
142 if self._tag == None:
143 return none
144 else:
145 return self._tag
147 def branch(self, none = None):
148 """Return git branch containing the HEAD or given string if none"""
149 if self._branch == None:
150 return none
151 else:
152 return self._branch
154 def dirty(self, dirty = "-dirty", clean = ""):
155 """Return git repository dirty state or empty string"""
156 if self._dirty:
157 return dirty
158 else:
159 return clean
161 def label(self):
162 """Return package label (tag if defined, or date-hash if no tag)"""
163 try:
164 if self._tag == None:
165 return ''.join([self.time('%Y%m%d'), "-", self.hash(8, 'untagged'), self.dirty()])
166 else:
167 return ''.join([self.tag(''), self.dirty()])
168 except:
169 return None
171 def revision(self):
172 """Return full revison string (tag if defined, or branch:hash date time if no tag)"""
173 try:
174 if self._tag == None:
175 return ''.join([self.branch('no-branch'), ":", self.hash(8, 'no-hash'), self.dirty(), self.time(' %Y%m%d %H:%M')])
176 else:
177 return ''.join([self.tag(''), self.dirty()])
178 except:
179 return None
181 def info(self):
182 """Print some repository info"""
183 print "path: ", self.path()
184 print "origin: ", self.origin()
185 print "Unix time: ", self.time()
186 print "commit date:", self.time("%Y%m%d")
187 print "hash: ", self.hash()
188 print "short hash: ", self.hash(8)
189 print "branch: ", self.branch()
190 print "commit tag: ", self.tag('')
191 print "dirty: ", self.dirty('yes', 'no')
192 print "label: ", self.label()
193 print "revision: ", self.revision()
195 def file_from_template(tpl_name, out_name, dict):
196 """Create or update file from template using dictionary
198 This function reads the template, performs placeholder replacement
199 using the dictionary and checks if output file with such content
200 already exists. If no such file or file data is different from
201 expected then it will be ovewritten with new data. Otherwise it
202 will not be updated so make will not update dependent targets.
204 Example:
205 # template.c:
206 # char source[] = "${OUTFILENAME}";
207 # uint32_t timestamp = ${UNIXTIME};
208 # uint32_t hash = 0x${HASH8};
210 r = Repo('/path/to/git/repository')
211 tpl_name = "template.c"
212 out_name = "output.c"
214 dictionary = dict(
215 HASH8 = r.hash(8),
216 UNIXTIME = r.time(),
217 OUTFILENAME = out_name,
220 file_from_template(tpl_name, out_name, dictionary)
223 # Read template first
224 tf = open(tpl_name, "rb")
225 tpl = tf.read()
226 tf.close()
228 # Replace placeholders using dictionary
229 out = Template(tpl).substitute(dict)
231 # Check if output file already exists
232 try:
233 of = open(out_name, "rb")
234 except IOError:
235 # No file - create new
236 of = open(out_name, "wb")
237 of.write(out)
238 of.close()
239 else:
240 # File exists - overwite only if content is different
241 inp = of.read()
242 of.close()
243 if inp != out:
244 of = open(out_name, "wb")
245 of.write(out)
246 of.close()
248 def sha1(file):
249 """Provides C source representation of sha1 sum of file"""
250 if file == None:
251 return ""
252 else:
253 sha1 = hashlib.sha1()
254 with open(file, 'rb') as f:
255 for chunk in iter(lambda: f.read(8192), ''):
256 sha1.update(chunk)
257 hex_stream = lambda s:",".join(['0x'+hex(ord(c))[2:].zfill(2) for c in s])
258 return hex_stream(sha1.digest())
260 def xtrim(string, suffix, length):
261 """Return string+suffix concatenated and trimmed up to length characters
263 This function appends suffix to the end of string and returns the result
264 up to length characters. If it does not fit then the string will be
265 truncated and the '+' will be put between it and the suffix.
267 if len(string) + len(suffix) <= length:
268 return ''.join([string, suffix])
269 else:
270 n = length - 1 - len(suffix)
271 assert n > 0, "length of truncated string+suffix exceeds maximum length"
272 return ''.join([string[:n], '+', suffix])
274 def get_hash_of_dirs(directory, verbose = 0, raw = 0, n = 40):
275 """Return hash of XML files from UAVObject definition directory"""
276 import hashlib, os
277 SHAhash = hashlib.sha1()
279 if not os.path.exists(directory):
280 return -1
282 try:
283 for root, dirs, files in os.walk(directory):
284 # os.walk() is unsorted. Must make sure we process files in sorted
285 # order so that the hash is stable across invocations and across OSes.
286 if files:
287 files.sort()
289 for names in files:
290 if verbose == 1:
291 print 'Hashing', names
292 filepath = os.path.join(root, names)
293 try:
294 f1 = open(filepath, 'rU')
295 except:
296 # You can't open the file for some reason
297 continue
299 # Compute file hash. Same as running "sha1sum <file>".
300 f1hash = hashlib.sha1()
301 while 1:
302 # Read file in as little chunks
303 buf = f1.read(4096)
304 if not buf:
305 break
306 f1hash.update(buf)
307 f1.close()
309 if verbose == 1:
310 print 'Hash is', f1hash.hexdigest()
312 # Append the hex representation of the current file's hash into the cumulative hash
313 SHAhash.update(f1hash.hexdigest())
315 except:
316 import traceback
317 # Print the stack traceback
318 traceback.print_exc()
319 return -2
321 if verbose == 1:
322 print 'Final hash is', SHAhash.hexdigest()
324 if raw == 1:
325 return SHAhash.hexdigest()[:n]
326 else:
327 hex_stream = lambda s:",".join(['0x'+hex(ord(c))[2:].zfill(2) for c in s])
328 return hex_stream(SHAhash.digest())
330 def main():
331 """This utility uses git repository in the current working directory
332 or from the given path to extract some info about it and HEAD commit.
333 Then some variables in the form of ${VARIABLE} could be replaced by
334 collected data. Optional board type, board revision and sha1 sum
335 of given image file could be applied as well or will be replaced by
336 empty strings if not defined.
338 If --info option is given, some repository info will be printed to
339 stdout.
341 If --format option is given then utility prints the format string
342 after substitution to the standard output.
344 If --outfile option is given then the --template option should be
345 defined too. In that case the utility reads a template file, performs
346 variable substitution and writes the result into output file. Output
347 file will be overwritten only if its content differs from expected.
348 Otherwise it will not be touched, so make utility will not remake
349 dependent targets.
351 Optional positional arguments may be used to add more dictionary
352 strings for replacement. Each argument has the form:
353 VARIABLE=replacement
354 and each ${VARIABLE} reference will be replaced with replacement
355 string given.
358 # Parse command line.
359 class RawDescriptionHelpFormatter(optparse.IndentedHelpFormatter):
360 """optparse formatter function to pretty print raw epilog"""
361 def format_epilog(self, epilog):
362 if epilog:
363 return "\n" + epilog + "\n"
364 else:
365 return ""
367 parser = optparse.OptionParser(
368 formatter=RawDescriptionHelpFormatter(),
369 description = "Performs variable substitution in template file or string.",
370 epilog = main.__doc__);
372 parser.add_option('--path', default='.',
373 help='path to the git repository');
374 parser.add_option('--info', action='store_true',
375 help='print repository info to stdout');
376 parser.add_option('--format',
377 help='format string to print to stdout');
378 parser.add_option('--template',
379 help='name of template file');
380 parser.add_option('--outfile',
381 help='name of output file');
382 parser.add_option('--image',
383 help='name of image file for sha1 calculation');
384 parser.add_option('--type', default="",
385 help='board type, for example, 0x04 for CopterControl');
386 parser.add_option('--revision', default = "",
387 help='board revision, for example, 0x01');
388 parser.add_option('--uavodir', default = "",
389 help='uav object definition directory');
390 (args, positional_args) = parser.parse_args()
392 # Process arguments. No advanced error handling is here.
393 # Any error will raise an exception and terminate process
394 # with non-zero exit code.
395 r = Repo(args.path)
397 dictionary = dict(
398 TEMPLATE = args.template,
399 OUTFILENAME = args.outfile,
400 ORIGIN = r.origin("local repository or using build servers"),
401 HASH = r.hash(),
402 HASH8 = r.hash(8),
403 TAG = r.tag(''),
404 TAG_OR_BRANCH = r.tag(r.branch('unreleased')),
405 TAG_OR_HASH8 = r.tag(r.hash(8, 'untagged')),
406 LABEL = r.label(),
407 REVISION = r.revision(),
408 DIRTY = r.dirty(),
409 FWTAG = xtrim(r.tag(r.branch('unreleased')), r.dirty(), 25),
410 UNIXTIME = r.time(),
411 DATE = r.time('%Y%m%d'),
412 DATETIME = r.time('%Y%m%d %H:%M'),
413 DAY = r.time('%d'),
414 MONTH = r.time('%m'),
415 YEAR = r.time('%Y'),
416 HOUR = r.time('%H'),
417 MINUTE = r.time('%M'),
418 BOARD_TYPE = args.type,
419 BOARD_REVISION = args.revision,
420 UAVO_HASH = get_hash_of_dirs(args.uavodir, verbose = 0, raw = 1),
421 UAVO_HASH8 = get_hash_of_dirs(args.uavodir, verbose = 0, raw = 1, n = 8),
422 UAVO_HASH_ARRAY = get_hash_of_dirs(args.uavodir, verbose = 0, raw = 0),
423 IMAGE_HASH_ARRAY = sha1(args.image),
426 # Process positional arguments in the form of:
427 # VAR1=str1 VAR2="string 2"
428 for var in positional_args:
429 (key, value) = var.split('=', 1)
430 dictionary[key] = value
432 if args.info:
433 r.info()
435 if args.format != None:
436 print Template(args.format).substitute(dictionary)
438 if args.outfile != None:
439 file_from_template(args.template, args.outfile, dictionary)
441 return 0
443 if __name__ == "__main__":
444 sys.exit(main())