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