config: git-pull config dirs automatically
[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', '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():
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                 '-p', '--pull-config-dirs',
143                 help='git-pull config dirs which are backed by Git repositories',
144                 dest='pull_config_dirs', action='callback', callback=set_flag)
145
146         parser.add_option(
147                 '-r', '--root', metavar='URL',
148                 help='add a location for finding git repos when cloning ' \
149                      '(may be set multiple times)',
150                 dest='roots', action='callback', callback=append,
151                 type='string')
152
153         parser.add_option(
154                 '-f', '--force',
155                 help='build (or be quiet about) components with dirty files',
156                 dest='force', action='callback', callback=set_flag)
157
158         parser.add_option(
159                 '-k', '--keep-going',
160                 help='build as much as possible after an error',
161                 dest='keep_going', action='callback', callback=set_flag)
162
163         parser.add_option(
164                 '-j', '--jobs',
165                 help='execute up to N build jobs in parallel', metavar='N',
166                 dest='jobs', action='callback', callback=set_jobs,
167                 type='int')
168
169         parser.add_option(
170                 '-J', '--make-jobs',
171                 help='run component builds with make -j N', metavar='N',
172                 dest='make_jobs', action='callback', callback=set_jobs,
173                 type='int')
174
175         values, args = parser.parse_args(values=None)
176         if not args:
177                 print_help(sys.stderr, parser)
178                 sys.exit(1)
179
180         return args[0], args[1:], options
181
182 def execute(args):
183         if config.debug:
184                 print 'Executing:', ' '.join(args)
185
186         if os.spawnvp(os.P_WAIT, args[0], args) != 0:
187                 raise Error('Failed: ' + ' '.join(args))
188
189 def remove_tree(path):
190         execute(['rm', '-rf', path])
191
192 def meta(targets):
193         for name in targets:
194                 c = components.by_name[name]
195                 if not c.meta.exists():
196                         c.meta.clone()
197         
198 def clone(targets):
199         for name in targets:
200                 c = components.by_name[name]
201                 clone_component(c)
202
203 def clone_component(c, overwrite=False):
204         have_source = c.source.exists()
205         have_meta = c.meta.exists()
206
207         if not overwrite and have_source and have_meta:
208                 return
209
210         if overwrite and os.path.exists(c.source.path):
211                 print 'Removing', c
212
213                 remove_tree(c.source.path)
214                 have_source = False
215                 have_meta = False
216
217         if not have_source:
218                 c.source.clone()
219         if not have_meta:
220                 c.meta.clone()
221
222 def clean(targets):
223         changed = False
224
225         for name in targets:
226                 c = components.by_name[name]
227                 c.source.clean()
228                 cache.remove(c)
229
230                 if not config.force:
231                         changed = check_dirty(c)
232
233         if changed:
234                 raise Error()
235
236 def for_each_repository(func, targets=None):
237         ret = None
238
239         for name in targets:
240                 c = components.by_name[name]
241                 if c.source.exists():
242                         value = func(c.source)
243                         if value:
244                                 ret = value
245                 value = func(c.meta)
246                 if value:
247                         ret = value
248
249         return ret
250
251 def update(targets):
252         for_each_repository(lambda repo: repo.update(), targets)
253
254 def rebase(targets):
255         for_each_repository(lambda repo: repo.rebase(), targets)
256
257 def update_rebase(targets):
258         def action(repo):
259                 repo.update()
260                 repo.rebase()
261
262         for_each_repository(action, targets)
263
264 def pull(targets):
265         for_each_repository(lambda repo: repo.pull(), targets)
266
267 def changes(targets):
268         changed = False
269
270         for name in targets:
271                 c = components.by_name[name]
272
273                 if c.source.exists():
274                         if check_changes(c.source):
275                                 changed = True
276
277                 if check_changes(c.meta):
278                         changed = True
279
280                 if not config.force and check_dirty(c):
281                         changed = True
282
283         if changed:
284                 raise Error()
285
286 def check_changes(repo):
287         ret = repo.changes()
288         if ret:
289                 log.message('Changes in %s' % repo)
290         return ret
291
292 def check_dirty(c):
293         ret = c.is_dirty()
294         if ret:
295                 log.message('Dirty files in %s' % c.name)
296         return ret
297
298 def source_dist(targets):
299         location = 'dist'
300         if not os.path.exists(location):
301                 os.makedirs(location)
302
303         for name in targets:
304                 c = components.by_name[name]
305                 dist_changes(c, location)
306                 dist_sources(c, location)
307
308 def dist_changes(c, location):
309         path = os.path.join(location, c.name) + '.changes'
310         c.source.dump_log(path)
311
312 def dist_sources(c, location):
313         rev = c.source.describe()
314         if rev:
315                 rev = '_' + rev
316         else:
317                 rev = ''
318
319         name = c.name + rev
320         path = os.path.join(location, name) + '.tar.bz2'
321         if os.path.exists(path):
322                 os.remove(path)
323
324         c.source.archive(name, path)