4bf796adcb20a1203a546db1da51da3aa5405dd4
[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         for name in targets:
286                 c = find_component(name)
287
288                 if c.source.exists():
289                         if check_changes(c.source):
290                                 changed = True
291
292                 if check_changes(c.meta):
293                         changed = True
294
295                 if not config.force and check_dirty(c):
296                         changed = True
297
298         if changed:
299                 raise Error()
300
301 def check_changes(repo):
302         ret = repo.changes()
303         if ret:
304                 log.message('Changes in %s' % repo)
305         return ret
306
307 def check_dirty(c):
308         ret = c.is_dirty()
309         if ret:
310                 log.message('Dirty files in %s' % c.name)
311         return ret
312
313 def source_dist(targets):
314         location = 'dist'
315         if not os.path.exists(location):
316                 os.makedirs(location)
317
318         for name in targets:
319                 c = find_component(name)
320                 dist_changes(c, location)
321                 dist_sources(c, location)
322
323 def dist_changes(c, location):
324         path = os.path.join(location, c.name) + '.changes'
325         c.source.dump_log(path)
326
327 def dist_sources(c, location):
328         rev = c.source.describe()
329         if rev:
330                 rev = '_' + rev
331         else:
332                 rev = ''
333
334         name = c.name + rev
335         path = os.path.join(location, name) + '.tar.bz2'
336         if os.path.exists(path):
337                 os.remove(path)
338
339         c.source.archive(name, path)