chiark / gitweb /
httpauth.py: Don't crash if Base-64 decoding of the CSRF token fails.
[chopwood] / agpl.py
diff --git a/agpl.py b/agpl.py
index 20b61668e9331d7348ef34b36f3413375820b22e..a35ea3f4c9416d23fa7bbac959866dc9eee3c94b 100644 (file)
--- a/agpl.py
+++ b/agpl.py
@@ -40,20 +40,45 @@ from cStringIO import StringIO
 from auto import PACKAGE, VERSION
 import util as U
 
+###--------------------------------------------------------------------------
+### Initial utilities.
+
 @CTX.contextmanager
 def tempdir():
+  """
+  Context manager: create and return the name of a temporary directory.
+
+  The directory will be deleted automatically on exit from the body.
+  """
   d = TF.mkdtemp()
   try: yield d
   finally: SH.rmtree(d, ignore_errors = True)
 
+###--------------------------------------------------------------------------
+### Determining which files to include.
+
 def dirs_to_dump():
+  """
+  Return a list of directories containing used Python modules.
+
+  Directories under `/usr/' but outside `/usr/local/' are excluded, since
+  they are assumed to be covered by the AGPL exception for parts of the
+  operating system.
+  """
+
+  ## Collect a set of directories.
   dirs = set()
+
+  ## Work through the list of known modules, adding them to the list.
   for m in SYS.modules.itervalues():
     try: f = m.__file__
     except AttributeError: continue
     d = OS.path.realpath(OS.path.dirname(f))
     if d.startswith('/usr/') and not d.startswith('/usr/local/'): continue
     dirs.add(d)
+
+  ## Now go through the directories again, and remove any which are wholly
+  ## included within other entries.
   dirs = sorted(dirs)
   last = '!'
   dump = []
@@ -61,55 +86,159 @@ def dirs_to_dump():
     if d.startswith(last): continue
     dump.append(d)
     last = d
+
+  ## We're done: return the filtered list.
   return dump
 
+### The `DUMPERS' list consists of (PREDICATE, LISTERS) pairs.  The PREDICATE
+### is a function of one argument -- a directory name -- which returns true
+### if the LISTERS should be used to enumerate that directory.  The LISTERS
+### are a list of functions of one argument -- again, the directory name --
+### which should return an iterable of files within that directory, relative
+### to its top-level.  Lister functions should not return the root directory,
+### because it should obviously only be included once.  Instead, the root is
+### handled separately by `dump_dir'.
+
 def exists_subdir(subdir):
+  """
+  Predicate for `DUMPERS': match if the directory has a subdirectory SUBDIR.
+
+  This is mainly useful for detecting working trees subject to version
+  control.
+  """
   return lambda dir: OS.path.isdir(OS.path.join(dir, subdir))
 
 def filez(cmd):
+  """
+  Lister for `DUMPERS': generate the null-terminated items output by CMD.
+
+  Run CMD, a string containing words with shell-like quoting (expected to be
+  a literal in the code, so security concerns don't arise) in the directory
+  of interest, yielding the invidual null-terminated strings which the
+  command writes to its standard output.
+  """
   def _(dir):
+
+    ## Start the command,
     kid = SUB.Popen(SL.split(cmd), stdout = SUB.PIPE, cwd = dir)
+
+    ## Collect and return the null-terminated items.  Strip off any leading
+    ## `./' and exclude the root directory because that gets handled
+    ## separately.
     left = ''
     while True:
+
+      ## Read a new bufferload of stuff.  If there's nothing left then we're
+      ## done.
       buf = kid.stdout.read(16384)
       if not buf: break
+
+      ## Tack whatever was left over from last time on the front, and carve
+      ## into null-terminated pieces.
       buf = left + buf
       i = 0
       while True:
         z = buf.find('\0', i)
         if z < 0: break
         f = buf[i:z]
+        i = z + 1
+        if f.rstrip('/') == '.': continue
         if f.startswith('./'): f = f[2:]
         yield f
-        i = z + 1
+
+      ## Whatever's left over will be dealt with next time through.
       left = buf[i:]
+
+    ## Make sure the command actually completed successfully.
+    if kid.wait():
+      rc = kid.returncode
+      raise U.ExpectedError, \
+          (500, "lister command `%s' failed (%s) in `%s'" % (
+            cmd,
+            (rc & 0xff00) and 'rc = %d' % (rc >> 8) or 'signal %d' % rc,
+            dir))
+
+    ## If there's trailing junk left over then we should complain.
     if left:
       raise U.ExpectedError, \
             (500, "trailing junk from `%s' in `%s'" % (cmd, dir))
