debrelease.proposed: squeeze has apt_pkg.init_system()
[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.
7 """Wrapper around wanna-build.
9 Syntax is:
11     % wb <command> srcpkg1 [...] . arch1 [...] [ . dist ] [ . <extra-opts> ]
13 Command can be:
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
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.
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.
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:
39     % wb gb foopkg . ALL -i386 m68k
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.
46 To use wb interactively, while keeping a long-running lock on the wanna-build
47 database, use
49     % wb --transaction
51 This will lock the databases for all arches until wb finishes.
53 """
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?)
59 DEFAULT_DISTRIBUTION = 'unstable'
61 import os
62 import re
63 import sys
64 import shlex
65 import string
66 import readline # nicer raw_input()
67 import subprocess
69 from subprocess import PIPE
71 ##
73 def main():
74     global DISTRIBUTION_ALIASES
75     DISTRIBUTION_ALIASES = get_distribution_aliases()
76     global DISTRIBUTION_ARCHES
77     DISTRIBUTION_ARCHES = get_distribution_arch_map()
79     args = sys.argv[1:]
81     transactional = False
82     if args == ["--transaction"]:
83         transactional = True
84         args = []
86     if args:
87         do_command(args)
88     else:
89         isatty = os.isatty(sys.stdin.fileno())
91         if transactional:
92             get_transaction_locks()
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
109             if re.search(r'^\s*(#|$)', line):
110                 continue
112             # shlex.split() handles -m 'foo bar' correctly
113             words = shlex.split(line, comments=True)
114             do_command(words, fatal_errors=False, transactional=transactional)
117         if transactional:
118             release_transaction_locks()
121 ##
123 commands = [ 'bp', 'gb', 'dw', 'nmu', 'fail', 'forget', 'info', 'pa', 'nfu', 'help', 'abort' ]
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
145     if command == 'help':
146         usage()
147         return
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         
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:]
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]
183     # distribution
184     dist = None
185     add_dist = False
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
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
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
233     if add_dist:
234         extra.extend([ '-d', dist ])
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
243     # remove commas
244     pkgs = [ re.sub(',', '', x) for x in pkgs ]
245     arches = [ re.sub(',', '', x) for x in arches ]
247     # process ALL and '-' in arches
248     arches_orig = arches
249     arches = set()
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)
259     ##
261     prio = None
262     global_binNMU = None
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))
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
282     ##
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     }
296     for pkg in pkgs:
297         if '_' in pkg:
298             pkg, srcver = pkg.split('_', 1)
299         else:
300             srcver = None
302         for arch in sorted(arches):
303             info = get_wb_info(pkg, dist, arch, transactional=transactional)
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']
314             cmd = [ 'wanna-build', '-A', arch ]
316             if transactional:
317                 cmd.extend([ '--transactional', '--act-on-behalve-of', str(os.getpid()) ])
319             if command in ['gb', 'dw', 'fail']:
320                 builder = info.get('Builder', None)
322                 if builder is not None:
323                     cmd.extend(['-U', builder])
325             if command in simple_command_options:
326                 cmd.append(simple_command_options[command])
328             elif command == 'bp':
329                 cmd.extend(['--build-priority', str(prio)])
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
343                 cmd.append('--pretend-avail')
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
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
377                 cmd.extend(['--binNMU', str(binNMU)])
379             cmd.extend(extra)
380             cmd.append('%s_%s' % (pkg, srcver))
382             p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
383             status = p.wait()
384             output = p.stdout.readlines() + p.stderr.readlines()
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)
392 ##
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())])
399         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
400         status = p.wait()
401         output = p.stdout.readlines() + p.stderr.readlines()
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)
409         cmd = [ 'wanna-build', '-A', arch ]
410         cmd.extend(['--act-on-behalve-of', str(os.getpid()) ])
411         cmd.extend(['--start-transaction'])
413         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
414         status = p.wait()
415         output = p.stdout.readlines() + p.stderr.readlines()
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)
423 ##
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'])
431         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
432         status = p.wait()
433         output = p.stdout.readlines() + p.stderr.readlines()
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)
441         cmd = [ 'wanna-build', '-A', arch ]
442         cmd.extend(['--unlock-for', str(os.getpid())])
444         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
445         status = p.wait()
446         output = p.stdout.readlines() + p.stderr.readlines()
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 ##
455 def abort():
456     for arch in DEFAULT_ARCHITECTURES:
457         cmd = [ 'wanna-build', '-A', arch ]
458         cmd.extend(['--unlock-for', str(os.getpid())])
460         p = subprocess.Popen(cmd, stdout=PIPE, stderr=PIPE)
461         status = p.wait()
462         output = p.stdout.readlines() + p.stderr.readlines()
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)
470 ##
472 def get_wb_info(pkg, dist, arch, transactional=False):
473     cmd = [ 'wanna-build', '-A', arch, '-d', dist]
475     if transactional:
476         cmd.extend([ '--transactional', '--act-on-behalve-of', str(os.getpid()) ])
478     cmd.extend(['--info', pkg])
480     p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
481     p.wait()
483     info = []
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)
492     return dict(info)
494 ##
496 def get_distribution_aliases():
497     cmd = [ 'wanna-build', '--distribution-aliases' ]
499     p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
500     p.wait()
502     info = []
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)
510     return dict(info)
512 ##
514 def get_distribution_arch_map():
515     cmd = [ 'wanna-build', '--distribution-architectures' ]
517     p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
518     p.wait()
520     info = {}
522     for line in p.stdout:
523         (dist,arches) = line.split(':', 1)
524         arches = string.strip(arches).split(' ')
525         info[dist] = arches
527     return info
529 ##
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)
539 def print_commands():
540     print >>sys.stderr, 'Available commands: %s' % ', '.join(commands)
542 ##
544 if __name__ == '__main__':
545     main()