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