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
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()
