'update' and 'update-rebase' commands
[matrix.git] / matrix / matrix.py
1 # Copyright (C) 2006-2008 Movial Oy
2 # Authors: Timo Savola <tsavola@movial.fi>
3 #          Toni Timonen
4 #          Kalle Vahlman <kalle.vahlman@movial.fi>
5 #          Tuomas Kulve <tuomas.kulve@movial.fi>
6
7 import optparse
8 import os
9 import sys
10 from sets import Set as set
11
12 import cache
13 import components
14 import git
15 import log
16 from config import config
17 from config import parse as config_parse
18 from build import build, build_only
19
20 Error = RuntimeError
21
22 def merge_config(name, value):
23         if isinstance(value, list):
24                 value = getattr(config, name, []) + value
25         setattr(config, name, value)
26
27 def main():
28         command, targets, options = parse_args()
29
30         for name in ('debug', 'initial_config_dirs'):
31                 if name in options:
32                         merge_config(name, options[name])
33
34         config_parse('main', True)
35
36         for name in options:
37                 merge_config(name, options[name])
38
39         cache.init()
40         all_targets = components.init()
41
42         for i in xrange(len(targets)):
43                 name = targets[i]
44                 if name.startswith('src' + os.path.sep):
45                         name = name.split(os.path.sep)[1]
46                         targets[i] = name
47
48                 if name not in config.components:
49                         raise Error('Component "%s" does not exist' % name)
50
51         if not targets:
52                 targets = all_targets
53
54         command_funcs = {
55                 'install':      build,
56                 'install-only': build_only,
57                 'meta':         meta,
58                 'clone':        clone,
59                 'clean':        clean,
60                 'update':       update,
61                 'rebase':       rebase,
62                 'update-rebase':update_rebase,
63                 'pull':         pull,
64                 'changes':      changes,
65                 'source-dist':  source_dist,
66         }
67
68         func = command_funcs.get(command)
69         if not func:
70                 raise Error('Invalid command: "%s"' % command)
71
72         func(targets)
73
74 def print_help(file, parser):
75         parser.print_help(file=file)
76         print >>file, '''
77 commands:
78   meta          download the components' meta repositories
79   clone         download the components' git repositories
80   install       download, build and install the components
81   install-only  download, build and install the specified components without
82                 dependencies
83   clean         remove all non-tracked files from the component git repository
84                 directories
85   update        download remote repositories from server
86   rebase        rebase local repositories against remote repositories (remotes
87                 won't be updated first)
88   update-rebase update remote repositories and rebase local repositories
89   pull          update remote repositories and merge with local repositories
90   changes       show commits which are not on the server
91   source-dist   download and package the component sources
92 '''
93
94 def parse_args():
95         options = {}
96
97         def help(option, opt, value, parser):
98                 print_help(sys.stdout, parser)
99                 sys.exit(0)
100
101         def set_flag(option, opt, value, parser):
102                 options[option.dest] = True
103
104         def set_jobs(option, opt, value, parser):
105                 if value < 1:
106                         raise optparse.OptionValueError(
107                                 'option %s: invalid job count: %d' % \
108                                 (opt, value))
109                 options[option.dest] = value
110
111         def append(option, opt, value, parser):
112                 l = options.get(option.dest, [])
113                 if not l:
114                         options[option.dest] = l
115                 l.append(value)
116
117         parser = optparse.OptionParser(
118                 usage='%prog [<options>] <command> [<components>]',
119                 add_help_option=False)
120
121         parser.add_option(
122                 '-h', '--help',
123                 help='show this help message and exit',
124                 action='callback', callback=help)
125
126         parser.add_option(
127                 '-v', '--verbose',
128                 help='show build output',
129                 dest='verbose', action='callback', callback=set_flag)
130
131         parser.add_option(
132                 '-d', '--debug',
133                 help='debug output',
134                 dest='debug', action='callback', callback=set_flag)
135
136         parser.add_option(
137                 '-c', '--config-dir', metavar='DIR',
138                 help='add a directory for finding config files (may be set ' \
139                      'multiple times)',
140                 dest='initial_config_dirs', action='callback', callback=append,
141                 type='string')
142
143         parser.add_option(
144                 '-r', '--root', metavar='URL',
145                 help='add a location for finding git repos when cloning ' \
146                      '(may be set multiple times)',
147                 dest='roots', action='callback', callback=append,
148                 type='string')
149
150         parser.add_option(
151                 '-f', '--force',
152                 help='build (or be quiet about) components with dirty files',
153                 dest='force', action='callback', callback=set_flag)
154
155         parser.add_option(
156                 '-k', '--keep-going',
157                 help='build as much as possible after an error',
158                 dest='keep_going', action='callback', callback=set_flag)
159
160         parser.add_option(
161                 '-j', '--jobs',
162                 help='execute up to N build jobs in parallel', metavar='N',
163                 dest='jobs', action='callback', callback=set_jobs,
164                 type='int')
165
166         parser.add_option(
167                 '-J', '--make-jobs',
168                 help='run component builds with make -j N', metavar='N',
169                 dest='make_jobs', action='callback', callback=set_jobs,
170                 type='int')
171
172         values, args = parser.parse_args(values=None)
173         if not args:
174                 print_help(sys.stderr, parser)
175                 sys.exit(1)
176
177         return args[0], args[1:], options
178
179 def execute(args):
180         if config.debug:
181                 print 'Executing:', ' '.join(args)
182
183         if os.spawnvp(os.P_WAIT, args[0], args) != 0:
184                 raise Error('Failed: ' + ' '.join(args))
185
186 def remove_tree(path):
187         execute(['rm', '-rf', path])
188
189 def meta(targets):
190         for name in targets:
191                 c = config.components[name]
192                 if not c.meta.exists():
193                         c.meta.clone()
194         
195 def clone(targets):
196         for name in targets:
197                 c = config.components[name]
198                 clone_component(c)
199
200 def clone_component(c, overwrite=False):
201         have_source = c.source.exists()
202         have_meta = c.meta.exists()
203
204         if not overwrite and have_source and have_meta:
205                 return
206
207         if overwrite and os.path.exists(c.source.path):
208                 print 'Removing', c
209
210                 remove_tree(c.source.path)
211                 have_source = False
212                 have_meta = False
213
214         if not have_source:
215                 c.source.clone()
216         if not have_meta:
217                 c.meta.clone()
218
219 def clean(targets):
220         changed = False
221
222         for name in targets:
223                 c = config.components[name]
224                 c.source.clean()
225                 cache.remove(c)
226
227                 if not config.force:
228                         changed = check_dirty(c)
229
230         if changed:
231                 raise Error()
232
233 def for_each_repository(func, targets=None):
234         ret = None
235
236         for name in targets:
237                 c = config.components[name]
238                 if c.source.exists():
239                         value = func(c.source)
240                         if value:
241                                 ret = value
242                 value = func(c.meta)
243                 if value:
244                         ret = value
245
246         return ret
247
248 def update(targets):
249         for_each_repository(lambda repo: repo.update(), targets)
250
251 def rebase(targets):
252         for_each_repository(lambda repo: repo.rebase(), targets)
253
254 def update_rebase(targets):
255         def action(repo):
256                 repo.update()
257                 repo.rebase()
258
259         for_each_repository(action, targets)
260
261 def pull(targets):
262         for_each_repository(lambda repo: repo.pull(), targets)
263
264 def changes(targets):
265         changed = False
266
267         for name in targets:
268                 c = config.components[name]
269
270                 if c.source.exists():
271                         if check_changes(c.source):
272                                 changed = True
273
274                 if check_changes(c.meta):
275                         changed = True
276
277                 if not config.force and check_dirty(c):
278                         changed = True
279
280         if changed:
281                 raise Error()
282
283 def check_changes(repo):
284         ret = repo.changes()
285         if ret:
286                 log.message('Changes in %s' % repo)
287         return ret
288
289 def check_dirty(c):
290         ret = c.is_dirty()
291         if ret:
292                 log.message('Dirty files in %s' % c.name)
293         return ret
294
295 def source_dist(targets):
296         location = 'dist'
297         if not os.path.exists(location):
298                 os.makedirs(location)
299
300         for name in targets:
301                 c = config.components[name]
302                 dist_changes(c, location)
303                 dist_sources(c, location)
304
305 def dist_changes(c, location):
306         path = os.path.join(location, c.name) + '.changes'
307         c.source.dump_log(path)
308
309 def dist_sources(c, location):
310         rev = c.source.describe()
311         if rev:
312                 rev = '_' + rev
313         else:
314                 rev = ''
315
316         name = c.name + rev
317         path = os.path.join(location, name) + '.tar.bz2'
318         if os.path.exists(path):
319                 os.remove(path)
320
321         c.source.archive(name, path)