summaryrefslogtreecommitdiffstats
path: root/scripts/combo-layer
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/combo-layer')
-rwxr-xr-xscripts/combo-layer600
1 files changed, 600 insertions, 0 deletions
diff --git a/scripts/combo-layer b/scripts/combo-layer
new file mode 100755
index 0000000000..19d64e64e1
--- /dev/null
+++ b/scripts/combo-layer
@@ -0,0 +1,600 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# Copyright 2011 Intel Corporation
6# Authored-by: Yu Ke <ke.yu@intel.com>
7# Paul Eggleton <paul.eggleton@intel.com>
8# Richard Purdie <richard.purdie@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import os, sys
24import optparse
25import logging
26import subprocess
27import ConfigParser
28import re
29
30__version__ = "0.2.1"
31
32def logger_create():
33 logger = logging.getLogger("")
34 loggerhandler = logging.StreamHandler()
35 loggerhandler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s","%H:%M:%S"))
36 logger.addHandler(loggerhandler)
37 logger.setLevel(logging.INFO)
38 return logger
39
40logger = logger_create()
41
42def get_current_branch(repodir=None):
43 try:
44 if not os.path.exists(os.path.join(repodir if repodir else '', ".git")):
45 # Repo not created yet (i.e. during init) so just assume master
46 return "master"
47 branchname = runcmd("git symbolic-ref HEAD 2>/dev/null", repodir).strip()
48 if branchname.startswith("refs/heads/"):
49 branchname = branchname[11:]
50 return branchname
51 except subprocess.CalledProcessError:
52 return ""
53
54class Configuration(object):
55 """
56 Manages the configuration
57
58 For an example config file, see combo-layer.conf.example
59
60 """
61 def __init__(self, options):
62 for key, val in options.__dict__.items():
63 setattr(self, key, val)
64
65 def readsection(parser, section, repo):
66 for (name, value) in parser.items(section):
67 if value.startswith("@"):
68 self.repos[repo][name] = eval(value.strip("@"))
69 else:
70 self.repos[repo][name] = value
71
72 logger.debug("Loading config file %s" % self.conffile)
73 self.parser = ConfigParser.ConfigParser()
74 with open(self.conffile) as f:
75 self.parser.readfp(f)
76
77 self.repos = {}
78 for repo in self.parser.sections():
79 self.repos[repo] = {}
80 readsection(self.parser, repo, repo)
81
82 # Load local configuration, if available
83 self.localconffile = None
84 self.localparser = None
85 self.combobranch = None
86 if self.conffile.endswith('.conf'):
87 lcfile = self.conffile.replace('.conf', '-local.conf')
88 if os.path.exists(lcfile):
89 # Read combo layer branch
90 self.combobranch = get_current_branch()
91 logger.debug("Combo layer branch is %s" % self.combobranch)
92
93 self.localconffile = lcfile
94 logger.debug("Loading local config file %s" % self.localconffile)
95 self.localparser = ConfigParser.ConfigParser()
96 with open(self.localconffile) as f:
97 self.localparser.readfp(f)
98
99 for section in self.localparser.sections():
100 if '|' in section:
101 sectionvals = section.split('|')
102 repo = sectionvals[0]
103 if sectionvals[1] != self.combobranch:
104 continue
105 else:
106 repo = section
107 if repo in self.repos:
108 readsection(self.localparser, section, repo)
109
110 def update(self, repo, option, value, initmode=False):
111 if self.localparser:
112 parser = self.localparser
113 section = "%s|%s" % (repo, self.combobranch)
114 conffile = self.localconffile
115 if initmode and not parser.has_section(section):
116 parser.add_section(section)
117 else:
118 parser = self.parser
119 section = repo
120 conffile = self.conffile
121 parser.set(section, option, value)
122 with open(conffile, "w") as f:
123 parser.write(f)
124
125 def sanity_check(self, initmode=False):
126 required_options=["src_uri", "local_repo_dir", "dest_dir", "last_revision"]
127 if initmode:
128 required_options.remove("last_revision")
129 msg = ""
130 missing_options = []
131 for name in self.repos:
132 for option in required_options:
133 if option not in self.repos[name]:
134 msg = "%s\nOption %s is not defined for component %s" %(msg, option, name)
135 missing_options.append(option)
136 if msg != "":
137 logger.error("configuration file %s has the following error: %s" % (self.conffile,msg))
138 if self.localconffile and 'last_revision' in missing_options:
139 logger.error("local configuration file %s may be missing configuration for combo branch %s" % (self.localconffile, self.combobranch))
140 sys.exit(1)
141
142 # filterdiff is required by action_splitpatch, so check its availability
143 if subprocess.call("which filterdiff > /dev/null 2>&1", shell=True) != 0:
144 logger.error("ERROR: patchutils package is missing, please install it (e.g. # apt-get install patchutils)")
145 sys.exit(1)
146
147def runcmd(cmd,destdir=None,printerr=True):
148 """
149 execute command, raise CalledProcessError if fail
150 return output if succeed
151 """
152 logger.debug("run cmd '%s' in %s" % (cmd, os.getcwd() if destdir is None else destdir))
153 out = os.tmpfile()
154 try:
155 subprocess.check_call(cmd, stdout=out, stderr=out, cwd=destdir, shell=True)
156 except subprocess.CalledProcessError,e:
157 out.seek(0)
158 if printerr:
159 logger.error("%s" % out.read())
160 raise e
161
162 out.seek(0)
163 output = out.read()
164 logger.debug("output: %s" % output )
165 return output
166
167def action_init(conf, args):
168 """
169 Clone component repositories
170 Check git is initialised; if not, copy initial data from component repos
171 """
172 for name in conf.repos:
173 ldir = conf.repos[name]['local_repo_dir']
174 if not os.path.exists(ldir):
175 logger.info("cloning %s to %s" %(conf.repos[name]['src_uri'], ldir))
176 subprocess.check_call("git clone %s %s" % (conf.repos[name]['src_uri'], ldir), shell=True)
177 if not os.path.exists(".git"):
178 runcmd("git init")
179 for name in conf.repos:
180 repo = conf.repos[name]
181 ldir = repo['local_repo_dir']
182 branch = repo.get('branch', "master")
183 lastrev = repo.get('last_revision', None)
184 if lastrev and lastrev != "HEAD":
185 initialrev = lastrev
186 if branch:
187 if not check_rev_branch(name, ldir, lastrev, branch):
188 sys.exit(1)
189 logger.info("Copying data from %s at specified revision %s..." % (name, lastrev))
190 else:
191 lastrev = None
192 initialrev = branch
193 logger.info("Copying data from %s..." % name)
194 dest_dir = repo['dest_dir']
195 if dest_dir and dest_dir != ".":
196 extract_dir = os.path.join(os.getcwd(), dest_dir)
197 if not os.path.exists(extract_dir):
198 os.makedirs(extract_dir)
199 else:
200 extract_dir = os.getcwd()
201 file_filter = repo.get('file_filter', "")
202 runcmd("git archive %s | tar -x -C %s %s" % (initialrev, extract_dir, file_filter), ldir)
203 if not lastrev:
204 lastrev = runcmd("git rev-parse %s" % initialrev, ldir).strip()
205 conf.update(name, "last_revision", lastrev, initmode=True)
206 runcmd("git add .")
207 if conf.localconffile:
208 localadded = True
209 try:
210 runcmd("git rm --cached %s" % conf.localconffile, printerr=False)
211 except subprocess.CalledProcessError:
212 localadded = False
213 if localadded:
214 localrelpath = os.path.relpath(conf.localconffile)
215 runcmd("grep -q %s .gitignore || echo %s >> .gitignore" % (localrelpath, localrelpath))
216 runcmd("git add .gitignore")
217 logger.info("Added local configuration file %s to .gitignore", localrelpath)
218 logger.info("Initial combo layer repository data has been created; please make any changes if desired and then use 'git commit' to make the initial commit.")
219 else:
220 logger.info("Repository already initialised, nothing to do.")
221
222
223def check_repo_clean(repodir):
224 """
225 check if the repo is clean
226 exit if repo is dirty
227 """
228 output=runcmd("git status --porcelain", repodir)
229 r = re.compile('\?\? patch-.*/')
230 dirtyout = [item for item in output.splitlines() if not r.match(item)]
231 if dirtyout:
232 logger.error("git repo %s is dirty, please fix it first", repodir)
233 sys.exit(1)
234
235def check_patch(patchfile):
236 f = open(patchfile)
237 ln = f.readline()
238 of = None
239 in_patch = False
240 beyond_msg = False
241 pre_buf = ''
242 while ln:
243 if not beyond_msg:
244 if ln == '---\n':
245 if not of:
246 break
247 in_patch = False
248 beyond_msg = True
249 elif ln.startswith('--- '):
250 # We have a diff in the commit message
251 in_patch = True
252 if not of:
253 print('WARNING: %s contains a diff in its commit message, indenting to avoid failure during apply' % patchfile)
254 of = open(patchfile + '.tmp', 'w')
255 of.write(pre_buf)
256 pre_buf = ''
257 elif in_patch and not ln[0] in '+-@ \n\r':
258 in_patch = False
259 if of:
260 if in_patch:
261 of.write(' ' + ln)
262 else:
263 of.write(ln)
264 else:
265 pre_buf += ln
266 ln = f.readline()
267 f.close()
268 if of:
269 of.close()
270 os.rename(patchfile + '.tmp', patchfile)
271
272def drop_to_shell(workdir=None):
273 shell = os.environ.get('SHELL', 'bash')
274 print('Dropping to shell "%s"\n' \
275 'When you are finished, run the following to continue:\n' \
276 ' exit -- continue to apply the patches\n' \
277 ' exit 1 -- abort\n' % shell);
278 ret = subprocess.call([shell], cwd=workdir)
279 if ret != 0:
280 print "Aborting"
281 return False
282 else:
283 return True
284
285def check_rev_branch(component, repodir, rev, branch):
286 try:
287 actualbranch = runcmd("git branch --contains %s" % rev, repodir, printerr=False)
288 except subprocess.CalledProcessError as e:
289 if e.returncode == 129:
290 actualbranch = ""
291 else:
292 raise
293
294 if not actualbranch:
295 logger.error("%s: specified revision %s is invalid!" % (component, rev))
296 return False
297
298 branches = []
299 branchlist = actualbranch.split("\n")
300 for b in branchlist:
301 branches.append(b.strip().split(' ')[-1])
302
303 if branch not in branches:
304 logger.error("%s: specified revision %s is not on specified branch %s!" % (component, rev, branch))
305 return False
306 return True
307
308def get_repos(conf, args):
309 repos = []
310 if len(args) > 1:
311 for arg in args[1:]:
312 if arg.startswith('-'):
313 break
314 else:
315 repos.append(arg)
316 for repo in repos:
317 if not repo in conf.repos:
318 logger.error("Specified component '%s' not found in configuration" % repo)
319 sys.exit(0)
320
321 if not repos:
322 repos = conf.repos
323
324 return repos
325
326def action_pull(conf, args):
327 """
328 update the component repos only
329 """
330 repos = get_repos(conf, args)
331
332 # make sure all repos are clean
333 for name in repos:
334 check_repo_clean(conf.repos[name]['local_repo_dir'])
335
336 for name in repos:
337 repo = conf.repos[name]
338 ldir = repo['local_repo_dir']
339 branch = repo.get('branch', "master")
340 runcmd("git checkout %s" % branch, ldir)
341 logger.info("git pull for component repo %s in %s ..." % (name, ldir))
342 output=runcmd("git pull", ldir)
343 logger.info(output)
344
345def action_update(conf, args):
346 """
347 update the component repos
348 generate the patch list
349 apply the generated patches
350 """
351 repos = get_repos(conf, args)
352
353 # make sure combo repo is clean
354 check_repo_clean(os.getcwd())
355
356 import uuid
357 patch_dir = "patch-%s" % uuid.uuid4()
358 if not os.path.exists(patch_dir):
359 os.mkdir(patch_dir)
360
361 # Step 1: update the component repos
362 if conf.nopull:
363 logger.info("Skipping pull (-n)")
364 else:
365 action_pull(conf, args)
366
367 for name in repos:
368 repo = conf.repos[name]
369 ldir = repo['local_repo_dir']
370 dest_dir = repo['dest_dir']
371 branch = repo.get('branch', "master")
372 repo_patch_dir = os.path.join(os.getcwd(), patch_dir, name)
373
374 # Step 2: generate the patch list and store to patch dir
375 logger.info("Generating patches from %s..." % name)
376 if dest_dir != ".":
377 prefix = "--src-prefix=a/%s/ --dst-prefix=b/%s/" % (dest_dir, dest_dir)
378 else:
379 prefix = ""
380 if repo['last_revision'] == "":
381 logger.info("Warning: last_revision of component %s is not set, starting from the first commit" % name)
382 patch_cmd_range = "--root %s" % branch
383 rev_cmd_range = branch
384 else:
385 if not check_rev_branch(name, ldir, repo['last_revision'], branch):
386 sys.exit(1)
387 patch_cmd_range = "%s..%s" % (repo['last_revision'], branch)
388 rev_cmd_range = patch_cmd_range
389
390 file_filter = repo.get('file_filter',"")
391
392 patch_cmd = "git format-patch -N %s --output-directory %s %s -- %s" % \
393 (prefix,repo_patch_dir, patch_cmd_range, file_filter)
394 output = runcmd(patch_cmd, ldir)
395 logger.debug("generated patch set:\n%s" % output)
396 patchlist = output.splitlines()
397
398 rev_cmd = "git rev-list --no-merges %s -- %s" % (rev_cmd_range, file_filter)
399 revlist = runcmd(rev_cmd, ldir).splitlines()
400
401 # Step 3: Call repo specific hook to adjust patch
402 if 'hook' in repo:
403 # hook parameter is: ./hook patchpath revision reponame
404 count=len(revlist)-1
405 for patch in patchlist:
406 runcmd("%s %s %s %s" % (repo['hook'], patch, revlist[count], name))
407 count=count-1
408
409 # Step 4: write patch list and revision list to file, for user to edit later
410 patchlist_file = os.path.join(os.getcwd(), patch_dir, "patchlist-%s" % name)
411 repo['patchlist'] = patchlist_file
412 f = open(patchlist_file, 'w')
413 count=len(revlist)-1
414 for patch in patchlist:
415 f.write("%s %s\n" % (patch, revlist[count]))
416 check_patch(os.path.join(patch_dir, patch))
417 count=count-1
418 f.close()
419
420 # Step 5: invoke bash for user to edit patch and patch list
421 if conf.interactive:
422 print('You may now edit the patch and patch list in %s\n' \
423 'For example, you can remove unwanted patch entries from patchlist-*, so that they will be not applied later' % patch_dir);
424 if not drop_to_shell(patch_dir):
425 sys.exit(0)
426
427 # Step 6: apply the generated and revised patch
428 apply_patchlist(conf, repos)
429 runcmd("rm -rf %s" % patch_dir)
430
431 # Step 7: commit the updated config file if it's being tracked
432 relpath = os.path.relpath(conf.conffile)
433 try:
434 output = runcmd("git status --porcelain %s" % relpath, printerr=False)
435 except:
436 # Outside the repository
437 output = None
438 if output:
439 logger.info("Committing updated configuration file")
440 if output.lstrip().startswith("M"):
441 runcmd('git commit -m "Automatic commit to update last_revision" %s' % relpath)
442
443def apply_patchlist(conf, repos):
444 """
445 apply the generated patch list to combo repo
446 """
447 for name in repos:
448 repo = conf.repos[name]
449 lastrev = repo["last_revision"]
450 prevrev = lastrev
451
452 # Get non-blank lines from patch list file
453 patchlist = []
454 if os.path.exists(repo['patchlist']) or not conf.interactive:
455 # Note: we want this to fail here if the file doesn't exist and we're not in
456 # interactive mode since the file should exist in this case
457 with open(repo['patchlist']) as f:
458 for line in f:
459 line = line.rstrip()
460 if line:
461 patchlist.append(line)
462
463 if patchlist:
464 logger.info("Applying patches from %s..." % name)
465 linecount = len(patchlist)
466 i = 1
467 for line in patchlist:
468 patchfile = line.split()[0]
469 lastrev = line.split()[1]
470 patchdisp = os.path.relpath(patchfile)
471 if os.path.getsize(patchfile) == 0:
472 logger.info("(skipping %d/%d %s - no changes)" % (i, linecount, patchdisp))
473 else:
474 cmd = "git am --keep-cr -s -p1 %s" % patchfile
475 logger.info("Applying %d/%d: %s" % (i, linecount, patchdisp))
476 try:
477 runcmd(cmd)
478 except subprocess.CalledProcessError:
479 logger.info('Running "git am --abort" to cleanup repo')
480 runcmd("git am --abort")
481 logger.error('"%s" failed' % cmd)
482 logger.info("Please manually apply patch %s" % patchdisp)
483 logger.info("Note: if you exit and continue applying without manually applying the patch, it will be skipped")
484 if not drop_to_shell():
485 if prevrev != repo['last_revision']:
486 conf.update(name, "last_revision", prevrev)
487 sys.exit(0)
488 prevrev = lastrev
489 i += 1
490 else:
491 logger.info("No patches to apply from %s" % name)
492 ldir = conf.repos[name]['local_repo_dir']
493 branch = conf.repos[name].get('branch', "master")
494 lastrev = runcmd("git rev-parse %s" % branch, ldir).strip()
495
496 if lastrev != repo['last_revision']:
497 conf.update(name, "last_revision", lastrev)
498
499def action_splitpatch(conf, args):
500 """
501 generate the commit patch and
502 split the patch per repo
503 """
504 logger.debug("action_splitpatch")
505 if len(args) > 1:
506 commit = args[1]
507 else:
508 commit = "HEAD"
509 patchdir = "splitpatch-%s" % commit
510 if not os.path.exists(patchdir):
511 os.mkdir(patchdir)
512
513 # filerange_root is for the repo whose dest_dir is root "."
514 # and it should be specified by excluding all other repo dest dir
515 # like "-x repo1 -x repo2 -x repo3 ..."
516 filerange_root = ""
517 for name in conf.repos:
518 dest_dir = conf.repos[name]['dest_dir']
519 if dest_dir != ".":
520 filerange_root = '%s -x "%s/*"' % (filerange_root, dest_dir)
521
522 for name in conf.repos:
523 dest_dir = conf.repos[name]['dest_dir']
524 patch_filename = "%s/%s.patch" % (patchdir, name)
525 if dest_dir == ".":
526 cmd = "git format-patch -n1 --stdout %s^..%s | filterdiff -p1 %s > %s" % (commit, commit, filerange_root, patch_filename)
527 else:
528 cmd = "git format-patch --no-prefix -n1 --stdout %s^..%s -- %s > %s" % (commit, commit, dest_dir, patch_filename)
529 runcmd(cmd)
530 # Detect empty patches (including those produced by filterdiff above
531 # that contain only preamble text)
532 if os.path.getsize(patch_filename) == 0 or runcmd("filterdiff %s" % patch_filename) == "":
533 os.remove(patch_filename)
534 logger.info("(skipping %s - no changes)", name)
535 else:
536 logger.info(patch_filename)
537
538def action_error(conf, args):
539 logger.info("invalid action %s" % args[0])
540
541actions = {
542 "init": action_init,
543 "update": action_update,
544 "pull": action_pull,
545 "splitpatch": action_splitpatch,
546}
547
548def main():
549 parser = optparse.OptionParser(
550 version = "Combo Layer Repo Tool version %s" % __version__,
551 usage = """%prog [options] action
552
553Create and update a combination layer repository from multiple component repositories.
554
555Action:
556 init initialise the combo layer repo
557 update [components] get patches from component repos and apply them to the combo repo
558 pull [components] just pull component repos only
559 splitpatch [commit] generate commit patch and split per component, default commit is HEAD""")
560
561 parser.add_option("-c", "--conf", help = "specify the config file (conf/combo-layer.conf is the default).",
562 action = "store", dest = "conffile", default = "conf/combo-layer.conf")
563
564 parser.add_option("-i", "--interactive", help = "interactive mode, user can edit the patch list and patches",
565 action = "store_true", dest = "interactive", default = False)
566
567 parser.add_option("-D", "--debug", help = "output debug information",
568 action = "store_true", dest = "debug", default = False)
569
570 parser.add_option("-n", "--no-pull", help = "skip pulling component repos during update",
571 action = "store_true", dest = "nopull", default = False)
572
573 options, args = parser.parse_args(sys.argv)
574
575 # Dispatch to action handler
576 if len(args) == 1:
577 logger.error("No action specified, exiting")
578 parser.print_help()
579 elif args[1] not in actions:
580 logger.error("Unsupported action %s, exiting\n" % (args[1]))
581 parser.print_help()
582 elif not os.path.exists(options.conffile):
583 logger.error("No valid config file, exiting\n")
584 parser.print_help()
585 else:
586 if options.debug:
587 logger.setLevel(logging.DEBUG)
588 confdata = Configuration(options)
589 initmode = (args[1] == 'init')
590 confdata.sanity_check(initmode)
591 actions.get(args[1], action_error)(confdata, args[1:])
592
593if __name__ == "__main__":
594 try:
595 ret = main()
596 except Exception:
597 ret = 1
598 import traceback
599 traceback.print_exc(5)
600 sys.exit(ret)