debdiff.py: Remove unused method _invoke_diffstat
[debian-release/release-tools.git] / scripts / wb
1 #! /usr/bin/python
2 ## vim:set encoding=utf-8 ts=4 sw=4 et:
3 #
4 # Copyright (c) 2008 Adeodato Simó (dato@net.com.org.es)
5 # Licensed under the terms of the MIT license.
6
7 """Wrapper around wanna-build.
8
9 Syntax is:
10
11     % wb <command> srcpkg1 [...] . arch1 [...] [ . dist ] [ . <extra-opts> ]
12
13 Command can be:
14
15     * gb: --give-back
16     * dw: --dep-wait
17     * bp: --build-priority
18     * nmu: --binNMU
19     * info: --info
20     * fail: --failed
21     * forget: --forget
22     * pa: --pretend-avail
23     * nfu: --no-build
24
25 'dw', 'nmu' and 'fail' prompt for a message (dep-wait expr, binNMU changelog,
26 or failure reason). 'nmu' and 'bp' accept an optional numeric argument between
27 the command name and the list of packages (binNMU version, and priority); if
28 not given, 'nmu' will use the next available binNMU number, and 'bp' will use
29 "100" as priority.
30
31 Package names can come with a version number too, as in pkgname_1.2-3; this
32 is useful when generating binNMU commands from Packages files, where a new
33 version of a package may have been uploaded but not dinstalled yet.
34
35 When listing architectures, the special word ALL can be used, and it'll expand
36 to all architectures for the suite being manipulated. You can then
37 subtract some of them by prefixing them with a dash, like this:
38
39     % wb gb foopkg . ALL -i386 m68k
40
41 Extra options, like -d 'stable' or -m 'message', can be passed by appending
42 them to the end of the line, with an extra dot.  If the extra options
43 contain -d and a distribution was specified earlier in the command line, 
44 an error occurs.
45
46 To use wb interactively, while keeping a long-running lock on the wanna-build
47 database, use
48
49     % wb --transaction
50
51 This will lock the databases for all arches until wb finishes.
52
53 """
54
55 # TODO
56 #   * allow for a "dry" (just print command) or "confirm" word to appear before
57 #     the command (NOTE: we need a shorter word instead of confirm, phil?)
58
59 DEFAULT_DISTRIBUTION = 'unstable'
60
61 import os
62 import re
63 import sys
64 import shlex
65 import string
66 import readline # nicer raw_input()
67 import subprocess
68
69 from subprocess import PIPE
70
71 ##
72
73 def main():
74     global DISTRIBUTION_ALIASES
75     DISTRIBUTION_ALIASES = get_distribution_aliases()
76     global DISTRIBUTION_ARCHES
77     DISTRIBUTION_ARCHES = get_distribution_arch_map()
78
79     args = sys.argv[1:]
80
81     transactional = False
82     if args == ["--transaction"]:
83         transactional = True
84         args = []
85
86     if args:
87         do_command(args)
88     else:
89         isatty = os.isatty(sys.stdin.fileno())
90
91         if transactional:
92             get_transaction_locks()
93
94         while True:
95             if isatty:
96                 try:
97                     line = raw_input('wb> ')
98                 except EOFError:
99                     print
100                     break
101                 except KeyboardInterrupt:
102                     print
103                     continue
104             else:
105                 line = sys.stdin.readline()
106                 if not line:
107                     break
108
109             if re.search(r'^\s*(#|$)', line):
110                 continue
111
112             # shlex.split() handles -m 'foo bar' correctly
113             words = shlex.split(line, comments=True)
114             do_command(words, fatal_errors=False, transactional=transactional)
115
116
117         if transactional:
118             release_transaction_locks()
119
120
121 ##
122
123 commands = [ 'bp', 'gb', 'dw', 'nmu', 'fail', 'forget', 'info', 'pa', 'nfu', 'help', 'abort' ]
124
125 def do_command(args, fatal_errors=True, transactional=False):
126     try:
127         command = args.pop(0)
128     except IndexError:
129         if fatal_errors:
130             usage(1)
131         else:
132             usage()
133             return
134     else:
135         if command == 'pb': # prio-bump
136             command = 'bp'
137         elif command not in commands:
138             print >>sys.stderr, 'E: unknown command %r' % command
139             print_commands()
140             if fatal_errors:
141                 sys.exit(1)
142             else:
143                 return
144
145     if command == 'help':
146         usage()
147         return
148
149     if command == 'abort':
150         if transactional:
151             abort()
152             sys.exit(1)
153         else:
154             print >>sys.stderr, 'E: abort only valid in a transaction'
155             if fatal_errors:
156                 sys.exit(1)
157             else:
158                 return
159         
160
161     # pkgs/arch split
162     try:
163         idx = args.index('.')
164     except ValueError:
165         print >>sys.stderr, 'E: missing dot in line'
166         if fatal_errors:
167             usage(1)
168         else:
169             return
170     else:
171         pkgs = args[:idx]
172         arches = args[idx+1:]
173
174     # arch/extra split
175     try:
176         idx = arches.index('.')
177     except ValueError:
178         extra = []
179     else:
180         extra = arches[idx+1:]
181         arches = arches[:idx]
182
183     # distribution
184     dist = None
185     add_dist = False
186
187     # . dist . extra ?
188     try:
189         idx = extra.index('.')
190     except ValueError:
191         if len(extra) > 0 and not extra[0].startswith('-'):
192             # assume that a first non-option parameter is a dist
193             dist = extra[0]
194             extra = extra[1:]
195             add_dist = True
196     else:
197         # split into dist and extra
198         dist = extra[0]
199         extra = extra[idx+1:]
200         add_dist = True
201
202     try:
203         idx = extra.index('-d')
204     except ValueError:
205         # if not already added in the previous step
206         if dist is None:
207             dist = DEFAULT_DISTRIBUTION
208             add_dist = True
209     else:
210         if dist is None:
211             dist = extra[idx+1]
212         else:
213             print >>sys.stderr, (
214                 "E: Multiple distributions specified - '%s' and '-d %s'"
215                 % (dist, extra[idx+1]))
216             if fatal_errors:
217                 usage(1)
218             else:
219                 return
220
221     if dist in DISTRIBUTION_ALIASES:
222         origdist = dist
223         dist = DISTRIBUTION_ALIASES[origdist]
224         try:
225             idx = extra.index('-d')
226         except ValueError:
227             pass
228         else:
229             # replace "-d alias" with "-d real"
230             if extra[idx+1] == origdist:
231                 extra[idx+1] = dist
232
233     if add_dist:
234         extra.extend([ '-d', dist ])
235
236     if dist not in DISTRIBUTION_ARCHES:
237         print >>sys.stderr, 'E: unknown distribution %s' % (dist)
238         if fatal_errors:
239             usage(1)
240         else:
241             return
242
243     # remove commas
244     pkgs = [ re.sub(',', '', x) for x in pkgs ]
245     arches = [ re.sub(',', '', x) for x in arches ]
246
247     # process ALL and '-' in arches
248     arches_orig = arches
249     arches = set()
250
251     for a in arches_orig:
252         if a == 'ALL':
253             arches.update(DISTRIBUTION_ARCHES[dist])
254         elif a.startswith('-'):
255             arches.discard(a[1:])
256         else:
257             arches.add(a)
258
259     ##
260
261     prio = None
262     global_binNMU = None
263
264     if command == 'bp':
265         if re.match(r'-?\d+$', pkgs[0]):
266             prio = pkgs.pop(0)
267         else:
268             prio = '100'
269     elif command == 'nmu' and re.match(r'\d+$', pkgs[0]):
270         global_binNMU = int(pkgs.pop(0))
271
272     if ('-m' not in extra and
273             (command in [ 'dw', 'fail' ] or
274                 command == 'nmu' and global_binNMU != 0)):
275         extra.append('-m')
276         try:
277             extra.append(raw_input('Message: ').strip())
278         except KeyboardInterrupt:
279             print '\nCancelled.'
280             return
281
282     ##
283
284     simple_command_options = {
285         # This dict contains the option corresponding to each
286         # command, if the command does not need any other handling
287         # than cmdline.append(option).
288         'gb': '--give-back',
289         'dw': '--dep-wait',
290         'info': '--info',
291         'fail': '--failed',
292         'forget': '--forget',
293         'nfu': '--no-build',
294     }
295
296     for pkg in pkgs:
297         if '_' in pkg:
298             pkg, srcver = pkg.split('_', 1)
299         else:
300             srcver = None
301
302         for arch in sorted(arches):
303             info = get_wb_info(pkg, dist, arch, transactional=transactional)
304
305             if command == 'pa':
306                 # May not exist in the database at all
307                 pass
308             elif not info.has_key('Version'):
309                 print >>sys.stderr, "W: can't get version info for %s/%s" % (pkg, arch)
310                 continue
311             elif srcver is None:
312                 srcver = info['Version']
313
314             cmd = [ 'wanna-build', '-A', arch ]
315
316             if transactional:
317                 cmd.extend([ '--transactional', '--act-on-behalve-of', str(os.getpid()) ])
318
319             if command in ['gb', 'dw', 'fail']:
320                 builder = info.get('Builder', None)
321
322                 if builder is not None:
323                     cmd.extend(['-U', builder])
324
325             if command in simple_command_options:
326                 cmd.append(simple_command_options[command])
327
328             elif command == 'bp':
329                 cmd.extend(['--build-priority', str(prio)])
330
331             elif command == 'pa':
332                 if srcver is None:
333                     print >>sys.stderr, (
334                         "W: must provide a version for %s/%s, skipping"
335                         % (pkg, arch))
336                     continue
337                 elif info.has_key('Version') and info['Version'] == srcver:
338                     print >>sys.stderr, (
339                         "W: %s/%s already exists at version %s, skipping"
340                         % (pkg, arch, srcver))
341                     continue
342
343                 cmd.append('--pretend-avail')
344
345             elif command == 'nmu':
346                 try:
347                     installed_version = info['Installed-Version']
348                 except KeyError:
349                     print >>sys.stderr, (
350                         "W: package %s is not installed on %s, can't binNMU."
351                         % (pkg, arch))
352                     continue
353                 m = re.search(r'%s\+b(\d+)$' % re.escape(srcver),
354                               installed_version)
355                 if m:
356                     binNMU = int(m.group(1)) + 1
357                 else:
358                     binNMU = 1
359                     try:
360                         current_binNMU = info['Binary-NMU-Version']
361                         if re.match(r'\d+$', current_binNMU):
362                             binNMU = int(current_binNMU) + 1
363                     except KeyError:
364                         pass
365
366                 if global_binNMU is not None:
367                     if (binNMU > global_binNMU
368                             and global_binNMU != 0):
369                         print >>sys.stderr, \
370                             'W: specified %s/+b%d is not greater than +b%d present in %s, skipping' % (
371                                     pkg, global_binNMU, binNMU - 1, arch)
372                         continue
373                     else:
374                         binNMU = global_binNMU
375
376
377                 cmd.extend(['--binNMU', str(binNMU)])
378
379             cmd.extend(extra)
380             cmd.append('%s_%s' % (pkg, srcver))
381
382             p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
383             status = p.wait()
384             output = p.stdout.readlines() + p.stderr.readlines()
385
386             if status or output:
387                 print '* %s/%s%s' % (pkg, arch,
388                         status and ': exit status %d' % status or '')
389                 if output:
390                     print ''.join('  | ' + x for x in output)
391
392 ##
393
394 def get_transaction_locks():
395     for arch in DEFAULT_ARCHITECTURES:
396         cmd = [ 'wanna-build', '-A', arch ]
397         cmd.extend(['--lock-for', str(os.getpid())])
398
399         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
400         status = p.wait()
401         output = p.stdout.readlines() + p.stderr.readlines()
402
403         if status or output:
404             print '* Aquiring lock for %s%s' % (arch,
405                     status and ': exit status %d' % status or '')
406             if output:
407                 print ''.join('  | ' + x for x in output)
408
409         cmd = [ 'wanna-build', '-A', arch ]
410         cmd.extend(['--act-on-behalve-of', str(os.getpid()) ])
411         cmd.extend(['--start-transaction'])
412
413         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
414         status = p.wait()
415         output = p.stdout.readlines() + p.stderr.readlines()
416
417         if status or output:
418             print '* Starting transaction for %s%s' % (arch,
419                     status and ': exit status %d' % status or '')
420             if output:
421                 print ''.join('  | ' + x for x in output)
422
423 ##
424
425 def release_transaction_locks():
426     for arch in DEFAULT_ARCHITECTURES:
427         cmd = [ 'wanna-build', '-A', arch ]
428         cmd.extend(['--act-on-behalve-of', str(os.getpid()) ])
429         cmd.extend(['--commit-transaction'])
430
431         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
432         status = p.wait()
433         output = p.stdout.readlines() + p.stderr.readlines()
434
435         if status or output:
436             print '* Aborting transaction for %s%s' % (arch,
437                     status and ': exit status %d' % status or '')
438             if output:
439                 print ''.join('  | ' + x for x in output)
440
441         cmd = [ 'wanna-build', '-A', arch ]
442         cmd.extend(['--unlock-for', str(os.getpid())])
443
444         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
445         status = p.wait()
446         output = p.stdout.readlines() + p.stderr.readlines()
447
448         if status or output:
449             print '* Releasing lock for %s%s' % (arch,
450                     status and ': exit status %d' % status or '')
451             if output:
452                 print ''.join('  | ' + x for x in output)
453 ##
454
455 def abort():
456     for arch in DEFAULT_ARCHITECTURES:
457         cmd = [ 'wanna-build', '-A', arch ]
458         cmd.extend(['--unlock-for', str(os.getpid())])
459
460         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
461         status = p.wait()
462         output = p.stdout.readlines() + p.stderr.readlines()
463
464         if status or output:
465             print '* Releasing lock for %s%s' % (arch,
466                     status and ': exit status %d' % status or '')
467             if output:
468                 print ''.join('  | ' + x for x in output)
469
470 ##
471
472 def get_wb_info(pkg, dist, arch, transactional=False):
473     cmd = [ 'wanna-build', '-A', arch, '-d', dist]
474
475     if transactional:
476         cmd.extend([ '--transactional', '--act-on-behalve-of', str(os.getpid()) ])
477
478     cmd.extend(['--info', pkg])
479
480     p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
481     p.wait()
482
483     info = []
484
485     for line in p.stdout:
486         if line.startswith('  '):
487             x = map(string.strip, line.split(':', 1))
488             if len(x) == 1:
489                 x.append(None)
490             info.append(x)
491
492     return dict(info)
493
494 ##
495
496 def get_distribution_aliases():
497     cmd = [ 'wanna-build', '--distribution-aliases' ]
498
499     p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
500     p.wait()
501
502     info = []
503
504     for line in p.stdout:
505         x = map(string.strip, line.split(':', 1))
506         if len(x) == 1:
507             x.append(None)
508         info.append(x)
509
510     return dict(info)
511
512 ##
513
514 def get_distribution_arch_map():
515     cmd = [ 'wanna-build', '--distribution-architectures' ]
516
517     p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
518     p.wait()
519
520     info = {}
521
522     for line in p.stdout:
523         (dist,arches) = line.split(':', 1)
524         arches = string.strip(arches).split(' ')
525         info[dist] = arches
526
527     return info
528
529 ##
530
531 def usage(exit=None):
532     print >>sys.stderr, \
533         'Usage: %s <command> srcpkg1 srcpkg2 [...] . arch1 arch2 [...]' % (
534                 os.path.basename(sys.argv[0]))
535     print_commands()
536     if exit is not None:
537         sys.exit(exit)
538
539 def print_commands():
540     print >>sys.stderr, 'Available commands: %s' % ', '.join(commands)
541
542 ##
543
544 if __name__ == '__main__':
545     main()