chiark / gitweb /
bin/chroot-maint: Program for maintaining chroots.
[distorted-chroot] / bin / chroot-maint
1 #! /usr/bin/python
2 ###
3 ### Create, upgrade, and maintain (native and cross-) chroots
4 ###
5 ### (c) 2018 Mark Wooding
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the distorted.org.uk chroot maintenance tools.
11 ###
12 ### distorted-chroot is free software: you can redistribute it and/or
13 ### modify it under the terms of the GNU General Public License as
14 ### published by the Free Software Foundation; either version 2 of the
15 ### License, or (at your option) any later version.
16 ###
17 ### distorted-chroot is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20 ### General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with distorted-chroot.  If not, write to the Free Software
24 ### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
25 ### USA.
26
27 ## still to do:
28 ##   tidy up
29
30 import contextlib as CTX
31 import errno as E
32 import fcntl as FC
33 import fnmatch as FM
34 import glob as GLOB
35 import itertools as I
36 import optparse as OP
37 import os as OS
38 import random as R
39 import re as RX
40 import signal as SIG
41 import select as SEL
42 import stat as ST
43 from cStringIO import StringIO
44 import sys as SYS
45 import time as T
46 import traceback as TB
47
48 import jobclient as JC
49
50 QUIS = OS.path.basename(SYS.argv[0])
51 TODAY = T.strftime("%Y-%m-%d")
52 NOW = T.time()
53
54 ###--------------------------------------------------------------------------
55 ### Random utilities.
56
57 RC = 0
58 def moan(msg):
59   """Print MSG to stderr as a warning."""
60   if not OPT.silent: OS.write(2, "%s: %s\n" % (QUIS, msg))
61 def error(msg):
62   """Print MSG to stderr, and remember to exit nonzero."""
63   global RC
64   moan(msg)
65   RC = 2
66
67 class ExpectedError (Exception):
68   """A fatal error which shouldn't print a backtrace."""
69   pass
70
71 @CTX.contextmanager
72 def toplevel_handler():
73   """Catch `ExpectedError's and report Unixish error messages."""
74   try: yield None
75   except ExpectedError, err: moan(err); SYS.exit(2)
76
77 def spew(msg):
78   """Print MSG to stderr as a debug trace."""
79   if OPT.debug: OS.write(2, ";; %s\n" % msg)
80
81 class Tag (object):
82   """Unique objects with no internal structure."""
83   def __init__(me, label): me._label = label
84   def __str__(me): return '#<%s %s>' % (me.__class__.__name__, me._label)
85   def __repr__(me): return '#<%s %s>' % (me.__class__.__name__, me._label)
86
87 class Struct (object):
88   def __init__(me, **kw): me.__dict__.update(kw)
89
90 class Cleanup (object):
91   """
92   A context manager for stacking other context managers.
93
94   By itself, it does nothing.  Attach other context managers with `enter' or
95   loose cleanup functions with `add'.  On exit, contexts are left and
96   cleanups performed in reverse order.
97   """
98   def __init__(me):
99     me._cleanups = []
100   def __enter__(me):
101     return me
102   def __exit__(me, exty, exval, extb):
103     trap = False
104     for c in reversed(me._cleanups):
105       if c(exty, exval, extb): trap = True
106     return trap
107   def enter(me, ctx):
108     v = ctx.__enter__()
109     me._cleanups.append(ctx.__exit__)
110     return v
111   def add(me, func):
112     me._cleanups.append(lambda exty, exval, extb: func())
113
114 def zulu(t = None):
115   """Return the time T (default now) as a string."""
116   return T.strftime("%Y-%m-%dT%H:%M:%SZ", T.gmtime(t))
117
118 R_ZULU = RX.compile(r"^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z$")
119 def unzulu(z):
120   """Convert the time string Z back to a Unix time."""
121   m = R_ZULU.match(z)
122   if not m: raise ValueError("bad time spec `%s'" % z)
123   yr, mo, dy, hr, mi, se = map(int, m.groups())
124   return T.mktime((yr, mo, dy, hr, mi, se, 0, 0, 0))
125
126 ###--------------------------------------------------------------------------
127 ### Simple select(2) utilities.
128
129 class BaseSelector (object):
130   """
131   A base class for hooking into `select_loop'.
132
133   See `select_loop' for details of the protocol.
134   """
135   def preselect(me, rfds, wfds): pass
136   def postselect_read(me, fd): pass
137   def postselect_write(me, fd): pass
138
139 class WriteLinesSelector (BaseSelector):
140   """Write whole lines to an output file descriptor."""
141
142   def __init__(me, fd, nextfn = None, *args, **kw):
143     """
144     Initialize the WriteLinesSelector to write to the file descriptor FD.
145
146     The FD is marked non-blocking.
147
148     The lines are produced by the NEXTFN, which is called without arguments.
149     It can affect the output in three ways:
150
151       * It can return a string (or almost any other kind of object, which
152         will be converted into a string by `str'), which will be written to
153         the descriptor followed by a newline.  Lines are written in the order
154         in which they are produced.
155
156       * It can return `None', which indicates that there are no more items to
157         be written for the moment.  The function will be called again from
158         time to time, to see if it has changed its mind.  This is the right
159         thing to do in order to stall output temporarily.
160
161       * It can raise `StopIteration', which indicates that there will never
162         be any more items.  The file descriptor will be closed.
163
164     Subclasses can override this behaviour by defining a method `_next' and
165     passing `None' as the NEXTFN.
166     """
167     super(WriteLinesSelector, me).__init__(*args, **kw)
168     set_nonblocking(fd)
169     me._fd = fd
170     if nextfn is not None: me._next = nextfn
171
172     ## Selector state.
173     ##
174     ##  * `_buf' contains a number of output items, already formatted, and
175     ##    ready for output in a single batch.  It might be empty.
176     ##
177     ##  * `_pos' is the current output position in `_buf'.
178     ##
179     ##  * `_more' is set unless the `_next' function has raised
180     ##    `StopIteration': it indicates that we should close the descriptor
181     ##    once the all of the remaining data in the buffer has been sent.
182     me._buf = ""
183     me._pos = 0
184     me._more = True
185
186   def _refill(me):
187     """Refill `_buf' by calling `_next'."""
188     sio = StringIO(); n = 0
189     while n < 4096:
190       try: item = me._next()
191       except StopIteration: me._more = False; break
192       if item is None: break
193       item = str(item)
194       sio.write(item); sio.write("\n"); n += len(item) + 1
195     me._buf = sio.getvalue(); me._pos = 0
196
197   def preselect(me, rfds, wfds):
198     if me._fd == -1: return
199     if me._buf == "" and me._more: me._refill()
200     if me._buf != "" or not me._more: wfds.append(me._fd)
201
202   def postselect_write(me, fd):
203     if fd != me._fd: return
204     while True:
205       if me._pos >= len(me._buf):
206         if me._more: me._refill()
207         if not me._more: OS.close(me._fd); me._fd = -1; break
208         if not me._buf: break
209       try: n = OS.write(me._fd, me._buf[me._pos:])
210       except OSError, err:
211         if err.errno == E.EAGAIN or err.errno == E.WOULDBLOCK: break
212         elif err.errno == E.EPIPE: OS.close(me._fd); me._fd = -1; break
213         else: raise
214       me._pos += n
215
216 class ReadLinesSelector (BaseSelector):
217   """Report whole lines from an input file descriptor as they arrive."""
218
219   def __init__(me, fd, linefn = None, *args, **kw):
220     """
221     Initialize the ReadLinesSelector to read from the file descriptor FD.
222
223     The FD is marked non-blocking.
224
225     For each whole line, and the final partial line (if any), the selector
226     calls LINEFN with the line as an argument (without the terminating
227     newline, if any).
228
229     Subclasses can override this behaviour by defining a method `_line' and
230     passing `None' as the LINEFN.
231     """
232     super(ReadLinesSelector, me).__init__(*args, **kw)
233     set_nonblocking(fd)
234     me._fd = fd
235     me._buf = ""
236     if linefn is not None: me._line = linefn
237
238   def preselect(me, rfds, wfds):
239     if me._fd != -1: rfds.append(me._fd)
240
241   def postselect_read(me, fd):
242     if fd != me._fd: return
243     while True:
244       try: buf = OS.read(me._fd, 4096)
245       except OSError, err:
246         if err.errno == E.EAGAIN or err.errno == E.WOULDBLOCK: break
247         else: raise
248       if buf == "":
249         OS.close(me._fd); me._fd = -1
250         if me._buf: me._line(me._buf)
251         break
252       buf = me._buf + buf
253       i = 0
254       while True:
255         try: j = buf.index("\n", i)
256         except ValueError: break
257         me._line(buf[i:j])
258         i = j + 1
259       me._buf = buf[i:]
260
261 def select_loop(selectors):
262   """
263   Multiplex I/O between the various SELECTORS.
264
265   A `selector' SEL is an object which implements the selector protocol, which
266   consists of three methods.
267
268     * SEL.preselect(RFDS, WFDS) -- add any file descriptors which the
269       selector is interested in reading from to the list RFDS, and add file
270       descriptors it's interested in writing to to the list WFDS.
271
272     * SEL.postselect_read(FD) -- informs the selector that FD is ready for
273       reading.
274
275     * SEL.postselect_write(FD) -- informs the selector that FD is ready for
276       writing.
277
278   The `select_loop' function loops as follows.
279
280     * It calls the `preselect' method on each SELECTOR to determine what I/O
281       events it thinks are interesting.
282
283     * It waits for some interesting event to happen.
284
285     * It calls the `postselect_read' and/or `postselect_write' methods on all
286       of the selectors for each file descriptor which is ready.
287
288   The loop ends when no selector is interested in any events.  This is simple
289   but rather inefficient.
290   """
291   while True:
292     rfds, wfds = [], []
293     for sel in selectors: sel.preselect(rfds, wfds)
294     if not rfds and not wfds: break
295     rfds, wfds, _ = SEL.select(rfds, wfds, [])
296     for fd in rfds:
297       for sel in selectors: sel.postselect_read(fd)
298     for fd in wfds:
299       for sel in selectors: sel.postselect_write(fd)
300
301 ###--------------------------------------------------------------------------
302 ### Running subprocesses.
303
304 def wait_outcome(st):
305   """
306   Given a ST from `waitpid' (or similar), return a human-readable outcome.
307   """
308   if OS.WIFSIGNALED(st): return "killed by signal %d" % OS.WTERMSIG(st)
309   elif OS.WIFEXITED(st):
310     rc = OS.WEXITSTATUS(st)
311     if rc: return "failed: rc = %d" % rc
312     else: return "completed successfully"
313   else: return "died with incomprehensible status 0x%04x" % st
314
315 class SubprocessFailure (Exception):
316   """An exception indicating that a subprocess failed."""
317   def __init__(me, what, st):
318     me.st = st
319     me.what = what
320     if OS.WIFEXITED(st): me.rc, me.sig = OS.WEXITSTATUS(st), None
321     elif OS.WIFSIGNALED(st): me.rc, me.sig = None, OS.WTERMSIG(st)
322     else: me.rc, me.sig = None, None
323   def __str__(me):
324     return "subprocess `%s' %s" % (me.what, wait_outcome(me.st))
325
326 INHERIT = Tag('INHERIT')
327 PIPE = Tag('PIPE')
328 DISCARD = Tag('DISCARD')
329 @CTX.contextmanager
330 def subprocess(command,
331                stdin = INHERIT, stdout = INHERIT, stderr = INHERIT,
332                cwd = INHERIT, jobserver = DISCARD):
333   """
334   Hairy context manager for running subprocesses.
335
336   The COMMAND is a list of arguments; COMMAND[0] names the program to be
337   invoked.  (There's currently no way to run a program with an unusual
338   `argv[0]'.)
339
340   The keyword arguments `stdin', `stdout', and `stderr' explain what to do
341   with the standard file descriptors.
342
343     * `INHERIT' means that they should be left alone: the child will use a
344       copy of the parent's descriptor.  This is the default.
345
346     * `DISCARD' means that the descriptor should be re-opened onto
347       `/dev/null' (for reading or writing as appropriate).
348
349     * `PIPE' means that the descriptor should be re-opened as (the read or
350       write end, as appropriate, of) a pipe, and the other end returned to
351       the context body.
352
353   Simiarly, the JOBSERVER may be `INHERIT' to pass the jobserver descriptors
354   and environment variable down to the child, or `DISCARD' to close it.  The
355   default is `DISCARD'.
356
357   The CWD may be `INHERIT' to run the child with the same working directory
358   as the parent, or a pathname to change to an explicitly given working
359   directory.
360
361   The context is returned three values, which are file descriptors for other
362   pipe ends for stdin, stdout, and stderr respectively, or -1 if there is no
363   pipe.
364
365   The context owns the pipe descriptors, and is expected to close them
366   itself.  (Timing of closure is significant, particularly for `stdin'.)
367   """
368
369   ## Set up.
370   r_in, w_in = -1, -1
371   r_out, w_out = -1, -1
372   r_err, w_err = -1, -1
373   spew("running subprocess `%s'" % " ".join(command))
374
375   ## Clean up as necessary...
376   try:
377
378     ## Set up stdin.
379     if stdin is PIPE: r_in, w_in = OS.pipe()
380     elif stdin is DISCARD: r_in = OS.open("/dev/null", OS.O_RDONLY)
381     elif stdin is not INHERIT:
382       raise ValueError("bad `stdin' value `%r'" % stdin)
383
384     ## Set up stdout.
385     if stdout is PIPE: r_out, w_out = OS.pipe()
386     elif stdout is DISCARD: w_out = OS.open("/dev/null", OS.O_WRONLY)
387     elif stdout is not INHERIT:
388       raise ValueError("bad `stderr' value `%r'" % stdout)
389
390     ## Set up stderr.
391     if stderr is PIPE: r_err, w_err = OS.pipe()
392     elif stderr is DISCARD: w_err = OS.open("/dev/null", OS.O_WRONLY)
393     elif stderr is not INHERIT:
394       raise ValueError("bad `stderr' value `%r'" % stderr)
395
396     ## Start up the child.
397     kid = OS.fork()
398
399     if kid == 0:
400       ## Child process.
401
402       ## Fix up stdin.
403       if r_in != -1: OS.dup2(r_in, 0); OS.close(r_in)
404       if w_in != -1: OS.close(w_in)
405
406       ## Fix up stdout.
407       if w_out != -1: OS.dup2(w_out, 1); OS.close(w_out)
408       if r_out != -1: OS.close(r_out)
409
410       ## Fix up stderr.
411       if w_err != -1: OS.dup2(w_err, 2); OS.close(w_err)
412       if r_err != -1: OS.close(r_err)
413
414       ## Change directory.
415       if cwd is not INHERIT: OS.chdir(cwd)
416
417       ## Fix up the jobserver.
418       if jobserver is DISCARD: SCHED.close_jobserver()
419
420       ## Run the program.
421       try: OS.execvp(command[0], command)
422       except OSError, err:
423         moan("failed to run `%s': %s" % err.strerror)
424         OS._exit(127)
425
426     ## Close the other ends of the pipes.
427     if r_in != -1: OS.close(r_in); r_in = -1
428     if w_out != -1: OS.close(w_out); w_out = -1
429     if w_err != -1: OS.close(w_err); w_err = -1
430
431     ## Return control to the context body.  Remember not to close its pipes.
432     yield w_in, r_out, r_err
433     w_in = r_out = r_err = -1
434
435     ## Collect the child process's exit status.
436     _, st = OS.waitpid(kid, 0)
437     spew("subprocess `%s' %s" % (" ".join(command), wait_outcome(st)))
438     if st: raise SubprocessFailure(" ".join(command), st)
439
440   ## Tidy up.
441   finally:
442
443     ## Close any left-over file descriptors.
444     for fd in [r_in, w_in, r_out, w_out, r_err, w_err]:
445       if fd != -1: OS.close(fd)
446
447 def set_nonblocking(fd):
448   """Mark the descriptor FD as non-blocking."""
449   FC.fcntl(fd, FC.F_SETFL, FC.fcntl(fd, FC.F_GETFL) | OS.O_NONBLOCK)
450
451 class DribbleOut (BaseSelector):
452   """A simple selector to feed a string to a descriptor, in pieces."""
453   def __init__(me, fd, string, *args, **kw):
454     super(DribbleOut, me).__init__(*args, **kw)
455     me._fd = fd
456     me._string = string
457     me._i = 0
458     set_nonblocking(me._fd)
459     me.result = None
460   def preselect(me, rfds, wfds):
461     if me._fd != -1: wfds.append(me._fd)
462   def postselect_write(me, fd):
463     if fd != me._fd: return
464     try: n = OS.write(me._fd, me._string)
465     except OSError, err:
466       if err.errno == E.EAGAIN or err.errno == E.EWOULDBLOCK: return
467       elif err.errno == E.EPIPE: OS.close(me._fd); me._fd = -1; return
468       else: raise
469     if n == len(me._string): OS.close(me._fd); me._fd = -1
470     else: me._string = me._string[n:]
471
472 class DribbleIn (BaseSelector):
473   """A simple selector to collect all the input as a big string."""
474   def __init__(me, fd, *args, **kw):
475     super(DribbleIn, me).__init__(*args, **kw)
476     me._fd = fd
477     me._buf = StringIO()
478     set_nonblocking(me._fd)
479   def preselect(me, rfds, wfds):
480     if me._fd != -1: rfds.append(me._fd)
481   def postselect_read(me, fd):
482     if fd != me._fd: return
483     while True:
484       try: buf = OS.read(me._fd, 4096)
485       except OSError, err:
486         if err.errno == E.EAGAIN or err.errno == E.EWOULDBLOCK: break
487         else: raise
488       if buf == "": OS.close(me._fd); me._fd = -1; break
489       else: me._buf.write(buf)
490   @property
491   def result(me): return me._buf.getvalue()
492
493 RETURN = Tag('RETURN')
494 def run_program(command,
495                 stdin = INHERIT, stdout = INHERIT, stderr = INHERIT,
496                 *args, **kwargs):
497   """
498   A simplifying wrapper around `subprocess'.
499
500   The COMMAND is a list of arguments; COMMAND[0] names the program to be
501   invoked, as for `subprocess'.
502
503   The keyword arguments `stdin', `stdout', and `stderr' explain what to do
504   with the standard file descriptors.
505
506     * `INHERIT' means that they should be left alone: the child will use a
507       copy of the parent's descriptor.
508
509     * `DISCARD' means that the descriptor should be re-opened onto
510       `/dev/null' (for reading or writing as appropriate).
511
512     * `RETURN', for an output descriptor, means that all of the output
513       produced on that descriptor should be collected and returned as a
514       string.
515
516     * A string, for stdin, means that the string should be provided on the
517       child's standard input.
518
519   (The value `PIPE' is not permitted here.)
520
521   Other arguments are passed on to `subprocess'.
522
523   If no descriptors are marked `RETURN', then the function returns `None'; if
524   exactly one descriptor is so marked, then the function returns that
525   descriptor's output as a string; otherwise, it returns a tuple of strings
526   for each such descriptor, in the usual order.
527   """
528   kw = dict(); kw.update(kwargs)
529   selfn = []
530
531   if isinstance(stdin, basestring):
532     kw['stdin'] = PIPE; selfn.append(lambda fds: DribbleOut(fds[0], stdin))
533   elif stdin is INHERIT or stdin is DISCARD:
534     kw['stdin'] = stdin
535   else:
536     raise ValueError("bad `stdin' value `%r'" % stdin)
537
538   if stdout is RETURN:
539     kw['stdout'] = PIPE; selfn.append(lambda fds: DribbleIn(fds[1]))
540   elif stdout is INHERIT or stdout is DISCARD:
541     kw['stdout'] = stdout
542   else:
543     raise ValueError("bad `stdout' value `%r'" % stdout)
544
545   if stderr is RETURN:
546     kw['stderr'] = PIPE; selfn.append(lambda fds: DribbleIn(fds[2]))
547   elif stderr is INHERIT or stderr is DISCARD:
548     kw['stderr'] = stderr
549   else:
550     raise ValueError("bad `stderr' value `%r'" % stderr)
551
552   with subprocess(command, *args, **kw) as fds:
553     sel = [fn(fds) for fn in selfn]
554     select_loop(sel)
555   rr = []
556   for s in sel:
557     r = s.result
558     if r is not None: rr.append(r)
559   if len(rr) == 0: return None
560   if len(rr) == 1: return rr[0]
561   else: return tuple(rr)
562
563 ###--------------------------------------------------------------------------
564 ### Other system-ish utilities.
565
566 @CTX.contextmanager
567 def safewrite(path):
568   """
569   Context manager for writing to a file.
570
571   A new file, named `PATH.new', is opened for writing, and the file object
572   provided to the context body.  If the body completes normally, the file is
573   closed and renamed to PATH.  If the body raises an exception, the file is
574   still closed, but not renamed into place.
575   """
576   new = path + ".new"
577   with open(new, "w") as f: yield f
578   OS.rename(new, path)
579
580 @CTX.contextmanager
581 def safewrite_root(path, mode = None, uid = None, gid = None):
582   """
583   Context manager for writing to a file with root privileges.
584
585   This is as for `safewrite', but the file is opened and written as root.
586   """
587   new = path + ".new"
588   with subprocess(C.ROOTLY + ["tee", new],
589                   stdin = PIPE, stdout = DISCARD) as (fd_in, _, _):
590     pipe = OS.fdopen(fd_in, 'w')
591     try: yield pipe
592     finally: pipe.close()
593   if mode is not None: run_program(C.ROOTLY + ["chmod", mode, new])
594   if uid is not None:
595     run_program(C.ROOTLY + ["chown",
596                             uid + (gid is not None and ":" + gid or ""),
597                             new])
598   elif gid is not None:
599     run_program(C.ROOTLY + ["chgrp", gid, new])
600   run_program(C.ROOTLY + ["mv", new, path])
601
602 def mountpoint_p(dir):
603   """Return true if DIR is a mountpoint."""
604
605   ## A mountpoint can be distinguished because it is a directory whose device
606   ## number differs from its parent.
607   try: st1 = OS.stat(dir)
608   except OSError, err:
609     if err.errno == E.ENOENT: return False
610     else: raise
611   if not ST.S_ISDIR(st1.st_mode): return False
612   st0 = OS.stat(OS.path.join(dir, ".."))
613   return st0.st_dev != st1.st_dev
614
615 def mkdir_p(dir, mode = 0777):
616   """
617   Make a directory DIR, and any parents, as necessary.
618
619   Unlike `OS.makedirs', this doesn't fail if DIR already exists.
620   """
621   d = ""
622   for p in dir.split("/"):
623     d = OS.path.join(d, p)
624     if d == "": continue
625     try: OS.mkdir(d, mode)
626     except OSError, err:
627       if err.errno == E.EEXIST: pass
628       else: raise
629
630 def umount(fs):
631   """
632   Unmount the filesystem FS.
633
634   The FS may be the block device holding the filesystem, or (more usually)
635   the mount point.
636   """
637
638   ## Sometimes random things can prevent unmounting.  Be persistent.
639   for i in xrange(5):
640     try: run_program(C.ROOTLY + ["umount", fs], stderr = DISCARD)
641     except SubprocessFailure, err:
642       if err.rc == 32: pass
643       else: raise
644     else: return
645     T.sleep(0.2)
646   run_program(C.ROOTLY + ["umount", fs], stderr = DISCARD)
647
648 @CTX.contextmanager
649 def lockfile(lock, exclp = True, waitp = True):
650   """
651   Acquire an exclusive lock on a named file LOCK while executing the body.
652
653   If WAITP is true, wait until the lock is available; if false, then fail
654   immediately if the lock can't be acquired.
655   """
656   fd = -1
657   flag = 0
658   if exclp: flag |= FC.LOCK_EX
659   else: flag |= FC.LOCK_SH
660   if not waitp: flag |= FC.LOCK_NB
661   spew("acquiring %s lock on `%s'" %
662        (exclp and "exclusive" or "shared", lock))
663   try:
664     while True:
665
666       ## Open the file and take note of which file it is.
667       fd = OS.open(lock, OS.O_RDWR | OS.O_CREAT, 0666)
668       st0 = OS.fstat(fd)
669
670       ## Acquire the lock, waiting if necessary.
671       FC.lockf(fd, flag)
672
673       ## Check that the lock file is still the same one.  It's permissible
674       ## for the lock holder to release the lock by unlinking or renaming the
675       ## lock file, in which case there might be a different lockfile there
676       ## now which we need to acquire instead.
677       ##
678       ## It's tempting to `optimize' this code by opening a new file
679       ## descriptor here so as to elide the additional call to fstat(2)
680       ## above.  But this doesn't work: if we successfully acquire the lock,
681       ## we then have two file descriptors open on the lock file, so we have
682       ## to close one -- but, under the daft fcntl(2) rules, even closing
683       ## `nfd' will release the lock immediately.
684       try:
685         st1 = OS.stat(lock)
686       except OSError, err:
687         if err.errno == E.ENOENT: pass
688         else: raise
689       if st0.st_dev == st1.st_dev and st0.st_ino == st1.st_ino: break
690       OS.close(fd)
691
692     ## We have the lock, so away we go.
693     spew("lock `%s' acquired" % lock)
694     yield None
695     spew("lock `%s' released" % lock)
696
697   finally:
698     if fd != -1: OS.close(fd)
699
700 def block_device_p(dev):
701   """Return true if DEV names a block device."""
702   try: st = OS.stat(dev)
703   except OSError, err:
704     if err.errno == E.ENOENT: return False
705     else: raise
706   else: return ST.S_ISBLK(st.st_mode)
707
708 ###--------------------------------------------------------------------------
709 ### Running parallel jobs.
710
711 ## Return codes from `check'
712 SLEEP = Tag('SLEEP')
713 READY = Tag('READY')
714 FAILED = Tag('FAILED')
715 DONE = Tag('DONE')
716
717 class BaseJob (object):
718   """
719   Base class for jobs.
720
721   Subclasses must implement `run' and `_mkname', and probably ought to extend
722   `prepare' and `check'.
723   """
724
725   ## A magic token to prevent sneaky uninterned jobs.
726   _MAGIC = Tag('MAGIC')
727
728   ## A map from job names to objects.
729   _MAP = {}
730
731   ## Number of tail lines of the log to print on failure.
732   LOGLINES = 20
733
734   def __init__(me, _token, *args, **kw):
735     """
736     Initialize a job.
737
738     Jobs are interned!  Don't construct instances (of subclasses) directly:
739     use the `ensure' class method.
740     """
741     assert _token is me._MAGIC
742     super(BaseJob, me).__init__(*args, **kw)
743
744     ## Dependencies on other jobs.
745     me._deps = None
746     me._waiting = set()
747
748     ## Attributes maintained by the JobServer.
749     me.done = False
750     me.started = False
751     me.win = None
752     me._token = None
753     me._known = False
754     me._st = None
755     me._logkid = -1
756     me._logfile = None
757
758   def prepare(me):
759     """
760     Establish any prerequisite jobs.
761
762     Delaying this allows command-line settings to override those chosen by
763     dependent jobs.
764     """
765     pass
766
767   @classmethod
768   def ensure(cls, *args, **kw):
769     """
770     Return the unique job with the given parameters.
771
772     If a matching job already exists, then return it.  Otherwise, create the
773     new job, register it in the table, and notify the scheduler about it.
774     """
775     me = cls(_token = cls._MAGIC, *args, **kw)
776     try:
777       job = cls._MAP[me.name]
778     except KeyError:
779       cls._MAP[me.name] = me
780       SCHED.add(me)
781       return me
782     else:
783       return job
784
785   ## Naming.
786   @property
787   def name(me):
788     """Return the job's name, as calculated by `_mkname'."""
789     try: name = me._name
790     except AttributeError: name = me._name = me._mkname()
791     return name
792
793   ## Subclass responsibilities.
794   def _mkname(me):
795     """
796     Return the job's name.
797
798     By default, this is an unhelpful string which is distinct for every job.
799     Subclasses should normally override this method to return a name as an
800     injective function of the job parameters.
801     """
802     return "%s.%x" % (me.__class__.__name__, id(me))
803
804   def check(me):
805     """
806     Return whether the job is ready to run.
807
808     Returns a pair STATE, REASON.  The REASON is a human-readable string
809     explaining what's going on, or `None' if it's not worth explaining.  The
810     STATE is one of the following.
811
812       * `READY' -- the job can be run at any time.
813
814       * `FAILED' -- the job can't be started.  Usually, this means that some
815         prerequisite job failed, there was some error in the job's
816         parameters, or the environment is unsuitable for the job to run.
817
818       * `DONE' -- the job has nothing to do.  Usually, this means that the
819         thing the job acts on is already up-to-date.  It's bad form to do
820         even minor work in `check'.
821
822       * `SLEEP' -- the job can't be run right now.  It has arranged to be
823         retried if conditions change.  (Spurious wakeups are permitted and
824         must be handled correctly.)
825
826     The default behaviour checks the set of dependencies, as built by the
827     `await' method, and returns `SLEEP' or `FAILED' as appropriate, or
828     `READY' if all the prerequisite jobs have completed successfully.
829     """
830     for job in me._deps:
831       if not job.done:
832         job._waiting.add(me)
833         return SLEEP, "waiting for job `%s'" % job.name
834       elif not job.win and not OPT.ignerr:
835         return FAILED, "dependent on failed job `%s'" % job.name
836     return READY, None
837
838   ## Subclass utilities.
839   def await(me, job):
840     """Make sure that JOB completes before allowing this job to start."""
841     me._deps.add(job)
842
843   def _logtail(me):
844     """
845     Dump the last `LOGLINES' lines of the logfile.
846
847     This is called if the job fails and was being run quietly, to provide the
848     user with some context for the failure.
849     """
850
851     ## Gather blocks from the end of the log until we have enough lines.
852     with open(me._logfile, 'r') as f:
853       nlines = 0
854       bufs = []
855       bufsz = 4096
856       f.seek(0, 2); off = f.tell()
857       spew("start: off = %d" % off)
858       while nlines <= me.LOGLINES and off > 0:
859         off = max(0, off - bufsz)
860         f.seek(off, 0)
861         spew("try at off = %d" % off)
862         buf = f.read(bufsz)
863         nlines += buf.count("\n")
864         spew("now lines = %d" % nlines)
865         bufs.append(buf)
866     buf = ''.join(reversed(bufs))
867
868     ## We probably overshot.  Skip the extra lines from the start.
869     i = 0
870     while nlines > me.LOGLINES: i = buf.index("\n", i) + 1; nlines -= 1
871
872     ## If we ended up trimming the log, print an ellipsis.
873     if off > 0 or i > 0: print "%-*s * [...]" % (TAGWD, me.name)
874
875     ## Print the log tail.
876     lines = buf[i:].split("\n")
877     if lines and lines[-1] == '': lines.pop()
878     for line in lines: print "%-*s %s" % (TAGWD, me.name, line)
879
880 class BaseJobToken (object):
881   """
882   A job token is the authorization for a job to be run.
883
884   Subclasses must implement `recycle' to allow some other job to use the
885   token.
886   """
887   pass
888
889 class TrivialJobToken (BaseJobToken):
890   """
891   A trivial reusable token, for when issuing jobs in parallel without limit.
892
893   There only needs to be one of these.
894   """
895   def recycle(me):
896     spew("no token needed; nothing to recycle")
897 TRIVIAL_TOKEN = TrivialJobToken()
898
899 class JobServerToken (BaseJobToken):
900   """A job token storing a byte from the jobserver pipe."""
901   def __init__(me, char, pipefd, *args, **kw):
902     super(JobServerToken, me).__init__(*args, **kw)
903     me._char = char
904     me._fd = pipefd
905   def recycle(me):
906     spew("returning token to jobserver pipe")
907     OS.write(me._fd, me._char)
908
909 class PrivateJobToken (BaseJobToken):
910   """
911   The private job token belonging to a scheduler.
912
913   When running under a GNU Make jobserver, there is a token for each byte in
914   the pipe, and an additional one which represents the slot we're actually
915   running in.  This class represents that additional token.
916   """
917   def __init__(me, sched, *args, **kw):
918     super(PrivateJobToken, me).__init__(*args, **kw)
919     me._sched = sched
920   def recycle(me):
921     assert me._sched._privtoken is None
922     spew("recycling private token")
923     me._sched._privtoken = me
924
925 TAGWD = 29
926 LOGKEEP = 20
927
928 class JobScheduler (object):
929   """
930   The main machinery for running and ordering jobs.
931
932   This handles all of the details of job scheduling.
933   """
934
935   def __init__(me, rfd = -1, wfd = -1, npar = 1):
936     """
937     Initialize a scheduler.
938
939       * RFD and WFD are the read and write ends of the jobserver pipe, as
940         determined from the `MAKEFLAGS' environment variable, or -1.
941
942       * NPAR is the maximum number of jobs to run in parallel, or `True' if
943         there is no maximum (i.e., we're in `forkbomb' mode).
944     """
945
946     ## Set the parallelism state.  The `_rfd' and `_wfd' are the read and
947     ## write ends of the jobserver pipe, or -1 if there is no jobserver.
948     ## `_par' is true if we're meant to run jobs in parallel.  The case _par
949     ## and _rfd = -1 means unconstrained parallelism.
950     ##
951     ## The jobserver pipe contains a byte for each shared job slot.  A
952     ## scheduler reads a byte from the pipe for each job it wants to run
953     ## (nearly -- see `_privtoken' below), and puts the byte back when the
954     ## job finishes.  The GNU Make jobserver protocol specification insists
955     ## that we preserve the value of the byte in the pipe (though doesn't
956     ## currently make any use of this flexibility), so we record it in a
957     ## `JobToken' object's `_char' attribute.
958     me._par = rfd != -1 or npar is True or npar != 1
959     spew("par is %r" % me._par)
960     if rfd == -1 and npar > 1:
961       rfd, wfd = OS.pipe()
962       OS.write(wfd, (npar - 1)*'+')
963       OS.environ["MAKEFLAGS"] = \
964         (" -j --jobserver-auth=%(rfd)d,%(wfd)d " +
965          "--jobserver-fds=%(rfd)d,%(wfd)d") % dict(rfd = rfd, wfd = wfd)
966     me._rfd = rfd; me._wfd = wfd
967
968     ## The scheduler state.  A job starts in the `_check' list.  Each
969     ## iteration of the scheduler loop will inspect the jobs here and see
970     ## whether it's ready to run: if not, it gets put in the `_sleep' list,
971     ## where it will languish until something moves it back; if it is ready,
972     ## it gets moved to the `_ready' list to wait for a token from the
973     ## jobserver.  At that point the job can be started, and it moves to the
974     ## `_kidmap', which associates a process-id with each running job.
975     ## Finally, jobs which have completed are simply forgotten.  The `_njobs'
976     ## counter keeps track of how many jobs are outstanding, so that we can
977     ## stop when there are none left.
978     me._check = set()
979     me._sleep = set()
980     me._ready = set()
981     me._kidmap = {}
982     me._logkidmap = {}
983     me._njobs = 0
984
985     ## As well as the jobserver pipe, we implicitly have one extra job slot,
986     ## which is the one we took when we were started by our parent.  The
987     ## right to do processing in this slot is represnted by the `private
988     ## token' here, distinguished from tokens from the jobserver pipe by
989     ## having `None' as its `_char' value.
990     me._privtoken = PrivateJobToken(me)
991
992   def add(me, job):
993     """Notice a new job and arrange for it to (try to) run."""
994     if job._known: return
995     spew("adding new job `%s'" % job.name)
996     job._known = True
997     me._check.add(job)
998     me._njobs += 1
999
1000   def close_jobserver(me):
1001     """
1002     Close the jobserver file descriptors.
1003
1004     This should be called within child processes to prevent them from messing
1005     with the jobserver.
1006     """
1007     if me._rfd != -1: OS.close(me._rfd); me._rfd = -1
1008     if me._wfd != -1: OS.close(me._wfd); me._wfd = -1
1009     try: del OS.environ["MAKEFLAGS"]
1010     except KeyError: pass
1011
1012   def _killall(me):
1013     """Zap all jobs which aren't yet running."""
1014     for jobset in [me._sleep, me._check, me._ready]:
1015       while jobset:
1016         job = jobset.pop()
1017         job.done = True
1018         job.win = False
1019         me._njobs -= 1
1020
1021   def _retire(me, job, win, outcome):
1022     """
1023     Declare that a job has stopped, and deal with the consequences.
1024
1025     JOB is the completed job, which should not be on any of the job queues.
1026     WIN is true if the job succeeded, and false otherwise.  OUTCOME is a
1027     human-readable string explaining how the job came to its end, or `None'
1028     if no message should be reported.
1029     """
1030
1031     global RC
1032
1033     ## Return the job's token to the pool.
1034     if job._token is not None: job._token.recycle()
1035     job._token = None
1036     me._njobs -= 1
1037
1038     ## Update and maybe report the job's status.
1039     job.done = True
1040     job.win = win
1041     if outcome is not None and not OPT.silent:
1042       if OPT.quiet and not job.win and job._logfile: job._logtail()
1043       if not job.win or not OPT.quiet:
1044         print "%-*s %c (%s)" % \
1045           (TAGWD, job.name, job.win and '|' or '*', outcome)
1046
1047     ## If the job failed, and we care, arrange to exit nonzero.
1048     if not win and not OPT.ignerr: RC = 2
1049
1050     ## If the job failed, and we're supposed to give up after the first
1051     ## error, then zap all of the waiting jobs.
1052     if not job.win and not OPT.keepon and not OPT.ignerr: me._killall()
1053
1054     ## If this job has dependents then wake them up and see whether they're
1055     ## ready to run.
1056     for j in job._waiting:
1057       try: me._sleep.remove(j)
1058       except KeyError: pass
1059       else:
1060         spew("waking dependent job `%s'" % j.name)
1061         me._check.add(j)
1062
1063   def _reap(me, kid, st):
1064     """
1065     Deal with the child with process-id KID having exited with status ST.
1066     """
1067
1068     ## Figure out what kind of child this is.  Note that it has finished.
1069     try: job = me._kidmap[kid]
1070     except KeyError:
1071       try: job = me._logkidmap[kid]
1072       except KeyError:
1073         spew("unknown child %d exits with status 0x%04x" % (kid, st))
1074         return
1075       else:
1076         ## It's a logging child.
1077         del me._logkidmap[kid]
1078         job._logkid = DONE
1079         spew("logging process for job `%s' exits with status 0x%04x" %
1080              (job.name, st))
1081     else:
1082       job._st = st
1083       del me._kidmap[kid]
1084       spew("main process for job `%s' exits with status 0x%04x" %
1085            (job.name, st))
1086
1087     ## If either of the job's associated processes is still running then we
1088     ## should stop now and give the other one a chance.
1089     if job._st is None or job._logkid is not DONE:
1090       spew("deferring retirement for job `%s'" % job.name)
1091       return
1092     spew("completing deferred retirement for job `%s'" % job.name)
1093
1094     ## Update and (maybe) report the job status.
1095     if job._st == 0: win = True; outcome = None
1096     else: win = False; outcome = wait_outcome(job._st)
1097
1098     ## Retire the job.
1099     me._retire(job, win, outcome)
1100
1101   def _reapkids(me):
1102     """Reap all finished child processes."""
1103     while True:
1104       try: kid, st = OS.waitpid(-1, OS.WNOHANG)
1105       except OSError, err:
1106         if err.errno == E.ECHILD: break
1107         else: raise
1108       if kid == 0: break
1109       me._reap(kid, st)
1110
1111   def run_job(me, job):
1112     """Start running the JOB."""
1113
1114     job.started = True
1115     if OPT.dryrun: return None, None
1116
1117     ## Make pipes to collect the job's output and error reports.
1118     r_out, w_out = OS.pipe()
1119     r_err, w_err = OS.pipe()
1120
1121     ## Find a log file to write.  Avoid races over the log names; but this
1122     ## means that the log descriptor needs to be handled somewhat carefully.
1123     logdir = OS.path.join(C.STATE, "log"); mkdir_p(logdir)
1124     logseq = 1
1125     while True:
1126       logfile = OS.path.join(logdir, "%s-%s#%d" % (job.name, TODAY, logseq))
1127       try:
1128         logfd = OS.open(logfile, OS.O_WRONLY | OS.O_CREAT | OS.O_EXCL, 0666)
1129       except OSError, err:
1130         if err.errno == E.EEXIST: logseq += 1; continue
1131         else: raise
1132       else:
1133         break
1134     job._logfile = logfile
1135
1136     ## Make sure there's no pending output, or we might get two copies.  (I
1137     ## don't know how to flush all output streams in Python, but this is good
1138     ## enough for our purposes.)
1139     SYS.stdout.flush()
1140
1141     ## Set up the logging child first.  If we can't, take down the whole job.
1142     try: job._logkid = OS.fork()
1143     except OSError, err: OS.close(logfd); return None, err
1144     if not job._logkid:
1145       ## The main logging loop.
1146
1147       ## Close the jobserver descriptors, and the write ends of the pipes.
1148       me.close_jobserver()
1149       OS.close(w_out); OS.close(w_err)
1150
1151       ## Capture the job's stdout and stderr and wait for everything to
1152       ## happen.
1153       def log_lines(fd, marker):
1154         def fn(line):
1155           if not OPT.quiet:
1156             OS.write(1, "%-*s %s %s\n" % (TAGWD, job.name, marker, line))
1157           OS.write(logfd, "%s %s\n" % (marker, line))
1158         return ReadLinesSelector(fd, fn)
1159       select_loop([log_lines(r_out, "|"), log_lines(r_err, "*")])
1160
1161       ## We're done.  (Closing the descriptors here would be like polishing
1162       ## the floors before the building is demolished.)
1163       OS._exit(0)
1164
1165     ## Back in the main process: record the logging child.  At this point we
1166     ## no longer need the logfile descriptor.
1167     me._logkidmap[job._logkid] = job
1168     OS.close(logfd)
1169
1170     ## Start the main job process.
1171     try: kid = OS.fork()
1172     except OSError, err: return None, err
1173     if not kid:
1174       ## The main job.
1175
1176       ## Close the read ends of the pipes, and move the write ends to the
1177       ## right places.  (This will go wrong if we were started without enough
1178       ## descriptors.  Fingers crossed.)
1179       OS.dup2(w_out, 1); OS.dup2(w_err, 2)
1180       OS.close(r_out); OS.close(w_out)
1181       OS.close(r_err); OS.close(w_err)
1182       spew("running job `%s' as pid %d" % (job.name, OS.getpid()))
1183
1184       ## Run the job, catching nonlocal flow.
1185       try:
1186         job.run()
1187       except ExpectedError, err:
1188         moan(str(err))
1189         OS._exit(2)
1190       except Exception, err:
1191         TB.print_exc(SYS.stderr)
1192         OS._exit(3)
1193       except BaseException, err:
1194         moan("caught unexpected exception: %r" % err)
1195         OS._exit(112)
1196       else:
1197         spew("job `%s' ran to completion" % job.name)
1198
1199         ## Clean up old logs.
1200         match = []
1201         pat = RX.compile(r"^%s-(\d{4})-(\d{2})-(\d{2})\#(\d+)$" %
1202                          RX.escape(job.name))
1203         for f in OS.listdir(logdir):
1204           m = pat.match(f)
1205           if m: match.append((f, int(m.group(1)), int(m.group(2)),
1206                               int(m.group(3)), int(m.group(4))))
1207         match.sort(key = lambda (_, y, m, d, q): (y, m, d, q))
1208         if len(match) > LOGKEEP:
1209           for (f, _, _, _, _) in match[:-LOGKEEP]:
1210             try: OS.unlink(OS.path.join(logdir, f))
1211             except OSError, err:
1212               if err.errno == E.ENOENT: pass
1213               else: raise
1214
1215         ## All done.
1216         OS._exit(0)
1217
1218     ## Back in the main process: close both the pipes and return the child
1219     ## process.
1220     OS.close(r_out); OS.close(w_out)
1221     OS.close(r_err); OS.close(w_err)
1222     if OPT.quiet: print "%-*s | (started)" % (TAGWD, job.name)
1223     return kid, None
1224
1225   def run(me):
1226     """Run the scheduler."""
1227
1228     spew("JobScheduler starts")
1229
1230     while True:
1231       ## The main scheduler loop.  We go through three main phases:
1232       ##
1233       ##   * Inspect the jobs in the `check' list to see whether they can
1234       ##     run.  After this, the `check' list will be empty.
1235       ##
1236       ##   * If there are running jobs, check to see whether any of them have
1237       ##     stopped, and deal with the results.  Also, if there are jobs
1238       ##     ready to start and a job token has become available, then
1239       ##     retrieve the token.  (Doing these at the same time is the tricky
1240       ##     part.)
1241       ##
1242       ##   * If there is a job ready to run, and we retrieved a token, then
1243       ##     start running the job.
1244
1245       ## Check the pending jobs to see if they can make progress: run each
1246       ## job's `check' method and move it to the appropriate queue.  (It's OK
1247       ## if `check' methods add more jobs to the list, as long as things
1248       ## settle down eventually.)
1249       while True:
1250         try: job = me._check.pop()
1251         except KeyError: break
1252         if job._deps is None:
1253           job._deps = set()
1254           job.prepare()
1255         state, reason = job.check()
1256         tail = reason is not None and ": %s" % reason or ""
1257         if state == READY:
1258           spew("job `%s' ready to run%s" % (job.name, tail))
1259           me._ready.add(job)
1260         elif state is FAILED:
1261           spew("job `%s' refused to run%s" % (job.name, tail))
1262           me._retire(job, False, "refused to run%s" % tail)
1263         elif state is DONE:
1264           spew("job `%s' has nothing to do%s" % (job.name, tail))
1265           me._retire(job, True, reason)
1266         elif state is SLEEP:
1267           spew("job `%s' can't run yet%s" % (job.name, tail))
1268           me._sleep.add(job)
1269         else:
1270           raise ValueError("unexpected job check from `%s': %r, %r" %
1271                            (job.name, state, reason))
1272
1273       ## If there are no jobs left, then we're done.
1274       if not me._njobs:
1275         spew("all jobs completed")
1276         break
1277
1278       ## Make sure we can make progress.  There are no jobs on the check list
1279       ## any more, because we just cleared it.  We assume that jobs which are
1280       ## ready to run will eventually receive a token.  So we only end up in
1281       ## trouble if there are jobs asleep, but none running or ready to run.
1282       ##spew("#jobs = %d" % me._njobs)
1283       ##spew("sleeping: %s" % ", ".join([j.name for j in me._sleep]))
1284       ##spew("ready: %s" % ", ".join([j.name for j in me._ready]))
1285       ##spew("running: %s" % ", ".join([j.name for j in me._kidmap.itervalues()]))
1286       assert not me._sleep or me._kidmap or me._logkidmap or me._ready
1287
1288       ## Wait for something to happen.
1289       if not me._ready or (not me._par and me._privtoken is None):
1290         ## If we have no jobs ready to run, then we must wait for an existing
1291         ## child to exit.  Hopefully, a sleeping job will be able to make
1292         ## progress after this.
1293         ##
1294         ## Alternatively, if we're not supposed to be running jobs in
1295         ## parallel and we don't have the private token, then we have no
1296         ## choice but to wait for the running job to complete.
1297         ##
1298         ## There's no check here for `ECHILD'.  We really shouldn't be here
1299         ## if there are no children to wait for.  (The check list must be
1300         ## empty because we just drained it.  If the ready list is empty,
1301         ## then all of the jobs must be running or sleeping; but the
1302         ## assertion above means that either there are no jobs at all, in
1303         ## which case we should have stopped, or at least one is running, in
1304         ## which case it's safe to wait for it.  The other case is that we're
1305         ## running jobs sequentially, and one is currently running, so
1306         ## there's nothing for it but to wait for it -- and hope that it will
1307         ## wake up one of the sleeping jobs.  The remaining possibility is
1308         ## that we've miscounted somewhere, which will cause a crash.)
1309         if not me._ready:
1310           spew("no new jobs ready: waiting for outstanding jobs to complete")
1311         else:
1312           spew("job running without parallelism: waiting for it to finish")
1313         kid, st = OS.waitpid(-1, 0)
1314         me._reap(kid, st)
1315         me._reapkids()
1316         continue
1317
1318       ## We have jobs ready to run, so try to acquire a token.
1319       if me._rfd == -1 and me._par:
1320         ## We're running with unlimited parallelism, so we don't need a token
1321         ## to run a job.
1322         spew("running new job without token")
1323         token = TRIVIAL_TOKEN
1324       elif me._privtoken:
1325         ## Our private token is available, so we can use that to start
1326         ## a new job.
1327         spew("private token available: assigning to new job")
1328         token = me._privtoken
1329         me._privtoken = None
1330       else:
1331         ## We have to read from the jobserver pipe.  Unfortunately, we're not
1332         ## allowed to set the pipe nonblocking, because make is also using it
1333         ## and will get into a serious mess.  And we must deal with `SIGCHLD'
1334         ## arriving at any moment.  We use the same approach as GNU Make.  We
1335         ## start by making a copy of the jobserver descriptor: it's this
1336         ## descriptor we actually try to read from.  We set a signal handler
1337         ## to close this descriptor if a child exits.  And we try one last
1338         ## time to reap any children which have exited just before we try
1339         ## reading the jobserver pipe.  This way we're covered:
1340         ##
1341         ##   * If a child exits during the main loop, before we establish the
1342         ##     descriptor copy then we'll notice when we try reaping
1343         ##     children.
1344         ##
1345         ##   * If a child exits between the last-chance reap and the read,
1346         ##     the signal handler will close the descriptor and the `read'
1347         ##     call will fail with `EBADF'.
1348         ##
1349         ##   * If a child exits while we're inside the `read' system call,
1350         ##     then the syscall will fail with `EINTR'.
1351         ##
1352         ## The only problem is that we can't do this from Python, because
1353         ## Python signal handlers are delayed.  This is what the `jobclient'
1354         ## module is for.
1355         ##
1356         ## The `jobclient' function is called as
1357         ##
1358         ##      jobclient(FD)
1359         ##
1360         ## It returns a tuple of three values: TOKEN, PID, STATUS.  If TOKEN
1361         ## is not `None', then reading the pipe succeeded; if TOKEN is empty,
1362         ## then the pipe returned EOF, so we should abort; otherwise, TOKEN
1363         ## is a singleton string holding the token character.  If PID is not
1364         ## `None', then PID is the process id of a child which exited, and
1365         ## STATUS is its exit status.
1366         spew("waiting for token from jobserver")
1367         tokch, kid, st = JC.jobclient(me._rfd)
1368
1369         if kid is not None:
1370           me._reap(kid, st)
1371           me._reapkids()
1372         if tokch is None:
1373           spew("no token; trying again")
1374           continue
1375         elif token == '':
1376           error("jobserver pipe closed; giving up")
1377           me._killall()
1378           continue
1379         spew("received token from jobserver")
1380         token = JobServerToken(tokch, me._wfd)
1381
1382       ## We have a token, so we should start up the job.
1383       job = me._ready.pop()
1384       job._token = token
1385       spew("start new job `%s'" % job.name)
1386       kid, err = me.run_job(job)
1387       if err is not None:
1388         me._retire(job, False, "failed to fork: %s" % err)
1389         continue
1390       if kid is None: me._retire(job, True, "dry run")
1391       else: me._kidmap[kid] = job
1392
1393     ## We ran out of work to do.
1394     spew("JobScheduler done")
1395
1396 ###--------------------------------------------------------------------------
1397 ### Configuration.
1398
1399 R_CONFIG = RX.compile(r"^([a-zA-Z0-9_]+)='(.*)'$")
1400
1401 class Config (object):
1402
1403   def _conv_str(s): return s
1404   def _conv_list(s): return s.split()
1405   def _conv_set(s): return set(s.split())
1406
1407   _CONVERT = {
1408     "ROOTLY": _conv_list,
1409     "DISTS": _conv_set,
1410     "MYARCH": _conv_set,
1411     "NATIVE_ARCHS": _conv_set,
1412     "FOREIGN_ARCHS": _conv_set,
1413     "FOREIGN_GNUARCHS": _conv_list,
1414     "ALL_ARCHS": _conv_set,
1415     "NATIVE_CHROOTS": _conv_set,
1416     "FOREIGN_CHROOTS": _conv_set,
1417     "ALL_CHROOTS": _conv_set,
1418     "BASE_PACKAGES": _conv_list,
1419     "EXTRA_PACKAGES": _conv_list,
1420     "CROSS_PACKAGES": _conv_list,
1421     "CROSS_PATHS": _conv_list,
1422     "APTCONF": _conv_list,
1423     "LOCALPKGS": _conv_list,
1424     "SCHROOT_COPYFILES": _conv_list,
1425     "SCHROOT_NSSDATABASES": _conv_list
1426   }
1427
1428   _CONV_MAP = {
1429     "*_APTCONFSRC": ("APTCONFSRC", _conv_str),
1430     "*_DEPS": ("PKGDEPS", _conv_list),
1431     "*_QEMUHOST": ("QEMUHOST", _conv_str),
1432     "*_QEMUARCH": ("QEMUARCH", _conv_str),
1433     "*_ALIASES": ("DISTALIAS", _conv_str)
1434   }
1435
1436   _conv_str = staticmethod(_conv_str)
1437   _conv_list = staticmethod(_conv_list)
1438   _conv_set = staticmethod(_conv_set)
1439
1440   def __init__(me):
1441     raw = r"""
1442     """; raw = open('state/config.sh').read(); _ignore = """ @@@config@@@
1443     """
1444     me._conf = {}
1445     for line in raw.split("\n"):
1446       line = line.strip()
1447       if not line or line.startswith('#'): continue
1448       m = R_CONFIG.match(line)
1449       if not m: raise ExpectedError("bad config line `%s'" % line)
1450       k, v = m.group(1), m.group(2).replace("'\\''", "'")
1451       d = me._conf
1452       try: conv = me._CONVERT[k]
1453       except KeyError:
1454         i = 0
1455         while True:
1456           try: i = k.index("_", i + 1)
1457           except ValueError: conv = me._conv_str; break
1458           try: map, conv = me._CONV_MAP["*" + k[i:]]
1459           except KeyError: pass
1460           else:
1461             d = me._conf.setdefault(map, dict())
1462             k = k[:i]
1463             if k.startswith("_"): k = k[1:]
1464             break
1465       d[k] = conv(v)
1466
1467   def __getattr__(me, attr):
1468     try: return me._conf[attr]
1469     except KeyError, err: raise AttributeError(err.args[0])
1470
1471 with toplevel_handler(): C = Config()
1472
1473 ###--------------------------------------------------------------------------
1474 ### Chroot maintenance utilities.
1475
1476 CREATE = Tag("CREATE")
1477 FORCE = Tag("FORCE")
1478
1479 def check_fresh(fresh, update):
1480   """
1481   Compare a refresh mode FRESH against an UPDATE time.
1482
1483   Return a (STATUS, REASON) pair, suitable for returning from a job `check'
1484   method.
1485
1486   The FRESH argument may be one of the following:
1487
1488     * `CREATE' is satisfied if the thing exists at all: it returns `READY' if
1489       the thing doesn't yet exist (UPDATE is `None'), or `DONE' otherwise.
1490
1491     * `FORCE' is never satisfied: it always returns `READY'.
1492
1493     * an integer N is satisfied if UPDATE time is at most N seconds earlier
1494       than the present: if returns `READY' if the UPDATE is too old, or
1495       `DONE' otherwise.
1496   """
1497   if update is None: return READY, "must create"
1498   elif fresh is FORCE: return READY, "update forced"
1499   elif fresh is CREATE: return DONE, "already created"
1500   elif NOW - unzulu(update) > fresh: return READY, "too stale: updating"
1501   else: return DONE, "already sufficiently up-to-date"
1502
1503 def lockfile_path(file):
1504   """
1505   Return the full path for a lockfile named FILE.
1506
1507   Create the lock directory if necessary.
1508   """
1509   lockdir = OS.path.join(C.STATE, "lock"); mkdir_p(lockdir)
1510   return OS.path.join(lockdir, file)
1511
1512 def chroot_src_lockfile(dist, arch):
1513   """
1514   Return the lockfile for the source-chroot for DIST on ARCH.
1515
1516   It is not allowed to acquire a source-chroot lock while holding any other
1517   locks.
1518   """
1519   return lockfile_path("source.%s-%s" % (dist, arch))
1520
1521 def chroot_src_lv(dist, arch):
1522   """
1523   Return the logical volume name for the source-chroot for DIST on ARCH.
1524   """
1525   return "%s%s-%s" % (C.LVPREFIX, dist, arch)
1526
1527 def chroot_src_blkdev(dist, arch):
1528   """
1529   Return the block-device name for the source-chroot for DIST on ARCH.
1530   """
1531   return OS.path.join("/dev", C.VG, chroot_src_lv(dist, arch))
1532
1533 def chroot_src_mntpt(dist, arch):
1534   """
1535   Return mountpoint path for setting up the source-chroot for DIST on ARCH.
1536
1537   Note that this is not the mountpoint that schroot(1) uses.
1538   """
1539   mnt = OS.path.join(C.STATE, "mnt", "%s-%s" % (dist, arch))
1540   mkdir_p(mnt)
1541   return mnt
1542
1543 def chroot_session_mntpt(session):
1544   """Return the mountpoint for an schroot session."""
1545   return OS.path.join("/schroot", session)
1546
1547 def crosstools_lockfile(dist, arch):
1548   """
1549   Return the lockfile for the cross-build tools for DIST, hosted by ARCH.
1550
1551   When locking multiple cross-build tools, you must acquire the locks in
1552   lexicographically ascending order.
1553   """
1554   return lockfile_path("cross-tools.%s-%s" % (dist, arch))
1555
1556 def switch_prefix(string, map):
1557   """
1558   Replace the prefix of a STRING, according to the given MAP.
1559
1560   MAP is a sequence of (OLD, NEW) pairs.  For each such pair in turn, test
1561   whether STRING starts with OLD: if so, return STRING, but with the prefix
1562   OLD replaced by NEW.  If no OLD prefix matches, then raise a `ValueError'.
1563   """
1564   for old, new in map:
1565     if string.startswith(old): return new + string[len(old):]
1566   raise ValueError("expected `%s' to start with one of %s" %
1567                    ", ".join(["`%s'" % old for old, new in map]))
1568
1569 def host_to_chroot(path):
1570   """
1571   Convert a host path under `C.LOCAL' to the corresponding chroot path under
1572   `/usr/local.schroot'.
1573   """
1574   return switch_prefix(path, [(C.LOCAL + "/", "/usr/local.schroot/")])
1575
1576 def chroot_to_host(path):
1577   """
1578   Convert a chroot path under `/usr/local.schroot' to the corresponding
1579   host path under `C.LOCAL'.
1580   """
1581   return switch_prefix(path, [("/usr/local.schroot/", C.LOCAL + "/")])
1582
1583 def split_dist_arch(spec):
1584   """Split a SPEC of the form `DIST-ARCH' into the pair (DIST, ARCH)."""
1585   dash = spec.index("-")
1586   return spec[:dash], spec[dash + 1:]
1587
1588 def elf_binary_p(arch, path):
1589   """Return whether PATH is an ELF binary for ARCH."""
1590   if not OS.path.isfile(path): return False
1591   with open(path, 'rb') as f: magic = f.read(20)
1592   if magic[0:4] != "\x7fELF": return False
1593   if magic[8:16] != 8*"\0": return False
1594   if arch == "i386":
1595     if magic[4:7] != "\x01\x01\x01": return False
1596     if magic[18:20] != "\x03\x00": return False
1597   elif arch == "amd64":
1598     if magic[4:7] != "\x02\x01\x01": return False
1599     if magic[18:20] != "\x3e\x00": return False
1600   else:
1601     raise ValueError("unsupported donor architecture `%s'" % arch)
1602   return True
1603
1604 def progress(msg):
1605   """
1606   Print a progress message MSG.
1607
1608   This is intended to be called within a job's `run' method, so it doesn't
1609   check `OPT.quiet' or `OPT.silent'.
1610   """
1611   OS.write(1, ";; %s\n" % msg)
1612
1613 class NoSuchChroot (Exception):
1614   """
1615   Exception indicating that a chroot does not exist.
1616
1617   Specifically, it means that it doesn't even have a logical volume.
1618   """
1619   def __init__(me, dist, arch):
1620     me.dist = dist
1621     me.arch = arch
1622   def __str__(me):
1623     return "chroot for `%s' on `%s' not found" % (me.dist, me.arch)
1624
1625 @CTX.contextmanager
1626 def mount_chroot_src(dist, arch):
1627   """
1628   Context manager for mounting the source-chroot for DIST on ARCH.
1629
1630   The context manager automatically unmounts the filesystem again when the
1631   body exits.  You must hold the appropriate source-chroot lock before
1632   calling this routine.
1633   """
1634   dev = chroot_src_blkdev(dist, arch)
1635   if not block_device_p(dev): raise NoSuchChroot(dist, arch)
1636   mnt = chroot_src_mntpt(dist, arch)
1637   try:
1638     run_program(C.ROOTLY + ["mount", dev, mnt])
1639     yield mnt
1640   finally:
1641     umount(mnt)
1642
1643 @CTX.contextmanager
1644 def chroot_session(dist, arch, sourcep = False):
1645   """
1646   Context manager for running an schroot(1) session.
1647
1648   Returns the (ugly, automatically generated) session name to the context
1649   body.  By default, a snapshot session is started: set SOURCEP true to start
1650   a source-chroot session.  You must hold the appropriate source-chroot lock
1651   before starting a source-chroot session.
1652
1653   The context manager automatically closes the session again when the body
1654   exits.
1655   """
1656   chroot = chroot_src_lv(dist, arch)
1657   if sourcep: chroot = "source:" + chroot
1658   session = run_program(["schroot", "-uroot", "-b", "-c", chroot],
1659                         stdout = RETURN).rstrip("\n")
1660   try:
1661     root = OS.path.join(chroot_session_mntpt(session), "fs")
1662     yield session, root
1663   finally:
1664     run_program(["schroot", "-e", "-c", session])
1665
1666 def run_root(command, **kw):
1667   """Run a COMMAND as root.  Arguments are as for `run_program'."""
1668   return run_program(C.ROOTLY + command, **kw)
1669
1670 def run_schroot_session(session, command, rootp = False, **kw):
1671   """
1672   Run a COMMAND within an schroot(1) session.
1673
1674   Arguments are as for `run_program'.
1675   """
1676   if rootp:
1677     return run_program(["schroot", "-uroot", "-r",
1678                         "-c", session, "--"] + command, **kw)
1679   else:
1680     return run_program(["schroot", "-r",
1681                         "-c", session, "--"] + command, **kw)
1682
1683 def run_schroot_source(dist, arch, command, **kw):
1684   """
1685   Run a COMMAND through schroot(1), in the source-chroot for DIST on ARCH.
1686
1687   Arguments are as for `run_program'.  You must hold the appropriate source-
1688   chroot lock before calling this routine.
1689   """
1690   return run_program(["schroot", "-uroot",
1691                       "-c", "source:%s" % chroot_src_lv(dist, arch),
1692                       "--"] + command, **kw)
1693
1694 ###--------------------------------------------------------------------------
1695 ### Metadata files.
1696
1697 class MetadataClass (type):
1698   """
1699   Metaclass for metadata classes.
1700
1701   Notice a `VARS' attribute in the class dictionary, and augment it with a
1702   `_VARSET' attribute, constructed as a set containing the same items.  (We
1703   need them both: the set satisfies fast lookups, while the original sequence
1704   remembers the ordering.)
1705   """
1706   def __new__(me, name, supers, dict):
1707     try: vars = dict['VARS']
1708     except KeyError: pass
1709     else: dict['_VARSET'] = set(vars)
1710     return super(MetadataClass, me).__new__(me, name, supers, dict)
1711
1712 class BaseMetadata (object):
1713   """
1714   Base class for metadate objects.
1715
1716   Metadata bundles are simple collections of key/value pairs.  Keys should
1717   usually be Python identifiers because they're used to name attributes.
1718   Values are strings, but shouldn't have leading or trailing whitespace, and
1719   can't contain newlines.
1720
1721   Metadata bundles are written to files.  The format is simple enough: empty
1722   lines and lines starting with `#' are ignored; otherwise, the line must
1723   have the form
1724
1725         KEY = VALUE
1726
1727   where KEY does not contain `='; spaces around the `=' are optional, and
1728   spaces around the KEY and VALUE are stripped.  The order of keys is
1729   unimportant; keys are always written in a standard order on output.
1730   """
1731   __metaclass__ = MetadataClass
1732
1733   def __init__(me, **kw):
1734     """Initialize a metadata bundle from keyword arguments."""
1735     for k, v in kw.iteritems():
1736       setattr(me, k, v)
1737     for v in me.VARS:
1738       try: getattr(me, v)
1739       except AttributeError: setattr(me, v, None)
1740
1741   def __setattr__(me, attr, value):
1742     """
1743     Try to set an attribute.
1744
1745     Only attribute names listed in the `VARS' class attribute are permitted.
1746     """
1747     if attr not in me._VARSET: raise AttributeError, attr
1748     super(BaseMetadata, me).__setattr__(attr, value)
1749
1750   @classmethod
1751   def read(cls, path):
1752     """Return a new metadata bundle read from a named PATH."""
1753     map = {}
1754     with open(path) as f:
1755       for line in f:
1756         line = line.strip()
1757         if line == "" or line.startswith("#"): continue
1758         k, v = line.split("=", 1)
1759         map[k.strip()] = v.strip()
1760     return cls(**map)
1761
1762   def _write(me, file):
1763     """
1764     Write the metadata bundle to the FILE (a file-like object).
1765
1766     This is intended for use by subclasses which want to override the default
1767     I/O behaviour of the main `write' method.
1768     """
1769     file.write("### -*-conf-*-\n")
1770     for k in me.VARS:
1771       try: v = getattr(me, k)
1772       except AttributeError: pass
1773       else:
1774         if v is not None: file.write("%s = %s\n" % (k, v))
1775
1776   def write(me, path):
1777     """
1778     Write the metadata bundle to a given PATH.
1779
1780     The file is replaced atomically.
1781     """
1782     with safewrite(path) as f: me._write(f)
1783
1784   def __repr__(me):
1785     return "#<%s: %s>" % (me.__class__.__name__,
1786                           ", ".join("%s=%r" % (k, getattr(me, k, None))
1787                                     for k in me.VARS))
1788
1789 class ChrootMetadata (BaseMetadata):
1790   VARS = ['dist', 'arch', 'update']
1791
1792   @classmethod
1793   def read(cls, dist, arch):
1794     try:
1795       with lockfile(chroot_src_lockfile(dist, arch), exclp = False):
1796         with mount_chroot_src(dist, arch) as mnt:
1797           return super(ChrootMetadata, cls).read(OS.path.join(mnt, "META"))
1798     except IOError, err:
1799       if err.errno == E.ENOENT: pass
1800       else: raise
1801     except NoSuchChroot: pass
1802     return cls(dist = dist, arch = arch)
1803
1804   def write(me):
1805     with mount_chroot_src(me.dist, me.arch) as mnt:
1806       with safewrite_root(OS.path.join(mnt, "META")) as f:
1807         me._write(f)
1808
1809 class CrossToolsMetadata (BaseMetadata):
1810   VARS = ['dist', 'arch', 'update']
1811
1812   @classmethod
1813   def read(cls, dist, arch):
1814     try:
1815       return super(CrossToolsMetadata, cls)\
1816         .read(OS.path.join(C.LOCAL, "cross", "%s-%s" % (dist, arch), "META"))
1817     except IOError, err:
1818       if err.errno == E.ENOENT: pass
1819       else: raise
1820     return cls(dist = dist, arch = arch)
1821
1822   def write(me, dir = None):
1823     if dir is None:
1824       dir = OS.path.join(C.LOCAL, "cross", "%s-%s" % (me.dist, me.arch))
1825     with safewrite_root(OS.path.join(dir, "META")) as f:
1826       me._write(f)
1827
1828 ###--------------------------------------------------------------------------
1829 ### Constructing a chroot.
1830
1831 R_DIVERT = RX.compile(r"^diversion of (.*) to .* by install-cross-tools$")
1832
1833 class ChrootJob (BaseJob):
1834   """
1835   Create or update a chroot.
1836   """
1837
1838   SPECS = C.ALL_CHROOTS
1839
1840   def __init__(me, spec, fresh = CREATE, *args, **kw):
1841     super(ChrootJob, me).__init__(*args, **kw)
1842     me._dist, me._arch = split_dist_arch(spec)
1843     me._fresh = fresh
1844     me._meta = ChrootMetadata.read(me._dist, me._arch)
1845     me._tools_chroot = me._qemu_chroot = None
1846
1847   def _mkname(me): return "chroot.%s-%s" % (me._dist, me._arch)
1848
1849   def prepare(me):
1850     if me._arch in C.FOREIGN_ARCHS:
1851       me._tools_chroot = CrossToolsJob.ensure\
1852         ("%s-%s" % (me._dist, C.TOOLSARCH), FRESH)
1853       me._qemu_chroot = CrossToolsJob.ensure\
1854         ("%s-%s" % (me._dist, C.QEMUHOST[me._arch]), FRESH)
1855       me.await(me._tools_chroot)
1856       me.await(me._qemu_chroot)
1857
1858   def check(me):
1859     status, reason = super(ChrootJob, me).check()
1860     if status is not READY: return status, reason
1861     if (me._tools_chroot is not None and me._tools_chroot.started) or \
1862        (me._qemu_chroot is not None and me._qemu_chroot.started):
1863       return READY, "prerequisites run"
1864     return check_fresh(me._fresh, me._meta.update)
1865
1866   def _install_cross_tools(me):
1867     """
1868     Install or refresh cross-tools in the source-chroot.
1869
1870     This function version assumes that the source-chroot lock is already
1871     held.
1872
1873     Note that there isn't a job class corresponding to this function.  It's
1874     done automatically as part of source-chroot setup and update for foreign
1875     architectures.
1876     """
1877     with Cleanup() as clean:
1878
1879       dist, arch = me._dist, me._arch
1880
1881       mymulti = run_program(["dpkg-architecture", "-a", C.TOOLSARCH,
1882                              "-qDEB_HOST_MULTIARCH"],
1883                             stdout = RETURN).rstrip("\n")
1884       gnuarch = run_program(["dpkg-architecture", "-A", arch,
1885                              "-qDEB_TARGET_GNU_TYPE"],
1886                             stdout = RETURN).rstrip("\n")
1887
1888       crossdir = OS.path.join(C.LOCAL, "cross",
1889                               "%s-%s" % (dist, C.TOOLSARCH))
1890
1891       qarch, qhost = C.QEMUARCH[arch], C.QEMUHOST[arch]
1892       qemudir = OS.path.join(C.LOCAL, "cross",
1893                              "%s-%s" % (dist, qhost), "QEMU")
1894
1895       ## Acquire lockfiles in a canonical order to prevent deadlocks.
1896       donors = [C.TOOLSARCH]
1897       if qarch != C.TOOLSARCH: donors.append(qarch)
1898       donors.sort()
1899       for a in donors:
1900         clean.enter(lockfile(crosstools_lockfile(dist, a), exclp = False))
1901
1902       ## Open a session.
1903       session, root = clean.enter(chroot_session(dist, arch, sourcep = True))
1904
1905       ## Search the cross-tools tree for tools, to decide what to do with
1906       ## each file.  Make lists:
1907       ##
1908       ##   * `want_div' is simply a set of all files in the chroot which need
1909       ##     dpkg diversions to prevent foreign versions of the tools from
1910       ##     clobbering our native versions.
1911       ##
1912       ##   * `want_link' is a dictionary mapping paths which need symbolic
1913       ##     links into the cross-tools trees to their link destinations.
1914       progress("scan cross-tools tree")
1915       want_div = set()
1916       want_link = dict()
1917       cross_prefix = crossdir + "/"
1918       qemu_prefix = qemudir + "/"
1919       toolchain_prefix = OS.path.join(crossdir, "TOOLCHAIN", gnuarch) + "/"
1920       def examine(path):
1921         dest = switch_prefix(path, [(qemu_prefix, "/usr/bin/"),
1922                                     (toolchain_prefix, "/usr/bin/"),
1923                                     (cross_prefix, "/")])
1924         if OS.path.islink(path): src = OS.readlink(path)
1925         else: src = host_to_chroot(path)
1926         want_link[dest] = src
1927         if not OS.path.isdir(path): want_div.add(dest)
1928       examine(OS.path.join(qemudir, "qemu-%s-static" % qarch))
1929       examine(OS.path.join(crossdir, "lib", mymulti))
1930       examine(OS.path.join(crossdir, "usr/lib", mymulti))
1931       examine(OS.path.join(crossdir, "usr/lib/gcc-cross"))
1932       def visit(_, dir, files):
1933         ff = []
1934         for f in files:
1935           if f == "META" or f == "QEMU" or f == "TOOLCHAIN" or \
1936              (dir.endswith("/lib") and (f == mymulti or f == "gcc-cross")):
1937             continue
1938           ff.append(f)
1939           path = OS.path.join(dir, f)
1940           if not OS.path.isdir(path): examine(path)
1941         files[:] = ff
1942       OS.path.walk(crossdir, visit, None)
1943       OS.path.walk(OS.path.join(crossdir, "TOOLCHAIN", gnuarch),
1944                    visit, None)
1945
1946       ## Build the set `have_div' of paths which already have diversions.
1947       progress("scan chroot")
1948       have_div = set()
1949       with subprocess(["schroot", "-uroot", "-r", "-c", session, "--",
1950                        "dpkg-divert", "--list"],
1951                       stdout = PIPE) as (_, fd_out, _):
1952         try:
1953           f = OS.fdopen(fd_out)
1954           for line in f:
1955             m = R_DIVERT.match(line.rstrip("\n"))
1956             if m: have_div.add(m.group(1))
1957         finally:
1958           f.close()
1959
1960       ## Build a dictionary `have_link' of symbolic links into the cross-
1961       ## tools trees.  Also, be sure to collect all of the relative symbolic
1962       ## links which are in the cross-tools tree.
1963       have_link = dict()
1964       with subprocess(["schroot", "-uroot", "-r", "-c", session, "--",
1965                        "sh", "-e", "-c", """
1966         find / -xdev -lname "/usr/local.schroot/cross/*" -printf "%p %l\n"
1967       """], stdout = PIPE) as (_, fd_out, _):
1968         try:
1969           f = OS.fdopen(fd_out)
1970           for line in f:
1971             dest, src = line.split()
1972             have_link[dest] = src
1973         finally:
1974           f.close()
1975       for path in want_link.iterkeys():
1976         real = root + path
1977         if not OS.path.islink(real): continue
1978         have_link[path] = OS.readlink(real)
1979
1980       ## Add diversions for the paths which need one, but don't have one.
1981       ## There's a hack here because the `--no-rename' option was required in
1982       ## the same version in which it was introduced, so there's no single
1983       ## incantation that will work across the boundary.
1984       progress("add missing diversions")
1985       with subprocess(["schroot", "-uroot", "-r", "-c", session, "--",
1986                        "sh", "-e", "-c", """
1987         a="%(arch)s"
1988
1989         if dpkg-divert >/dev/null 2>&1 --no-rename --help
1990         then no_rename=--no-rename
1991         else no_rename=
1992         fi
1993
1994         while read path; do
1995           dpkg-divert --package "install-cross-tools" $no_rename \
1996             --divert "$path.$a" --add "$path"
1997         done
1998       """ % dict(arch = arch)], stdin = PIPE) as (fd_in, _, _):
1999         try:
2000           f = OS.fdopen(fd_in, 'w')
2001           for path in want_div:
2002             if path not in have_div: f.write(path + "\n")
2003         finally:
2004           f.close()
2005
2006       ## Go through each diverted tool, and, if it hasn't been moved aside,
2007       ## then /link/ it across now.  If we rename it, then the chroot will
2008       ## stop working -- which is why we didn't allow `dpkg-divert' to do the
2009       ## rename.  We can tell a tool that hasn't been moved, because it's a
2010       ## symlink into one of the cross trees.
2011       progress("preserve existing foreign files")
2012       chroot_cross_prefix = host_to_chroot(crossdir) + "/"
2013       chroot_qemu_prefix = host_to_chroot(qemudir) + "/"
2014       for path in want_div:
2015         real = root + path; div = real + "." + arch; cross = crossdir + path
2016         if OS.path.exists(div): continue
2017         if not OS.path.exists(real): continue
2018         if OS.path.islink(real):
2019           realdest = OS.readlink(real)
2020           if realdest.startswith(chroot_cross_prefix) or \
2021              realdest.startswith(chroot_qemu_prefix):
2022             continue
2023           if OS.path.islink(cross) and realdest == OS.readlink(cross):
2024             continue
2025         progress("preserve existing foreign file `%s'" % path)
2026         run_root(["ln", real, div])
2027
2028       ## Update all of the symbolic links which are currently wrong: add
2029       ## links which are missing, delete ones which are obsolete, and update
2030       ## ones which have the wrong target.
2031       progress("update symlinks")
2032       for path, src in want_link.iteritems():
2033         real = root + path
2034         try: old_src = have_link[path]
2035         except KeyError: pass
2036         else:
2037           if src == old_src: continue
2038         new = real + ".new"
2039         progress("link `%s' -> `%s'" % (path, src))
2040         dir = OS.path.dirname(real)
2041         if not OS.path.isdir(dir): run_root(["mkdir", "-p", dir])
2042         if OS.path.exists(new): run_root(["rm", "-f", new])
2043         run_root(["ln", "-s", src, new])
2044         run_root(["mv", new, real])
2045       for path in have_link.iterkeys():
2046         if path in want_link: continue
2047         progress("remove obsolete link `%s' -> `%s'" % path)
2048         real = root + path
2049         run_root(["rm", "-f", real])
2050
2051       ## Remove diversions from paths which don't need them any more.  Here
2052       ## it's safe to rename, because either the tool isn't there, in which
2053       ## case it obviously wasn't important, or it is, and `dpkg-divert' will
2054       ## atomically replace our link with the foreign version.
2055       progress("remove obsolete diversions")
2056       with subprocess(["schroot", "-uroot", "-r", "-c", session, "--",
2057                        "sh", "-e", "-c", """
2058         a="%(arch)s"
2059
2060         while read path; do
2061           dpkg-divert --package "install-cross-tools" --rename \
2062             --divert "$path.$a" --remove "$path"
2063         done
2064       """ % dict(arch = arch)], stdin = PIPE) as (fd_in, _, _):
2065         try:
2066           f = OS.fdopen(fd_in, 'w')
2067           for path in have_div:
2068             if path not in want_div: f.write(path + "\n")
2069         finally:
2070           f.close()
2071
2072   def _make_chroot(me):
2073     """
2074     Create the source-chroot with chroot metadata META.
2075
2076     This will recreate a source-chroot from scratch, destroying the existing
2077     logical volume if necessary.
2078     """
2079     with Cleanup() as clean:
2080
2081       dist, arch = me._dist, me._arch
2082       clean.enter(lockfile(chroot_src_lockfile(dist, arch)))
2083
2084       mnt = chroot_src_mntpt(dist, arch)
2085       dev = chroot_src_blkdev(dist, arch)
2086       lv = chroot_src_lv(dist, arch)
2087       newlv = lv + ".new"
2088
2089       ## Clean up any leftover debris.
2090       if mountpoint_p(mnt): umount(mnt)
2091       if block_device_p(dev):
2092         run_root(["lvremove", "-f", "%s/%s" % (C.VG, lv)])
2093
2094       ## Create the logical volume and filesystem.  It's important that the
2095       ## logical volume not have its official name until after it contains a
2096       ## mountable filesystem.
2097       progress("create filesystem")
2098       run_root(["lvcreate", "--yes", C.LVSZ, "-n", newlv, C.VG])
2099       run_root(["mkfs", "-j", "-L%s-%s" % (dist, arch),
2100                 OS.path.join("/dev", C.VG, newlv)])
2101       run_root(["lvrename", C.VG, newlv, lv])
2102
2103       ## Start installing the chroot.
2104       with mount_chroot_src(dist, arch) as mnt:
2105
2106         ## Set the basic structure.
2107         run_root(["mkdir", "-m755", OS.path.join(mnt, "fs")])
2108         run_root(["chmod", "750", mnt])
2109
2110         ## Install the base system.
2111         progress("install base system")
2112         run_root(["eatmydata", "debootstrap"] +
2113                  (arch in C.FOREIGN_ARCHS and ["--foreign"] or []) +
2114                  ["--arch=" + arch, "--variant=minbase",
2115                   "--include=" + ",".join(C.BASE_PACKAGES),
2116                   dist, OS.path.join(mnt, "fs"), C.DEBMIRROR])
2117
2118         ## If this is a cross-installation, then install the necessary `qemu'
2119         ## and complete the installation.
2120         if arch in C.FOREIGN_ARCHS:
2121           qemu = OS.path.join("cross", "%s-%s" % (dist, C.QEMUHOST[arch]),
2122                               "QEMU", "qemu-%s-static" % C.QEMUARCH[arch])
2123           run_root(["install", OS.path.join(C.LOCAL, qemu),
2124                     OS.path.join(mnt, "fs/usr/bin")])
2125           run_root(["chroot", OS.path.join(mnt, "fs"),
2126                     "/debootstrap/debootstrap", "--second-stage"])
2127           run_root(["ln", "-sf",
2128                     OS.path.join("/usr/local.schroot", qemu),
2129                     OS.path.join(mnt, "fs/usr/bin")])
2130
2131         ## Set up `/usr/local'.
2132         progress("install `/usr/local' symlink")
2133         run_root(["rm", "-rf", OS.path.join(mnt, "fs/usr/local")])
2134         run_root(["ln", "-s",
2135                   OS.path.join("local.schroot", arch),
2136                   OS.path.join(mnt, "fs/usr/local")])
2137
2138         ## Install the `apt' configuration.
2139         progress("configure package manager")
2140         run_root(["rm", "-f", OS.path.join(mnt, "fs/etc/apt/sources.list")])
2141         for c in C.APTCONF:
2142           run_root(["ln", "-s",
2143                     OS.path.join("/usr/local.schroot/etc/apt/apt.conf.d", c),
2144                     OS.path.join(mnt, "fs/etc/apt/apt.conf.d")])
2145         run_root(["ln", "-s",
2146                   "/usr/local.schroot/etc/apt/sources.%s" % dist,
2147                   OS.path.join(mnt, "fs/etc/apt/sources.list")])
2148
2149         with safewrite_root\
2150              (OS.path.join(mnt, "fs/etc/apt/apt.conf.d/20arch")) as f:
2151           f.write("""\
2152   ### -*-conf-*-
2153
2154   APT {
2155           Architecture "%s";
2156   };
2157   """ % arch)
2158
2159         ## Set up the locale and time zone from the host system.
2160         progress("configure locales and timezone")
2161         run_root(["cp", "/etc/locale.gen", "/etc/timezone",
2162                   OS.path.join(mnt, "fs/etc")])
2163         with open("/etc/timezone") as f: tz = f.readline().strip()
2164         run_root(["ln", "-sf",
2165                   OS.path.join("/usr/share/timezone", tz),
2166                   OS.path.join(mnt, "fs/etc/localtime")])
2167         run_root(["cp", "/etc/default/locale",
2168                   OS.path.join(mnt, "fs/etc/default")])
2169
2170         ## Fix `/etc/mtab'.
2171         progress("set `/etc/mtab'")
2172         run_root(["ln", "-sf", "/proc/mounts",
2173                   OS.path.join(mnt, "fs/etc/mtab")])
2174
2175         ## Prevent daemons from starting within the chroot.
2176         progress("inhibit daemon startup")
2177         with safewrite_root(OS.path.join(mnt, "fs/usr/sbin/policy-rc.d"),
2178                             mode = "755") as f:
2179           f.write("""\
2180   #! /bin/sh
2181   echo >&2 "policy-rc.d: Services disabled by policy."
2182   exit 101
2183   """)
2184
2185         ## Hack the dynamic linker to prefer libraries in `/usr' over
2186         ## `/usr/local'.  This prevents `dpkg-shlibdeps' from becoming
2187         ## confused.
2188         progress("configure dynamic linker")
2189         with safewrite_root\
2190              (OS.path.join(mnt, "fs/etc/ld.so.conf.d/libc.conf")) as f:
2191           f.write("# libc default configuration")
2192         with safewrite_root\
2193              (OS.path.join(mnt, "fs/etc/ld.so.conf.d/zzz-local.conf")) as f:
2194           f.write("""\
2195   ### -*-conf-*-
2196   ### Local hack to make /usr/local/ late.
2197   /usr/local/lib
2198   """)
2199
2200       ## If this is a foreign architecture then we need to set it up.
2201       if arch in C.FOREIGN_ARCHS:
2202
2203         ## Keep the chroot's native Qemu out of our way: otherwise we'll stop
2204         ## being able to run programs in the chroot.  There's a hack here
2205         ## because the `--no-rename' option was required in the same version
2206         ## in which is was introduced, so there's no single incantation that
2207         ## will work across the boundary.
2208         progress("divert emulator")
2209         run_schroot_source(dist, arch, ["eatmydata", "sh", "-e", "-c", """
2210           if dpkg-divert >/dev/null 2>&1 --no-rename --help
2211           then no_rename=--no-rename
2212           else no_rename=
2213           fi
2214
2215           dpkg-divert --package install-cross-tools $no_rename \
2216             --divert /usr/bin/%(qemu)s.%(arch)s --add /usr/bin/%(qemu)s
2217         """ % dict(arch = arch, qemu = "qemu-%s-static" % C.QEMUARCH[arch])])
2218
2219         ## Install faster native tools.
2220         me._install_cross_tools()
2221
2222       ## Finishing touches.
2223       progress("finishing touches")
2224       run_schroot_source(dist, arch, ["eatmydata", "sh", "-e", "-c", """
2225         apt-get update
2226         apt-get -y upgrade
2227         apt-get -y install "$@"
2228         ldconfig
2229         apt-get -y autoremove
2230         apt-get clean
2231       """, "."] + C.EXTRA_PACKAGES, stdin = DISCARD)
2232
2233       ## Mark the chroot as done.
2234       me._meta.update = zulu()
2235       me._meta.write()
2236
2237   def _update_chroot(me):
2238     """Refresh the source-chroot with chroot metadata META."""
2239     with Cleanup() as clean:
2240       dist, arch = me._dist, me._arch
2241       clean.enter(lockfile(chroot_src_lockfile(dist, arch)))
2242       run_schroot_source(dist, arch, ["eatmydata", "sh", "-e", "-c", """
2243         apt-get update
2244         apt-get -y dist-upgrade
2245         apt-get -y autoremove
2246         apt-get -y clean
2247       """], stdin = DISCARD)
2248       if arch in C.FOREIGN_ARCHS: me._install_cross_tools()
2249       me._meta.update = zulu(); me._meta.write()
2250
2251   def run(me):
2252     if me._meta.update is not None: me._update_chroot()
2253     else: me._make_chroot()
2254
2255 ###--------------------------------------------------------------------------
2256 ### Extracting the cross tools.
2257
2258 class CrossToolsJob (BaseJob):
2259   """Extract cross-tools from a donor chroot."""
2260
2261   SPECS = C.NATIVE_CHROOTS
2262
2263   def __init__(me, spec, fresh = CREATE, *args, **kw):
2264     super(CrossToolsJob, me).__init__(*args, **kw)
2265     me._dist, me._arch = split_dist_arch(spec)
2266     me._meta = CrossToolsMetadata.read(me._dist, me._arch)
2267     me._fresh = fresh
2268     me._chroot = None
2269
2270   def _mkname(me): return "cross-tools.%s-%s" % (me._dist, me._arch)
2271
2272   def prepare(me):
2273     st, r = check_fresh(me._fresh, me._meta.update)
2274     if st is DONE: return
2275     me._chroot = ChrootJob.ensure("%s-%s" % (me._dist, me._arch), FRESH)
2276     me.await(me._chroot)
2277
2278   def check(me):
2279     status, reason = super(CrossToolsJob, me).check()
2280     if status is not READY: return status, reason
2281     if me._chroot is not None and me._chroot.started:
2282       return READY, "prerequisites run"
2283     return check_fresh(me._fresh, me._meta.update)
2284
2285   def run(me):
2286     with Cleanup() as clean:
2287
2288       dist, arch = me._dist, me._arch
2289
2290       mymulti = run_program(["dpkg-architecture", "-a" + arch,
2291                              "-qDEB_HOST_MULTIARCH"],
2292                             stdout = RETURN).rstrip("\n")
2293       crossarchs = [run_program(["dpkg-architecture", "-A" + a,
2294                                  "-qDEB_TARGET_GNU_TYPE"],
2295                                 stdout = RETURN).rstrip("\n")
2296                     for a in C.FOREIGN_ARCHS]
2297
2298       crossdir = OS.path.join(C.LOCAL, "cross", "%s-%s" % (dist, arch))
2299       crossold = crossdir + ".old"; crossnew = crossdir + ".new"
2300       usrbin = OS.path.join(crossnew, "usr/bin")
2301
2302       clean.enter(lockfile(crosstools_lockfile(dist, arch)))
2303       run_program(["rm", "-rf", crossnew])
2304       mkdir_p(crossnew)
2305
2306       ## Open a session to the donor chroot.
2307       progress("establish snapshot")
2308       session, root = clean.enter(chroot_session(dist, arch))
2309
2310       ## Make sure the donor tree is up-to-date, and install the extra
2311       ## packages we need.
2312       progress("install tools packages")
2313       run_schroot_session(session, ["eatmydata", "sh", "-e", "-c", """
2314         apt-get update
2315         apt-get -y upgrade
2316         apt-get -y install "$@"
2317       """, "."] + C.CROSS_PACKAGES, rootp = True, stdin = DISCARD)
2318
2319       def chase(path):
2320         dest = ""
2321
2322         ## Work through the remaining components of the PATH.
2323         while path != "":
2324           try: sl = path.index("/")
2325           except ValueError: step = path; path = ""
2326           else: step, path = path[:sl], path[sl + 1:]
2327
2328           ## Split off and analyse the first component.
2329           if step == "" or step == ".":
2330             ## A redundant `/' or `./'.  Skip it.
2331             pass
2332           elif step == "..":
2333             ## A `../'.  Strip off the trailing component of DEST.
2334             dest = dest[:dest.rindex("/")]
2335           else:
2336             ## Something else.  Transfer the component name to DEST.
2337             dest += "/" + step
2338
2339           ## If DEST refers to something in the cross-tools tree then we're
2340           ## good.
2341           crossdest = crossnew + dest
2342           try: st = OS.lstat(crossdest)
2343           except OSError, err:
2344             if err.errno == E.ENOENT:
2345               ## No.  We need to copy something from the donor tree so that
2346               ## the name works.
2347
2348               st = OS.lstat(root + dest)
2349               if ST.S_ISDIR(st.st_mode):
2350                 OS.mkdir(crossdest)
2351               else:
2352                 progress("copy `%s'" % dest)
2353                 run_program(["rsync", "-aHR",
2354                              "%s/.%s" % (root, dest),
2355                              crossnew])
2356             else:
2357               raise
2358
2359           ## If DEST refers to a symbolic link, then prepend the link target
2360           ## to PATH so that we can be sure the link will work.
2361           if ST.S_ISLNK(st.st_mode):
2362             link = OS.readlink(crossdest)
2363             if link.startswith("/"): dest = ""; link = link[1:]
2364             else:
2365               try: dest = dest[:dest.rindex("/")]
2366               except ValueError: dest = ""
2367             if path == "": path = link
2368             else: path = "%s/%s" % (path, link)
2369
2370       ## Work through the shopping list, copying the things it names into the
2371       ## cross-tools tree.
2372       scan = []
2373       for pat in C.CROSS_PATHS:
2374         pat = pat.replace("MULTI", mymulti)
2375         any = False
2376         for rootpath in GLOB.iglob(root + pat):
2377           any = True
2378           path = rootpath[len(root):]
2379           progress("copy `%s'" % path)
2380           run_program(["rsync", "-aHR", "%s/.%s" % (root, path), crossnew])
2381         if not any:
2382           raise RuntimeError("no matches for cross-tool pattern `%s'" % pat)
2383
2384       ## Scan the new tree: chase down symbolic links, copying extra stuff
2385       ## that we'll need; and examine ELF binaries to make sure we get the
2386       ## necessary shared libraries.
2387       def visit(_, dir, files):
2388         for f in files:
2389           path = OS.path.join(dir, f)
2390           inside = switch_prefix(path, [(crossnew + "/", "/")])
2391           if OS.path.islink(path): chase(inside)
2392           if elf_binary_p(arch, path): scan.append(inside)
2393       OS.path.walk(crossnew, visit, None)
2394
2395       ## Work through the ELF binaries in `scan', determining which shared
2396       ## libraries they'll need.
2397       ##
2398       ## The rune running in the chroot session reads ELF binary names on
2399       ## stdin, one per line, and runs `ldd' on them to discover the binary's
2400       ## needed libraries and resolve them into pathnames.  Each pathname is
2401       ## printed to stderr as a line `+PATHNAME', followed by a final line
2402       ## consisting only of `-' as a terminator.  This is necessary so that
2403       ## we can tell when we've finished, because newly discovered libraries
2404       ## need to be fed back to discover their recursive dependencies.  (This
2405       ## is why the `WriteLinesSelector' interface is quite so hairy.)
2406       with subprocess(["schroot", "-r", "-c", session, "--",
2407                        "sh", "-e", "-c", """
2408         while read path; do
2409           ldd "$path" | while read a b c d; do
2410             case $a:$b:$c:$d in
2411               not:a:dynamic:executable) ;;
2412               statically:linked::) ;;
2413               /*) echo "+$a" ;;
2414               *:=\\>:/*) echo "+$c" ;;
2415               linux-*) ;;
2416               *) echo >&2 "failed to find shared library \\`$a'"; exit 2 ;;
2417             esac
2418           done
2419           echo -
2420         done
2421       """], stdin = PIPE, stdout = PIPE) as (fd_in, fd_out, _):
2422
2423         ## Keep track of the number of binaries we've reported to the `ldd'
2424         ## process for which we haven't yet seen all of their dependencies.
2425         ## (This is wrapped in a `Struct' because of Python's daft scoping
2426         ## rules.)
2427         v = Struct(n = 0)
2428
2429         def line_in():
2430           ## Provide a line in., so raise `StopIteration' to signal this.
2431
2432           try:
2433             ## See if there's something to scan.
2434             path = scan.pop()
2435
2436           except IndexError:
2437             ## There's nothing currently waiting to be scanned.
2438             if v.n:
2439               ## There are still outstanding replies, so stall.
2440               return None
2441             else:
2442               ## There are no outstanding replies left, and we have nothing
2443               ## more to scan, then we must be finished.
2444               raise StopIteration
2445
2446           else:
2447             ## The `scan' list isn't empty, so return an item from that, and
2448             ## remember that there's one more thing we expect to see answers
2449             ## from.
2450             v.n += 1; return path
2451
2452         def line_out(line):
2453           ## We've received a line from the `ldd' process.
2454
2455           if line == "-":
2456             ## It's finished processing one of our binaries.  Note this.
2457             ## Maybe it's time to stop
2458             v.n -= 1
2459             return
2460
2461           ## Strip the leading marker (which is just there so that the
2462           ## terminating `-' is unambiguous).
2463           assert line.startswith("+")
2464           lib = line[1:]
2465
2466           ## If we already have this binary then we'll already have submitted
2467           ## it.
2468           path = crossnew + lib
2469           try: OS.lstat(path)
2470           except OSError, err:
2471             if err.errno == E.ENOENT: pass
2472             else: raise
2473           else: return
2474
2475           ## Copy it into the tools tree, together with any symbolic links
2476           ## along the path.
2477           chase(lib)
2478
2479           ## If this is an ELF binary (and it ought to be!) then submit it
2480           ## for further scanning.
2481           if elf_binary_p(arch, path):
2482             scan.append(switch_prefix(path, [(crossnew + "/", "/")]))
2483
2484         ## And run this entire contraption.  When this is done, we should
2485         ## have all of the library dependencies for all of our binaries.
2486         select_loop([WriteLinesSelector(fd_in, line_in),
2487                      ReadLinesSelector(fd_out, line_out)])
2488
2489       ## Set up the cross-compiler and emulator.  Start by moving the cross
2490       ## compilers and emulator into their specific places, so they don't end
2491       ## up cluttering chroots for non-matching architectures.
2492       progress("establish TOOLCHAIN and QEMU")
2493       OS.mkdir(OS.path.join(crossnew, "TOOLCHAIN"))
2494       qemudir = OS.path.join(crossnew, "QEMU")
2495       OS.mkdir(qemudir)
2496       for gnu in C.FOREIGN_GNUARCHS:
2497         OS.mkdir(OS.path.join(crossnew, "TOOLCHAIN", gnu))
2498       for f in OS.listdir(usrbin):
2499         for gnu in C.FOREIGN_GNUARCHS:
2500           gnuprefix = gnu + "-"
2501           if f.startswith(gnuprefix):
2502             tooldir = OS.path.join(crossnew, "TOOLCHAIN", gnu)
2503             OS.rename(OS.path.join(usrbin, f), OS.path.join(tooldir, f))
2504             OS.symlink(f, OS.path.join(tooldir, f[len(gnuprefix):]))
2505             break
2506         else:
2507           if f.startswith("qemu-") and f.endswith("-static"):
2508             OS.rename(OS.path.join(usrbin, f), OS.path.join(qemudir, f))
2509
2510       ## The GNU cross compilers try to find their additional pieces via a
2511       ## relative path, which isn't going to end well.  Add a symbolic link
2512       ## at the right place to where the things are actually going to live.
2513       toollib = OS.path.join(crossnew, "TOOLCHAIN", "lib")
2514       OS.mkdir(toollib)
2515       OS.symlink("../../usr/lib/gcc-cross",
2516                  OS.path.join(toollib, "gcc-cross"))
2517
2518       ## We're done.  Replace the old cross-tools with our new one.
2519       me._meta.update = zulu()
2520       me._meta.write(crossnew)
2521       if OS.path.exists(crossdir): run_program(["mv", crossdir, crossold])
2522       OS.rename(crossnew, crossdir)
2523       run_program(["rm", "-rf", crossold])
2524
2525 ###--------------------------------------------------------------------------
2526 ### Buliding and installing local packages.
2527
2528 def pkg_metadata_lockfile(pkg):
2529   return lockfile_path("pkg-meta.%s" % pkg)
2530
2531 def pkg_srcdir_lockfile(pkg, ver):
2532   return lockfile_path("pkg-source.%s-%s" % (pkg, ver))
2533
2534 def pkg_srcdir(pkg, ver):
2535   return OS.path.join(C.LOCAL, "src", "%s-%s" % (pkg, ver))
2536
2537 def pkg_builddir(pkg, ver, arch):
2538   return OS.path.join(pkg_srcdir(pkg, ver), "build.%s" % arch)
2539
2540 class PackageMetadata (BaseMetadata):
2541   VARS = ["pkg"] + list(C.ALL_ARCHS)
2542
2543   @classmethod
2544   def read(cls, pkg):
2545     try:
2546       return super(PackageMetadata, cls)\
2547         .read(OS.path.join(C.LOCAL, "src", "META.%s" % pkg))
2548     except IOError, err:
2549       if err.errno == E.ENOENT: pass
2550       else: raise
2551     return cls(pkg = pkg)
2552
2553   def write(me):
2554     super(PackageMetadata, me)\
2555       .write(OS.path.join(C.LOCAL, "src", "META.%s" % me.pkg))
2556
2557 class PackageSourceJob (BaseJob):
2558
2559   SPECS = C.LOCALPKGS
2560
2561   def __init__(me, pkg, fresh = CREATE, *args, **kw):
2562     super(PackageSourceJob, me).__init__(*args, **kw)
2563     me._pkg = pkg
2564     tar = None; ver = None
2565     r = RX.compile("^%s-(\d.*)\.tar.(?:Z|z|gz|bz2|xz|lzma)$" %
2566                    RX.escape(pkg))
2567     for f in OS.listdir("pkg"):
2568       m = r.match(f)
2569       if not m: pass
2570       elif tar is not None:
2571         raise ExpectedError("multiple source tarballs of package `%s'" % pkg)
2572       else: tar, ver = f, m.group(1)
2573     me.version = ver
2574     me.tarball = OS.path.join("pkg", tar)
2575
2576   def _mkname(me): return "pkg-source.%s" % me._pkg
2577
2578   def check(me):
2579     status, reason = super(PackageSourceJob, me).check()
2580     if status is not READY: return status, reason
2581     if OS.path.isdir(pkg_srcdir(me._pkg, me.version)):
2582       return DONE, "already unpacked"
2583     else:
2584       return READY, "no source tree"
2585
2586   def run(me):
2587     with Cleanup() as clean:
2588       pkg, ver, tar = me._pkg, me.version, me.tarball
2589       srcdir = pkg_srcdir(pkg, ver)
2590       newdir = srcdir + ".new"
2591
2592       progress("unpack `%s'" % me.tarball)
2593       clean.enter(lockfile(pkg_srcdir_lockfile(pkg, ver)))
2594       run_program(["rm", "-rf", newdir])
2595       mkdir_p(newdir)
2596       run_program(["tar", "xf", OS.path.join(OS.getcwd(), me.tarball)],
2597                   cwd = newdir)
2598       things = OS.listdir(newdir)
2599       if len(things) == 1:
2600         OS.rename(OS.path.join(newdir, things[0]), srcdir)
2601         OS.rmdir(newdir)
2602       else:
2603         OS.rename(newdir, srcdir)
2604
2605 class PackageBuildJob (BaseJob):
2606
2607   SPECS = ["%s:%s" % (pkg, arch)
2608            for pkg in C.LOCALPKGS
2609            for arch in C.ALL_ARCHS]
2610
2611   def __init__(me, spec, fresh = CREATE, *args, **kw):
2612     super(PackageBuildJob, me).__init__(*args, **kw)
2613     colon = spec.index(":")
2614     me._pkg, me._arch = spec[:colon], spec[colon + 1:]
2615
2616   def _mkname(me): return "pkg-build.%s:%s" % (me._pkg, me._arch)
2617
2618   def prepare(me):
2619     me.await(ChrootJob.ensure("%s-%s" % (C.PRIMARY_DIST, me._arch), CREATE))
2620     me._meta = PackageMetadata.read(me._pkg)
2621     me._src = PackageSourceJob.ensure(me._pkg, FRESH); me.await(me._src)
2622     me._prereq = [PackageBuildJob.ensure("%s:%s" % (prereq, me._arch), FRESH)
2623                   for prereq in C.PKGDEPS[me._pkg]]
2624     for j in me._prereq: me.await(j)
2625
2626   def check(me):
2627     status, reason = super(PackageBuildJob, me).check()
2628     if status is not READY: return status, reason
2629     if me._src.started: return READY, "fresh source directory"
2630     for j in me._prereq:
2631       if j.started:
2632         return READY, "dependency `%s' freshly installed" % j._pkg
2633     if getattr(me._meta, me._arch) == me._src.version:
2634       return DONE, "already installed"
2635     return READY, "not yet installed"
2636
2637   def run(me):
2638     with Cleanup() as clean:
2639       pkg, ver, arch = me._pkg, me._src.version, me._arch
2640
2641       session, _ = clean.enter(chroot_session(C.PRIMARY_DIST, arch))
2642       builddir = OS.path.join(pkg_srcdir(pkg, ver), "build.%s" % arch)
2643       chroot_builddir = host_to_chroot(builddir)
2644       run_program(["rm", "-rf", builddir])
2645       OS.mkdir(builddir)
2646
2647       progress("prepare %s chroot" % (arch))
2648       run_schroot_session(session,
2649                           ["eatmydata", "apt-get", "update"],
2650                           rootp = True, stdin = DISCARD)
2651       run_schroot_session(session,
2652                           ["eatmydata", "apt-get", "-y", "upgrade"],
2653                           rootp = True, stdin = DISCARD)
2654       run_schroot_session(session,
2655                           ["eatmydata", "apt-get", "-y",
2656                            "install", "pkg-config"],
2657                           rootp = True, stdin = DISCARD)
2658       run_schroot_session(session,
2659                           ["mount", "-oremount,rw", "/usr/local.schroot"],
2660                           rootp = True, stdin = DISCARD)
2661
2662       progress("configure `%s' %s for %s" % (pkg, ver, arch))
2663       run_schroot_session(session, ["sh", "-e", "-c", """
2664         cd "$1" &&
2665         ../configure PKG_CONFIG_PATH=/usr/local/lib/pkgconfig.hidden
2666       """, ".", chroot_builddir])
2667
2668       progress("compile `%s' %s for %s" % (pkg, ver, arch))
2669       run_schroot_session(session, ["sh", "-e", "-c", """
2670         cd "$1" && make -j4 && make -j4 check
2671       """, ".", chroot_builddir])
2672
2673       existing = getattr(me._meta, arch, None)
2674       if existing is not None and existing != ver:
2675         progress("uninstall existing `%s' %s for %s" % (pkg, existing, arch))
2676         run_schroot_session(session, ["sh", "-e", "-c", """
2677           cd "$1" && make uninstall
2678         """, ".", OS.path.join(pkg_srcdir(pkg, existing),
2679                                "build.%s" % arch)],
2680                             rootp = True)
2681
2682       progress("install `%s' %s for %s" % (pkg, existing, arch))
2683       run_schroot_session(session, ["sh", "-e", "-c", """
2684         cd "$1" && make install
2685         mkdir -p /usr/local/lib/pkgconfig.hidden
2686         mv /usr/local/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig.hidden || :
2687       """, ".", chroot_builddir], rootp = True)
2688
2689       clean.enter(lockfile(pkg_metadata_lockfile(pkg)))
2690       me._meta = PackageMetadata.read(pkg)
2691       setattr(me._meta, arch, ver); me._meta.write()
2692
2693     with lockfile(chroot_src_lockfile(C.PRIMARY_DIST, arch)):
2694       run_schroot_source(C.PRIMARY_DIST, arch, ["ldconfig"])
2695
2696 ###--------------------------------------------------------------------------
2697 ### Process the configuration and options.
2698
2699 OPTIONS = OP.OptionParser\
2700   (usage = "chroot-maint [-diknqs] [-fFRESH] [-jN] JOB[.SPEC,...] ...")
2701 for short, long, props in [
2702   ("-d", "--debug", {
2703     'dest': 'debug', 'default': False, 'action': 'store_true',
2704     'help': "print lots of debugging drivel" }),
2705   ("-f", "--fresh", {
2706     'dest': 'fresh', 'metavar': 'FRESH', 'default': "create",
2707     'help': "how fresh (`create', `force', or `N[s|m|h|d|w]')" }),
2708   ("-i", "--ignore-errors", {
2709     'dest': 'ignerr', 'default': False, 'action': 'store_true',
2710     'help': "ignore all errors encountered while processing" }),
2711   ("-j", "--jobs", {
2712     'dest': 'njobs', 'metavar': 'N', 'default': 1, 'type': 'int',
2713     'help': 'run up to N jobs in parallel' }),
2714   ("-J", "--forkbomb", {
2715     'dest': 'njobs', 'action': 'store_true',
2716     'help': 'run as many jobs in parallel as possible' }),
2717   ("-k", "--keep-going", {
2718     'dest': 'keepon', 'default': False, 'action': 'store_true',
2719     'help': "keep going even if independent jobs fail" }),
2720   ("-n", "--dry-run", {
2721     'dest': 'dryrun', 'default': False, 'action': 'store_true',
2722     'help': "don't actually do anything" }),
2723   ("-q", "--quiet", {
2724     'dest': 'quiet', 'default': False, 'action': 'store_true',
2725     'help': "don't print the output from successful jobs" }),
2726   ("-s", "--silent", {
2727     'dest': 'silent', 'default': False, 'action': 'store_true',
2728     'help': "don't print progress messages" })]:
2729   OPTIONS.add_option(short, long, **props)
2730
2731 ###--------------------------------------------------------------------------
2732 ### Main program.
2733
2734 R_JOBSERV = RX.compile(r'^--jobserver-(?:fds|auth)=(\d+),(\d+)$')
2735
2736 JOBMAP = { "chroot": ChrootJob,
2737            "cross-tools": CrossToolsJob,
2738            "pkg-source": PackageSourceJob,
2739            "pkg-build": PackageBuildJob }
2740
2741 R_FRESH = RX.compile(r"^(?:create|force|(\d+)(|[smhdw]))$")
2742
2743 def parse_fresh(spec):
2744   m = R_FRESH.match(spec)
2745   if not m: raise ExpectedError("bad freshness `%s'" % spec)
2746   if spec == "create": fresh = CREATE
2747   elif spec == "force": fresh = FORCE
2748   else:
2749     n, u = int(m.group(1)), m.group(2)
2750     if u == "" or u == "s": fresh = n
2751     elif u == "m": fresh = 60*n
2752     elif u == "h": fresh = 3600*n
2753     elif u == "d": fresh = 86400*n
2754     elif u == "w": fresh = 604800*n
2755     else: assert False
2756   return fresh
2757
2758 with toplevel_handler():
2759   OPT, args = OPTIONS.parse_args()
2760   rfd, wfd = -1, -1
2761   njobs = OPT.njobs
2762   try: mkflags = OS.environ['MAKEFLAGS']
2763   except KeyError: pass
2764   else:
2765     ff = mkflags.split()
2766     for f in ff:
2767       if f == "--": break
2768       m = R_JOBSERV.match(f)
2769       if m: rfd, wfd = int(m.group(1)), int(m.group(2))
2770       elif f == '-j': njobs = None
2771       elif not f.startswith('-'):
2772         for ch in f:
2773           if ch == 'i': OPT.ignerr = True
2774           elif ch == 'k': OPT.keepon = True
2775           elif ch == 'n': OPT.dryrun = True
2776           elif ch == 's': OPT.silent = True
2777   if OPT.njobs < 1:
2778     raise ExpectedError("running no more than %d jobs is silly" % OPT.njobs)
2779
2780   FRESH = parse_fresh(OPT.fresh)
2781
2782   SCHED = JobScheduler(rfd, wfd, njobs)
2783   OS.environ["http_proxy"] = C.PROXY
2784
2785   jobs = []
2786   if not args: OPTIONS.print_usage(SYS.stderr); SYS.exit(2)
2787   for arg in args:
2788     try: sl = arg.index("/")
2789     except ValueError: fresh = FRESH
2790     else: arg, fresh = arg[:sl], parse_fresh(arg[sl + 1:])
2791     try: dot = arg.index(".")
2792     except ValueError: jty, pats = arg, "*"
2793     else: jty, pats = arg[:dot], arg[dot + 1:]
2794     try: jcls = JOBMAP[jty]
2795     except KeyError: raise ExpectedError("unknown job type `%s'" % jty)
2796     specs = []
2797     for pat in pats.split(","):
2798       any = False
2799       for s in jcls.SPECS:
2800         if FM.fnmatch(s, pat): specs.append(s); any = True
2801       if not any: raise ExpectedError("no match for `%s'" % pat)
2802     for s in specs:
2803       jobs.append(jcls.ensure(s, fresh))
2804
2805   SCHED.run()
2806
2807 SYS.exit(RC)
2808
2809 ###----- That's all, folks --------------------------------------------------