mdwsetup.py (progoutput): Only read one byte to decide whether there is more.
[runlisp] / mdwsetup.py
index 7b864c8..0210714 100644 (file)
@@ -31,6 +31,7 @@ import re as RE
 import subprocess as SUB
 
 import distutils.core as DC
+import distutils.log as DL
 
 ###--------------------------------------------------------------------------
 ### Random utilities.
@@ -58,10 +59,10 @@ class SubprocessFailure (Exception):
     me.file = file
     me.rc = rc
   def __str__(me):
-    if WIFEXITED(me.rc):
-      return '%s failed (rc = %d)' % (me.file, WEXITSTATUS(me.rc))
-    elif WIFSIGNALED(me.rc):
-      return '%s died (signal %d)' % (me.file, WTERMSIG(me.rc))
+    if OS.WIFEXITED(me.rc):
+      return '%s failed (rc = %d)' % (me.file, OS.WEXITSTATUS(me.rc))
+    elif OS.WIFSIGNALED(me.rc):
+      return '%s died (signal %d)' % (me.file, OS.WTERMSIG(me.rc))
     else:
       return '%s died inexplicably' % (me.file)
 
@@ -73,14 +74,15 @@ def progoutput(command):
   status zero.
   """
   kid = SUB.Popen(command, stdout = SUB.PIPE)
-  out = kid.stdout.readline()
-  junk = kid.stdout.read()
-  if junk != '':
-    raise ValueError, \
-          "Child process `%s' produced unspected output %r" % (command, junk)
+  try:
+    out = kid.stdout.readline()
+    junk = kid.stdout.read(1)
+  finally:
+    kid.stdout.close()
+  if junk != '': raise ValueError \
+    ("Child process `%s' produced unspected output %r" % (command, junk))
   rc = kid.wait()
-  if rc != 0:
-    raise SubprocessFailure, (command, rc)
+  if rc != 0: raise SubprocessFailure(command, rc)
   return out.rstrip('\n')
 
 ###--------------------------------------------------------------------------
@@ -98,76 +100,96 @@ def pkg_config(pkg, version):
   library-directory names are in LIBDIRS; and the library names themselves
   are in LIBS.
   """
-  spec = '%s >= %s' % (pkg, version)
+
   def weird(what, word):
