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