strip "src/" from target names early enough
[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         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():
93         options = {}
94
95         def help(option, opt, value, parser):
96                 print_help(sys.stdout, parser)
97                 sys.exit(0)
98
99         def set_flag(option, opt, value, parser):
100                 options[option.dest] = True
101
102         def set_jobs(option, opt, value, parser):
103                 if value < 1:
104                         raise optparse.OptionValueError(
105                                 'option %s: invalid job count: %d' % \
106                                 (opt, value))
107                 options[option.dest] = value
108
109         def append(option, opt, value, parser):
110                 l = options.get(option.dest, [])
111                 if not l:
112                         options[option.dest] = l
113                 l.append(value)
114
115         parser = optparse.OptionParser(
116                 usage='%prog [<options>] <command> [<components>]',
117                 add_help_option=False)
118
119         parser.add_option(
120                 '-h', '--help',
121                 help='show this help message and exit',
122                 action='callback', callback=help)
123
124         parser.add_option(
125                 '-v', '--verbose',
126                 help='show build output',
127                 dest='verbose', action='callback', callback=set_flag)
128
129         parser.add_option(
130                 '-d', '--debug',
131                 help='debug output',
132                 dest='debug', action='callback', callback=set_flag)
133
134         parser.add_option(
135                 '-c', '--config-dir', metavar='DIR',
136                 help='add a directory for finding config files (may be set ' \
137                      'multiple times)',
138                 dest='initial_config_dirs', action='callback', callback=append,
139                 type='string')
140
141         parser.add_option(
142                 '-r', '--root', metavar='URL',
143                 help='add a location for finding git repos when cloning ' \
144                      '(may be set multiple times)',
145                 dest='roots', action='callback', callback=append,
146                 type='string')
147
148         parser.add_option(
149                 '-f', '--force',
150                 help='build (or be quiet about) components with dirty files',
151                 dest='force', action='callback', callback=set_flag)
152
153         parser.add_option(
154                 '-k', '--keep-going',
155                 help='build as much as possible after an error',
156                 dest='keep_going', action='callback', callback=set_flag)
157
158         parser.add_option(
159                 '-j', '--jobs',
160                 help='execute up to N build jobs in parallel', metavar='N',
161                 dest='jobs', action='callback', callback=set_jobs,
162                 type='int')
163
164         parser.add_option(
165                 '-J', '--make-jobs',
166                 help='run component builds with make -j N', metavar='N',
167                 dest='make_jobs', action='callback', callback=set_jobs,
168                 type='int')
169
170         values, args = parser.parse_args(values=None)
171         if not args:
172                 print_help(sys.stderr, parser)
173                 sys.exit(1)
174
175         return args[0], args[1:], options
176
177 def execute(args):
178         if config.debug:
179                 print 'Executing:', ' '.join(args)
180
181         if os.spawnvp(os.P_WAIT, args[0], args) != 0:
182                 raise Error('Failed: ' + ' '.join(args))
183
184 def remove_tree(path):
185         execute(['rm', '-rf', path])
186
187 def meta(targets):
188         for name in targets:
189                 c = components.by_name[name]
190                 if not c.meta.exists():
191                         c.meta.clone()
192         
193 def clone(targets):
194         for name in targets:
195                 c = components.by_name[name]
196                 clone_component(c)
197
198 def clone_component(c, overwrite=False):
199         have_source = c.source.exists()
200         have_meta = c.meta.exists()
201
202         if not overwrite and have_source and have_meta:
203                 return
204
205         if overwrite and os.path.exists(c.source.path):
206                 print 'Removing', c
207
208                 remove_tree(c.source.path)
209                 have_source = False
210                 have_meta = False
211
212         if not have_source:
213                 c.source.clone()
214         if not have_meta:
215                 c.meta.clone()
216
217 def clean(targets):
218         changed = False
219
220         for name in targets:
221                 c = components.by_name[name]
222                 c.source.clean()
223                 cache.remove(c)
224
225                 if not config.force:
226                         changed = check_dirty(c)
227
228         if changed:
229                 raise Error()
230
231 def for_each_repository(func, targets=None):
232         ret = None
233
234         for name in targets:
235                 c = components.by_name[name]
236                 if c.source.exists():
237                         value = func(c.source)
238                         if value:
239                                 ret = value
240                 value = func(c.meta)
241                 if value:
242                         ret = value
243
244         return ret
245
246 def update(targets):
247         for_each_repository(lambda repo: repo.update(), targets)
248
249 def rebase(targets):
250         for_each_repository(lambda repo: repo.rebase(), targets)
251
252 def update_rebase(targets):
253         def action(repo):
254                 repo.update()
255                 repo.rebase()
256
257         for_each_repository(action, targets)
258
259 def pull(targets):
260         for_each_repository(lambda repo: repo.pull(), targets)
261
262 def changes(targets):
263         changed = False
264
265         for name in targets:
266                 c = components.by_name[name]
267
268                 if c.source.exists():
269                         if check_changes(c.source):
270                                 changed = True
271
272                 if check_changes(c.meta):
273                         changed = True
274
275                 if not config.force and check_dirty(c):
276                         changed = True
277
278         if changed:
279                 raise Error()
280
281 def check_changes(repo):
282         ret = repo.changes()
283         if ret:
284                 log.message('Changes in %s' % repo)
285         return ret
286
287 def check_dirty(c):
288         ret = c.is_dirty()
289         if ret:
290                 log.message('Dirty files in %s' % c.name)
291         return ret
292
293 def source_dist(targets):
294         location = 'dist'
295         if not os.path.exists(location):
296                 os.makedirs(location)
297
298         for name in targets:
299                 c = components.by_name[name]
300                 dist_changes(c, location)
301                 dist_sources(c, location)
302
303 def dist_changes(c, location):
304         path = os.path.join(location, c.name) + '.changes'
305         c.source.dump_log(path)
306
307 def dist_sources(c, location):
308         rev = c.source.describe()
309         if rev:
310                 rev = '_' + rev
311         else:
312                 rev = ''
313
314         name = c.name + rev
315         path = os.path.join(location, name) + '.tar.bz2'
316         if os.path.exists(path):
317                 os.remove(path)
318
319         c.source.archive(name, path)