-    raise ValueError, \
-          "Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg)
-  for word in progoutput(['pkg-config', '--cflags', spec]).split():
-    if word.startswith('-I'):
-      INCLUDEDIRS.append(word[2:])
-    else:
-      weird('--cflags', word)
-  for word in progoutput(['pkg-config', '--libs', spec]).split():
-    if word.startswith('-L'):
-      LIBDIRS.append(word[2:])
-    elif word.startswith('-l'):
-      LIBS.append(word[2:])
-    else:
-      weird('--libs', word)
+    raise ValueError \
+      ("Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg))
+
+  spec = '%s >= %s' % (pkg, version)
+
+  try: cflags = OS.environ["%s_CFLAGS" % pkg]
+  except KeyError: cflags = progoutput(['pkg-config', '--cflags', spec])
+  for word in cflags.split():
+    if word.startswith('-I'): INCLUDEDIRS.append(word[2:])
+    else: weird('CFLAGS', word)
+  try: libs = OS.environ["%s_LIBS" % pkg]
+  except KeyError: libs = progoutput(['pkg-config', '--libs', spec])
+  for word in libs.split():
+    if word.startswith('-L'): LIBDIRS.append(word[2:])
+    elif word.startswith('-l'): LIBS.append(word[2:])
+    else: weird('LIBS', word)
 
 ###--------------------------------------------------------------------------
 ### Substituting variables in files.
 
-def needs_update_p(target, sources):
+class BaseGenFile (object):
   """
-  Returns whether TARGET is out of date relative to its SOURCES.
+  A base class for file generators.
+
+  Instances of subclasses are suitable for listing in the `genfiles'
+  attribute, passed to `setup'.
 
-  If TARGET exists and was modified more recentently than any of its SOURCES
-  then it doesn't need updating.
+  Subclasses need to implement `_gen', which should simply do the work of
+  generating the target file from its sources.  This class will print
+  progress messages and check whether the target actually needs regenerating.
   """
-  if not OS.path.exists(target):
-    return True
-  t_target = OS.stat(target).st_mtime
-  for source in sources:
-    if OS.stat(source).st_mtime >= t_target:
-      return True
-  return False
+  def __init__(me, target, sources = []):
+    me.target = target
+    me.sources = sources
+  def _needs_update_p(me):
+    if not OS.path.exists(me.target): return True
+    t_target = OS.stat(me.target).st_mtime
+    for s in me.sources:
+      if OS.stat(s).st_mtime >= t_target: return True
+    return False
+  def gen(me, dry_run_p = False):
+    if not me._needs_update_p(): return
+    DL.log(DL.INFO, "generate `%s' from %s", me.target,
+           ', '.join("`%s'" % s for s in me.sources))
+    if not dry_run_p: me._gen()
+  def clean(me, dry_run_p):
+    if not OS.path.exists(me.target): return
+    DL.log(DL.INFO, "delete `%s'", me.target)
+    if not dry_run_p: OS.remove(me.target)
 
-RX_SUBST = RE.compile(r'\%(\w+)\%')
-def derive(target, source, substmap):
+class Derive (BaseGenFile):
   """
   Derive TARGET from SOURCE by making simple substitutions.
 
   The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
   in the TARGET file.
   """
-  if not needs_update_p(target, [source]):
-    return False
-  print "making `%s' from `%s'" % (target, source)
-  temp = target + '.new'
-  with open(temp, 'w') as ft:
-    with open(source, 'r') as fs:
-      for line in fs:
-        ft.write(RX_SUBST.sub((lambda m: substmap[m.group(1)]), line))
-  OS.rename(temp, target)
+  RX_SUBST = RE.compile(r'\%(\w+)\%')
+  def __init__(me, target, source, substmap):
+    BaseGenFile.__init__(me, target, [source])
+    me._map = substmap
+  def _gen(me):
+    temp = me.target + '.new'
+    with open(temp, 'w') as ft:
+      with open(me.sources[0], 'r') as fs:
+        for line in fs:
+          ft.write(me.RX_SUBST.sub((lambda m: me._map[m.group(1)]), line))
+    OS.rename(temp, me.target)
 
-def generate(target, source = None):
+class Generate (BaseGenFile):
   """
   Generate TARGET by running the SOURCE Python script.
 
   If SOURCE is omitted, replace the extension of TARGET by `.py'.
   """
-  if source is None:
-    source = OS.path.splitext(target)[0] + '.py'
-  if not needs_update_p(target, [source]):
-    return
-  print "making `%s' using `%s'" % (target, source)
-  temp = target + '.new'
-  with open(temp, 'w') as ft:
-    rc = SUB.call([SYS.executable, source], stdout = ft)
-  if rc != 0:
-    raise SubprocessFailure, (source, rc)
-  OS.rename(temp, target)
+  def __init__(me, target, source = None):
+    if source is None: source = OS.path.splitext(target)[0] + '.py'
+    BaseGenFile.__init__(me, target, [source])
+  def _gen(me):
+    temp = me.target + '.new'
+    with open(temp, 'w') as ft:
+      rc = SUB.call([SYS.executable, me.sources[0]], stdout = ft)
+    if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8)
+    OS.rename(temp, me.target)
+
+## Backward compatibility.
+def derive(target, source, substmap): Derive(target, source, substmap).gen()
+def generate(target, source = None): Generate(target, source).gen()
 
 ###--------------------------------------------------------------------------
 ### Discovering version numbers.
@@ -178,6 +200,10 @@ def auto_version(writep = True):
 
   As a side-effect, if WRITEP is true, then write the version number to the
   RELEASE file so that it gets included in distributions.
