print --version
[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 meta(targets):
202         for name in targets:
203                 c = components.by_name[name]
204                 if not c.meta.exists():
205                         c.meta.clone()
206         
207 def clone(targets):
208         for name in targets:
209                 c = components.by_name[name]
210                 clone_component(c)
211
212 def clone_component(c, overwrite=False):
213         have_source = c.source.exists()
214         have_meta = c.meta.exists()
215
216         if not overwrite and have_source and have_meta:
217                 return
218
219         if overwrite and os.path.exists(c.source.path):
220                 print 'Removing', c
221
222                 remove_tree(c.source.path)
223                 have_source = False
224                 have_meta = False
225
226         if not have_source:
227                 c.source.clone()
228         if not have_meta:
229                 c.meta.clone()
230
231 def clean(targets):
232         changed = False
233
234         for name in targets:
235                 c = components.by_name[name]
236                 c.source.clean()
237                 cache.remove(c)
238
239                 if not config.force:
240                         changed = check_dirty(c)
241
242         if changed:
243                 raise Error()
244
245 def for_each_repository(func, targets=None):
246         ret = None
247
248         for name in targets:
249                 c = components.by_name[name]
250                 if c.source.exists():
251                         value = func(c.source)
252                         if value:
253                                 ret = value
254                 value = func(c.meta)
255                 if value:
256                         ret = value
257
258         return ret
259
260 def update(targets):
261         for_each_repository(lambda repo: repo.update(), targets)
262
263 def rebase(targets):
264         for_each_repository(lambda repo: repo.rebase(), targets)
265
266 def update_rebase(targets):
267         def action(repo):
268                 repo.update()
269                 repo.rebase()
270
271         for_each_repository(action, targets)
272
273 def pull(targets):
274         for_each_repository(lambda repo: repo.pull(), targets)
275
276 def changes(targets):
277         changed = False
278
279         for name in targets:
280                 c = components.by_name[name]
281
282                 if c.source.exists():
283                         if check_changes(c.source):
284                                 changed = True
285
286                 if check_changes(c.meta):
287                         changed = True
288
289                 if not config.force and check_dirty(c):
290                         changed = True
291
292         if changed:
293                 raise Error()
294
295 def check_changes(repo):
296         ret = repo.changes()
297         if ret:
298                 log.message('Changes in %s' % repo)
299         return ret
300
301 def check_dirty(c):
302         ret = c.is_dirty()
303         if ret:
304                 log.message('Dirty files in %s' % c.name)
305         return ret
306
307 def source_dist(targets):
308         location = 'dist'
309         if not os.path.exists(location):
310                 os.makedirs(location)
311
312         for name in targets:
313                 c = components.by_name[name]
314                 dist_changes(c, location)
315                 dist_sources(c, location)
316
317 def dist_changes(c, location):
318         path = os.path.join(location, c.name) + '.changes'
319         c.source.dump_log(path)
320
321 def dist_sources(c, location):
322         rev = c.source.describe()
323         if rev:
324                 rev = '_' + rev
325         else:
326                 rev = ''
327
328         name = c.name + rev
329         path = os.path.join(location, name) + '.tar.bz2'
330         if os.path.exists(path):
331                 os.remove(path)
332
333         c.source.archive(name, path)