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