+
+  ## Return the listing function.
   return _
 
+## The list of predicates and listers.
 DUMPERS = [
   (exists_subdir('.git'), [filez('git ls-files -coz --exclude-standard'),
                            filez('find .git -print0')]),
   (lambda d: True, [filez('find . ( ! -perm +004 -prune ) -o -print0')])]
 
-def dump_dir(dir, tf, root):
+###--------------------------------------------------------------------------
+### Actually dumping files.
+
+def dump_dir(name, dir, dirmap, tf, root):
+  """
+  Add the contents of directory DIR to the tarfile TF, under the given NAME.
+
+  The ROOT names the toplevel of the tarball (we're not in the business of
+  making tarbombs here).  The DIRMAP is a list of all of the (DIR, NAME)
+  pairs being dumped, used for fixing up symbolic links between directories.
+  """
+
+  ## Find an appropriate `DUMPERS' list entry.
   for test, listers in DUMPERS:
     if test(dir): break
   else:
     raise U.ExpectedError, (500, "no dumper for `%s'" % dir)
+
+  ## Write a tarfile entry for the toplevel.
+  tf.add(dir, OS.path.join(root, name), recursive = False)
+
+  ## Work through all of the listers.
   for lister in listers:
-    base = OS.path.basename(dir)
+
+    ## Work through each file.
     for file in lister(dir):
-      tf.add(OS.path.join(dir, file), OS.path.join(root, base, file),
-             recursive = False)
+      with U.Escape() as skip:
+        full = OS.path.join(dir, file)
+        tarname = OS.path.join(root, name, file)
+
+        ## Check for symbolic links.  If we find one that points to another
+        ## directory we're going to dump separately then fiddle it so that it
+        ## works in the directory tree we're going to make.
+        if OS.path.islink(full):
+          dest = OS.path.realpath(full)
+          for d, local in dirmap:
+            if dest.startswith(d):
+              fix = OS.path.relpath(OS.path.join('/', local, dest[len(d):]),
+                                    OS.path.join('/', name,
+                                                 OS.path.dirname(file)))
+              st = OS.stat(full)
+              ti = tf.gettarinfo(full, tarname)
+              ti.linkname = fix
+              tf.addfile(ti)
+              skip()
+
+        ## Nothing special, so just dump the file.  Or whatever it is.
+        tf.add(full, tarname, recursive = False)
 
 def source(out):
+  """
+  Write a tarball for the program's source code to OUT.
+
+  This function automatically dumps all of the program's dependencies except
+  for those covered by the operating-system exemption.
+  """
+
+  ## Make a tarfile writer.  There's an annoying incompatibility to bodge
+  ## around.
   if SYS.version_info >= (2, 6):
     tf = TAR.open(fileobj = out, mode = 'w|gz', format = TAR.USTAR_FORMAT)
   else:
     tf = TAR.open(fileobj = out, mode = 'w|gz')
     tf.posix = True
+
+  ## First of all, find out what needs to be dumped, and assign names to all
+  ## of the various directories.
   root = '%s-%s' % (PACKAGE, VERSION)
   seen = set()
   dirmap = []
@@ -125,6 +254,9 @@ def source(out):
         if name not in seen: break
     dirmap.append((dir + '/', name))
     festout.write('%s = %s\n' % (name, dir))
+
+  ## Write a map of where things were in the filesystem.  This may help a
+  ## user figure out how to deploy the thing.
   fest = festout.getvalue()
   ti = TAR.TarInfo(OS.path.join(root, 'MANIFEST'))
   ti.size = len(fest)
@@ -134,8 +266,12 @@ def source(out):
   uid = OS.getuid(); ti.uid, ti.uname = uid, PW.getpwuid(uid).pw_name
   gid = OS.getgid(); ti.gid, ti.gname = gid, GR.getgrgid(gid).gr_name
   tf.addfile(ti, fileobj = StringIO(fest))
+
+  ## Now actually dump all of the individual directories.
   for dir, name in dirmap:
-    dump_dir(dir, tf, root)
+    dump_dir(name, dir, dirmap, tf, root)
+
+  ## We're done.
   tf.close()
 
 ###----- That's all, folks --------------------------------------------------