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
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
25 http://packages.python.org/GitPython
26 It is not installed by default, so we cannot rely on it.
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()
42 """Execute git using cmd as arguments"""
44 git
= Popen(self
._git
+ " " + cmd
, cwd
= self
._path
,
45 shell
= True, stdout
= PIPE
, stderr
= PIPE
)
46 self
._out
, self
._err
= git
.communicate()
49 def _get_origin(self
):
50 """Get and store the repository fetch origin path"""
52 self
._exec
('remote -v')
54 m
= search(r
"^origin\s+(.+)\s+\(fetch\)$", self
._out
, MULTILINE
)
56 self
._origin
= m
.group(1)
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.
65 self
._exec
('log -n1 --no-color --format=format:%ct HEAD')
67 self
._time
= self
._out
70 """Get and store git tag for the HEAD commit"""
72 self
._exec
('describe --tags --exact-match HEAD')
74 self
._tag
= self
._out
.strip(' \t\n\r')
76 def _get_branch(self
):
77 """Get and store current branch containing the HEAD commit"""
79 self
._exec
('branch --contains HEAD')
81 m
= search(r
"^\*\s+(.+)$", self
._out
, MULTILINE
)
83 self
._branch
= m
.group(1)
86 """Check for dirty state of repository"""
88 self
._exec
('update-index --refresh --unmerged')
89 self
._exec
('diff-index --name-only --exit-code --quiet HEAD')
93 def __init__(self
, path
= "."):
94 """Initialize object instance and read repo info"""
96 self
._exec
('rev-parse --verify HEAD')
98 self
._hash
= self
._out
.strip(' \t\n\r')
113 """Return the repository path"""
116 def origin(self
, none
= None):
117 """Return fetch origin of the repository"""
118 if self
._origin
== None:
123 def hash(self
, n
= 40, none
= None):
124 """Return hash of the HEAD commit"""
125 if self
._hash
== None:
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:
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:
147 def branch(self
, none
= None):
148 """Return git branch containing the HEAD or given string if none"""
149 if self
._branch
== None:
154 def dirty(self
, dirty
= "-dirty", clean
= ""):
155 """Return git repository dirty state or empty string"""
162 """Return package label (tag if defined, or date-hash if no tag)"""
164 if self
._tag
== None:
165 return ''.join([self
.time('%Y%m%d'), "-", self
.hash(8, 'untagged'), self
.dirty()])
167 return ''.join([self
.tag(''), self
.dirty()])
172 """Return full revison string (tag if defined, or branch:hash date time if no tag)"""
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')])
177 return ''.join([self
.tag(''), self
.dirty()])
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.
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"
217 OUTFILENAME = out_name,
220 file_from_template(tpl_name, out_name, dictionary)
223 # Read template first
224 tf
= open(tpl_name
, "rb")
228 # Replace placeholders using dictionary
229 out
= Template(tpl
).substitute(dict)
231 # Check if output file already exists
233 of
= open(out_name
, "rb")
235 # No file - create new
236 of
= open(out_name
, "wb")
240 # File exists - overwite only if content is different
244 of
= open(out_name
, "wb")
249 """Provides C source representation of sha1 sum of file"""
253 sha1
= hashlib
.sha1()
254 with
open(file, 'rb') as f
:
255 for chunk
in iter(lambda: f
.read(8192), ''):
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
])
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"""
277 SHAhash
= hashlib
.sha1()
279 if not os
.path
.exists(directory
):
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.
291 print 'Hashing', names
292 filepath
= os
.path
.join(root
, names
)
294 f1
= open(filepath
, 'rU')
296 # You can't open the file for some reason
299 # Compute file hash. Same as running "sha1sum <file>".
300 f1hash
= hashlib
.sha1()
302 # Read file in as little chunks
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())
317 # Print the stack traceback
318 traceback
.print_exc()
322 print 'Final hash is', SHAhash
.hexdigest()
325 return SHAhash
.hexdigest()[:n
]
327 hex_stream
= lambda s
:",".join(['0x'+hex(ord(c
))[2:].zfill(2) for c
in s
])
328 return hex_stream(SHAhash
.digest())
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
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
351 Optional positional arguments may be used to add more dictionary
352 strings for replacement. Each argument has the form:
354 and each ${VARIABLE} reference will be replaced with replacement
358 # Parse command line.
359 class RawDescriptionHelpFormatter(optparse
.IndentedHelpFormatter
):
360 """optparse formatter function to pretty print raw epilog"""
361 def format_epilog(self
, epilog
):
363 return "\n" + epilog
+ "\n"
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.
398 TEMPLATE
= args
.template
,
399 OUTFILENAME
= args
.outfile
,
400 ORIGIN
= r
.origin("local repository or using build servers"),
404 TAG_OR_BRANCH
= r
.tag(r
.branch('unreleased')),
405 TAG_OR_HASH8
= r
.tag(r
.hash(8, 'untagged')),
407 REVISION
= r
.revision(),
409 FWTAG
= xtrim(r
.tag(r
.branch('unreleased')), r
.dirty(), 25),
411 DATE
= r
.time('%Y%m%d'),
412 DATETIME
= r
.time('%Y%m%d %H:%M'),
414 MONTH
= r
.time('%m'),
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
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
)
443 if __name__
== "__main__":