3 # Utility functions to access git repository info and
4 # generate source files and binary objects using templates.
6 # (C) 2015, The LibrePilot Project, http://www.librepilot.org
7 # (c) 2011, The OpenPilot Team, http://www.openpilot.org
8 # See also: The GNU Public License (GPL) Version 3
11 from subprocess
import Popen
, PIPE
12 from re
import search
, MULTILINE
13 from datetime
import datetime
14 from string
import Template
22 """A simple git repository HEAD commit info class
24 This simple class provides object notation to access
25 the git repository current commit info. If one needs
26 better access one can try the GitPython class available
28 http://packages.python.org/GitPython
29 It is not installed by default, so we cannot rely on it.
32 r = Repo('/path/to/git/repository')
33 print "path: ", r.path()
34 print "origin: ", r.origin()
35 print "hash: ", r.hash()
36 print "short hash: ", r.hash(8)
37 print "Unix time: ", r.time()
38 print "commit date:", r.time("%Y%m%d")
39 print "commit tag: ", r.tag()
40 print "branch: ", r.branch()
41 print "release tag:", r.reltag()
45 """Execute git using cmd as arguments"""
47 git
= Popen(self
._git
+ " " + cmd
, cwd
= self
._path
,
48 shell
= True, stdout
= PIPE
, stderr
= PIPE
)
49 self
._out
, self
._err
= git
.communicate()
52 def _get_origin(self
):
53 """Get and store the repository fetch origin path"""
55 self
._exec
('remote -v')
57 m
= search(r
"^origin\s+(.+)\s+\(fetch\)$", self
._out
, MULTILINE
)
59 self
._origin
= m
.group(1)
62 """Get and store HEAD commit timestamp in Unix format
64 We use commit timestamp rather than the build time,
65 so it always is the same for the current commit or tag.
68 self
._exec
('log -n1 --no-color --format=format:%ct HEAD')
70 self
._time
= self
._out
72 def _get_last_tag(self
):
73 """Get and store git tag for the HEAD commit"""
75 self
._num
_commits
_past
_tag
= None
76 self
._exec
('describe --tags --long')
78 descriptions
= self
._out
.rsplit('-', 2)
79 self
._last
_tag
= descriptions
[-3]
80 self
._num
_commits
_past
_tag
= descriptions
[-2]
82 def _get_branch(self
):
83 """Get and store current branch containing the HEAD commit"""
85 self
._exec
('branch --contains HEAD')
87 m
= search(r
"^\*\s+(.+)$", self
._out
, MULTILINE
)
89 self
._branch
= m
.group(1)
92 """Check for dirty state of repository"""
94 self
._exec
('update-index --refresh --unmerged')
95 self
._exec
('diff-index --name-only --exit-code --quiet HEAD')
100 """Loads the repo data from version-info.json"""
101 json_path
= os
.path
.join(self
._path
, 'version-info.json')
102 if os
.path
.isfile(json_path
):
103 with
open(json_path
) as json_file
:
104 json_data
= json
.load(json_file
)
106 self
._hash
= json_data
['hash']
107 self
._origin
= json_data
['origin']
108 self
._time
= json_data
['time']
109 self
._last
_tag
= json_data
['last_tag']
110 self
._num
_commits
_past
_tag
= json_data
['num_commits_past_tag']
111 self
._branch
= json_data
['branch']
112 self
._dirty
= json_data
['dirty']
117 def __init__(self
, path
= "."):
118 """Initialize object instance and read repo info"""
120 self
._exec
('rev-parse --verify HEAD')
121 if self
._load
_json
():
124 self
._hash
= self
._out
.strip(' \t\n\r')
134 self
._last
_tag
= None
135 self
._num
_commits
_past
_tag
= None
140 """Return the repository path"""
143 def origin(self
, none
= None):
144 """Return fetch origin of the repository"""
145 if self
._origin
== None:
150 def hash(self
, n
= 40, none
= None):
151 """Return hash of the HEAD commit"""
152 if self
._hash
== None:
155 return self
._hash
[:n
]
157 def time(self
, format
= None, none
= None):
158 """Return Unix or formatted time of the HEAD commit"""
159 if self
._time
== None:
165 return datetime
.utcfromtimestamp(float(self
._time
)).strftime(format
)
167 def tag(self
, none
= None):
168 """Return git tag for the HEAD commit or given string if none"""
169 if self
._last
_tag
== None or self
._num
_commits
_past
_tag
!= "0":
172 return self
._last
_tag
174 def branch(self
, none
= None):
175 """Return git branch containing the HEAD or given string if none"""
176 if self
._branch
== None:
181 def dirty(self
, dirty
= "-dirty", clean
= ""):
182 """Return git repository dirty state or empty string"""
189 """Return package label (similar to git describe)"""
191 if self
._num
_commits
_past
_tag
== "0":
192 return self
._last
_tag
+ self
.dirty()
194 return self
._last
_tag
+ "+r" + self
._num
_commits
_past
_tag
+ "-g" + self
.hash(7, '') + self
.dirty()
198 def version_four_num(self
):
199 """Return package version in format X.X.X.X using only numbers"""
202 (release
, junk
, candidate
) = self
._last
_tag
.partition("-RC")
203 (year
, dot
, month_and_patch
) = release
.partition(".")
204 (month
, dot
, patch
) = month_and_patch
.partition(".")
207 candidate
= "64" # Need to stay below 65536 for last part
212 return "{}.{}.{}.{}{:0>3.3}".format(year
,month
,patch
,candidate
,self
._num
_commits
_past
_tag
)
218 """Return full revison string (tag if defined, or branch:hash date time if no tag)"""
220 if self
._num
_commits
_past
_tag
== "0":
221 return self
.tag('') + self
.dirty()
223 return self
.branch('no-branch') + ":" + self
.hash(8, 'no-hash') + self
.dirty() + self
.time(' %Y%m%d %H:%M')
228 """Print some repository info"""
229 print "path: ", self
.path()
230 print "origin: ", self
.origin()
231 print "Unix time: ", self
.time()
232 print "commit date:", self
.time("%Y%m%d")
233 print "hash: ", self
.hash()
234 print "short hash: ", self
.hash(8)
235 print "branch: ", self
.branch()
236 print "commit tag: ", self
.tag('')
237 print "dirty: ", self
.dirty('yes', 'no')
238 print "label: ", self
.label()
239 print "revision: ", self
.revision()
241 def save_to_json(self
, path
):
242 """Saves the repo data to version-info.json"""
245 json_data
['hash'] = self
._hash
246 json_data
['origin'] = self
._origin
247 json_data
['time'] = self
._time
248 json_data
['last_tag'] = self
._last
_tag
249 json_data
['num_commits_past_tag'] = self
._num
_commits
_past
_tag
250 json_data
['branch'] = self
._branch
251 # version-info.json is for use with git archive which doesn't take in dirty changes
252 json_data
['dirty'] = False
254 json_path
= os
.path
.join(path
, 'version-info.json')
256 write_if_different(json_path
, json
.dumps(json_data
))
259 def write_if_different(out_name
, out
):
260 """Write ouput to file only if it differs from current"""
262 # Check if output file already exists
264 of
= open(out_name
, "rb")
266 # No file - create new
267 of
= open(out_name
, "wb")
271 # File exists - overwite only if content is different
275 of
= open(out_name
, "wb")
279 def escape_dict(dictionary
):
280 """Escapes dictionary values for C"""
282 # We need to escape the strings for C
283 for key
in dictionary
:
284 # Using json.dumps and removing the surounding quotes escapes for C
285 dictionary
[key
] = json
.dumps(dictionary
[key
])[1:-1]
288 def file_from_template(tpl_name
, out_name
, dictionary
):
289 """Create or update file from template using dictionary
291 This function reads the template, performs placeholder replacement
292 using the dictionary and checks if output file with such content
293 already exists. If no such file or file data is different from
294 expected then it will be ovewritten with new data. Otherwise it
295 will not be updated so make will not update dependent targets.
299 # char source[] = "${OUTFILENAME}";
300 # uint32_t timestamp = ${UNIXTIME};
301 # uint32_t hash = 0x${HASH8};
303 r = Repo('/path/to/git/repository')
304 tpl_name = "template.c"
305 out_name = "output.c"
310 OUTFILENAME = out_name,
313 file_from_template(tpl_name, out_name, dictionary)
316 # Read template first
317 tf
= open(tpl_name
, "rb")
321 # Replace placeholders using dictionary
322 out
= Template(tpl
).substitute(dictionary
)
324 write_if_different(out_name
, out
)
328 """Provides C source representation of sha1 sum of file"""
332 sha1
= hashlib
.sha1()
333 with
open(file, 'rb') as f
:
334 for chunk
in iter(lambda: f
.read(8192), ''):
336 hex_stream
= lambda s
:",".join(['0x'+hex(ord(c
))[2:].zfill(2) for c
in s
])
337 return hex_stream(sha1
.digest())
339 def xtrim(string
, suffix
, length
):
340 """Return string+suffix concatenated and trimmed up to length characters
342 This function appends suffix to the end of string and returns the result
343 up to length characters. If it does not fit then the string will be
344 truncated and the '+' will be put between it and the suffix.
346 if len(string
) + len(suffix
) <= length
:
347 return ''.join([string
, suffix
])
349 n
= length
- 1 - len(suffix
)
350 assert n
> 0, "length of truncated string+suffix exceeds maximum length"
351 return ''.join([string
[:n
], '+', suffix
])
353 def get_hash_of_dirs(directory
, verbose
= 0, raw
= 0, n
= 40):
354 """Return hash of XML files from UAVObject definition directory"""
356 SHAhash
= hashlib
.sha1()
358 if not os
.path
.exists(directory
):
362 for root
, dirs
, files
in os
.walk(directory
):
363 # os.walk() is unsorted. Must make sure we process files in sorted
364 # order so that the hash is stable across invocations and across OSes.
369 if names
.endswith('.xml'):
371 print 'Hashing', names
372 filepath
= os
.path
.join(root
, names
)
374 f1
= open(filepath
, 'rU')
376 # You can't open the file for some reason
379 # Compute file hash. Same as running "sha1sum <file>".
380 f1hash
= hashlib
.sha1()
382 # Read file in as little chunks
390 print 'Hash is', f1hash
.hexdigest()
392 # Append the hex representation of the current file's hash into the cumulative hash
393 SHAhash
.update(f1hash
.hexdigest())
397 # Print the stack traceback
398 traceback
.print_exc()
402 print 'Final hash is', SHAhash
.hexdigest()
405 return SHAhash
.hexdigest()[:n
]
407 hex_stream
= lambda s
:",".join(['0x'+hex(ord(c
))[2:].zfill(2) for c
in s
])
408 return hex_stream(SHAhash
.digest())
411 """This utility uses git repository in the current working directory
412 or from the given path to extract some info about it and HEAD commit.
413 Then some variables in the form of ${VARIABLE} could be replaced by
414 collected data. Optional board type, board revision and sha1 sum
415 of given image file could be applied as well or will be replaced by
416 empty strings if not defined.
418 If --info option is given, some repository info will be printed to
421 If --format option is given then utility prints the format string
422 after substitution to the standard output.
424 If --outfile option is given then the --template option should be
425 defined too. In that case the utility reads a template file, performs
426 variable substitution and writes the result into output file. Output
427 file will be overwritten only if its content differs from expected.
428 Otherwise it will not be touched, so make utility will not remake
431 Optional positional arguments may be used to add more dictionary
432 strings for replacement. Each argument has the form:
434 and each ${VARIABLE} reference will be replaced with replacement
438 # Parse command line.
439 class RawDescriptionHelpFormatter(optparse
.IndentedHelpFormatter
):
440 """optparse formatter function to pretty print raw epilog"""
441 def format_epilog(self
, epilog
):
443 return "\n" + epilog
+ "\n"
447 parser
= optparse
.OptionParser(
448 formatter
=RawDescriptionHelpFormatter(),
449 description
= "Performs variable substitution in template file or string.",
450 epilog
= main
.__doc
__);
452 parser
.add_option('--path', default
='.',
453 help='path to the git repository');
454 parser
.add_option('--info', action
='store_true',
455 help='print repository info to stdout');
456 parser
.add_option('--format',
457 help='format string to print to stdout');
458 parser
.add_option('--template',
459 help='name of template file');
460 parser
.add_option('--outfile',
461 help='name of output file');
462 parser
.add_option('--escape', action
="store_true",
463 help='do escape strings for C (default based on file ext)');
464 parser
.add_option('--no-escape', action
="store_false", dest
="escape",
465 help='do not escape strings for C');
466 parser
.add_option('--image',
467 help='name of image file for sha1 calculation');
468 parser
.add_option('--type', default
="",
469 help='board type, for example, 0x04 for CopterControl');
470 parser
.add_option('--revision', default
= "",
471 help='board revision, for example, 0x01');
472 parser
.add_option('--uavodir', default
= "",
473 help='uav object definition directory');
474 parser
.add_option('--jsonpath',
475 help='path to save version info');
476 (args
, positional_args
) = parser
.parse_args()
478 # Process arguments. No advanced error handling is here.
479 # Any error will raise an exception and terminate process
480 # with non-zero exit code.
484 TEMPLATE
= args
.template
,
485 OUTFILENAME
= args
.outfile
,
486 ORIGIN
= r
.origin("local repository or using build servers"),
490 TAG_OR_BRANCH
= r
.tag(r
.branch('unreleased')),
491 TAG_OR_HASH8
= r
.tag(r
.hash(8, 'untagged')),
493 VERSION_FOUR_NUM
= r
.version_four_num(),
494 REVISION
= r
.revision(),
496 FWTAG
= xtrim(r
.tag(r
.branch('unreleased')), r
.dirty(), 25),
498 DATE
= r
.time('%Y%m%d'),
499 DATETIME
= r
.time('%Y%m%d %H:%M'),
501 MONTH
= r
.time('%m'),
504 MINUTE
= r
.time('%M'),
505 BOARD_TYPE
= args
.type,
506 BOARD_REVISION
= args
.revision
,
507 UAVO_HASH
= get_hash_of_dirs(args
.uavodir
, verbose
= 0, raw
= 1),
508 UAVO_HASH8
= get_hash_of_dirs(args
.uavodir
, verbose
= 0, raw
= 1, n
= 8),
509 UAVO_HASH_ARRAY
= get_hash_of_dirs(args
.uavodir
, verbose
= 0, raw
= 0),
510 IMAGE_HASH_ARRAY
= sha1(args
.image
),
513 # Process positional arguments in the form of:
514 # VAR1=str1 VAR2="string 2"
515 for var
in positional_args
:
516 (key
, value
) = var
.split('=', 1)
517 dictionary
[key
] = value
522 files_to_escape
= ['.c', '.cpp']
524 if (args
.escape
== None and args
.outfile
!= None and
525 os
.path
.splitext(args
.outfile
)[1] in files_to_escape
) or args
.escape
:
526 escape_dict(dictionary
)
528 if args
.format
!= None:
529 print Template(args
.format
).substitute(dictionary
)
531 if args
.outfile
!= None:
532 file_from_template(args
.template
, args
.outfile
, dictionary
)
534 if args
.jsonpath
!= None:
535 r
.save_to_json(args
.jsonpath
)
539 if __name__
== "__main__":