+
+  All of this is for backwards compatibility.  New projects should omit the
+  `version' keyword entirely and let `setup' discover it and write it into
+  tarballs automatically.
   """
   version = progoutput(['./auto-version'])
   if writep:
@@ -185,4 +211,154 @@ def auto_version(writep = True):
     OS.rename('RELEASE.new', 'RELEASE')
   return version
 
+###--------------------------------------------------------------------------
+### Adding new commands.
+
+CMDS = {}
+
+class CommandClass (type):
+  """
+  Metaclass for command classes: automatically adds them to the `CMDS' map.
+  """
+  def __new__(cls, name, supers, dict):
+    c = super(CommandClass, cls).__new__(cls, name, supers, dict)
+    try: name = c.NAME
+    except AttributeError: pass
+    else: CMDS[name] = c
+    return c
+
+class Command (DC.Command, object):
+  """
+  Base class for `mdwsetup' command classes.
+
+  This provides the automatic registration machinery, via the metaclass, and
+  also trivial implementations of various responsibilities of `DC.Command'
+  methods and attributes.
+  """
+  __metaclass__ = CommandClass
+  user_options = []
+  def initialize_options(me): pass
+  def finalize_options(me): pass
+  def run_subs(me):
+    for s in me.get_sub_commands(): me.run_command(s)
+
+###--------------------------------------------------------------------------
+### Some handy new commands.
+
+class distdir (Command):
+  NAME = 'distdir'
+  description = "print the distribution directory name to stdout"
+  def run(me):
+    d = me.distribution
+    print '%s-%s' % (d.get_name(), d.get_version())
+
+class build_gen(Command):
+  """
+  Generate files, according to the `genfiles'.
+
+  The `genfiles' keyword argument to `setup' lists a number of objects which
+  guide the generation of output files.  These objects must implement the
+  following methods.
+
+  clean(DRY_RUN_P)      Remove the output files.
+
+  gen(DRY_RUN_P)        Generate the output files, if they don't exist or are
+                        out of date with respect to their prerequisites.
+
+  If DRY_RUN_P is true then the methods must not actually do anything with a
+  lasting effect, but should print progress messages as usual.
+  """
+  NAME = 'build_gen'
+  description = "build generated source files"
+  def run(me):
+    d = me.distribution
+    for g in d.genfiles: g.gen(dry_run_p = me.dry_run)
+
+from distutils.command.build import build as _build
+class build (_build, Command):
+  ## Add `build_gen' early in the list of subcommands.
+  NAME = 'build'
+  sub_commands = [('build_gen', lambda me: me.distribution.genfiles)]
+  sub_commands += _build.sub_commands
+
+class clean_gen(Command):
+  """
+  Remove the generated files, as listed in `genfiles'.
+
+  See the `build_gen' command for more detailed information.
+  """
+  NAME = 'clean_gen'
+  description = "clean generated source files"
+  def run(me):
+    d = me.distribution
+    for g in d.genfiles: g.clean(dry_run_p = me.dry_run)
+
+class clean_others(Command):
+  """
+  Remove the files listed in the `cleanfiles' argument to `setup'.
+  """
+  NAME = 'clean_others'
+  description = "clean miscellaneous output files"
+  def run(me):
+    d = me.distribution
+    for f in d.cleanfiles:
+      if not OS.path.exists(f): continue
+      DL.log(DL.INFO, "delete `%s'", f)
+      if not me.dry_run: OS.remove(f)
+
+from distutils.command.clean import clean as _clean
+class clean (_clean, Command):
+  ## Add `clean_gen' and `clean_others' to the list of subcommands.
+  NAME = 'clean'
+  sub_commands = [('clean_gen', lambda me: me.distribution.genfiles),
+                  ('clean_others', lambda me: me.distribution.cleanfiles)]
+  sub_commands += _clean.sub_commands
+  def run(me):
+    me.run_subs()
+    _clean.run(me)
+
+from distutils.command.sdist import sdist as _sdist
+class sdist (_sdist, Command):
+  ## Write a `RELEASE' file to the output, if we extracted the version number
+  ## from version control.  Also arrange to dereference symbolic links while
+  ## copying.  Symlinks to directories will go horribly wrong, so don't do
+  ## that.
+  NAME = 'sdist'
+  def make_release_tree(me, base_dir, files):
+    _sdist.make_release_tree(me, base_dir, files)
+    d = me.distribution
+    if d._auto_version_p:
+      v = d.metadata.get_version()
+      DL.log(DL.INFO, "write `RELEASE' file: %s" % v)
+      with open(OS.path.join(base_dir, 'RELEASE'), 'w') as f:
+        f.write('%s\n' % v)
+  def copy_file(me, infile, outfile, link = None, *args, **kw):
+    if OS.path.islink(infile): link = None
+    return _sdist.copy_file(me, infile, outfile, link = link, *args, **kw)
+
+###--------------------------------------------------------------------------
+### Our own version of `setup'.
+
+class Dist (DC.Distribution):
+  ## Like the usual version, but with some additional attributes to support
+  ## our enhanced commands.
+  def __init__(me, attrs = None):
+    me.genfiles = []
+    me.cleanfiles = []
+    me._auto_version_p = False
+    DC.Distribution.__init__(me, attrs)
+    if me.metadata.version is None:
+      me.metadata.version = auto_version(writep = False)
+      me._auto_version_p = True
+    me.cleanfiles = set(me.cleanfiles)
+    me.cleanfiles.add('MANIFEST')
+
+def setup(cmdclass = {}, distclass = Dist, **kw):
+  ## Like the usual version, but provides defaults more suited to our
+  ## purposes.
+  cmds = dict()
+  cmds.update(CMDS)
+  cmds.update(cmdclass)
+  DC.setup(cmdclass = cmds, distclass = distclass, **kw)
+
 ###----- That's all, folks --------------------------------------------------