mdwsetup.py (progoutput): Only read one byte to decide whether there is more.
[runlisp] / mdwsetup.py
index 9ce67e6..0210714 100644 (file)
@@ -7,28 +7,31 @@
 
 ###----- Licensing notice ---------------------------------------------------
 ###
 
 ###----- Licensing notice ---------------------------------------------------
 ###
-### This file is part of the Python interface to mLib.
+### This file is part of the Common Files Distribution (`common')
 ###
 ###
-### mLib/Python is free software; you can redistribute it and/or modify
+### `Common' is free software; you can redistribute it and/or modify
 ### it under the terms of the GNU General Public License as published by
 ### the Free Software Foundation; either version 2 of the License, or
 ### (at your option) any later version.
 ###
 ### it under the terms of the GNU General Public License as published by
 ### the Free Software Foundation; either version 2 of the License, or
 ### (at your option) any later version.
 ###
-### mLib/Python is distributed in the hope that it will be useful,
+### `Common' is distributed in the hope that it will be useful,
 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 ### GNU General Public License for more details.
 ###
 ### You should have received a copy of the GNU General Public License
 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 ### GNU General Public License for more details.
 ###
 ### You should have received a copy of the GNU General Public License
-### along with mLib/Python; if not, write to the Free Software Foundation,
+### along with `common'; if not, write to the Free Software Foundation,
 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
+from __future__ import with_statement
+
 import sys as SYS
 import os as OS
 import re as RE
 import subprocess as SUB
 
 import distutils.core as DC
 import sys as SYS
 import os as OS
 import re as RE
 import subprocess as SUB
 
 import distutils.core as DC
+import distutils.log as DL
 
 ###--------------------------------------------------------------------------
 ### Random utilities.
 
 ###--------------------------------------------------------------------------
 ### Random utilities.
@@ -56,10 +59,10 @@ class SubprocessFailure (Exception):
     me.file = file
     me.rc = rc
   def __str__(me):
     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)
 
     else:
       return '%s died inexplicably' % (me.file)
 
@@ -71,14 +74,15 @@ def progoutput(command):
   status zero.
   """
   kid = SUB.Popen(command, stdout = SUB.PIPE)
   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()
   rc = kid.wait()
-  if rc != 0:
-    raise SubprocessFailure, (command, rc)
+  if rc != 0: raise SubprocessFailure(command, rc)
   return out.rstrip('\n')
 
 ###--------------------------------------------------------------------------
   return out.rstrip('\n')
 
 ###--------------------------------------------------------------------------
@@ -96,85 +100,96 @@ def pkg_config(pkg, version):
   library-directory names are in LIBDIRS; and the library names themselves
   are in LIBS.
   """
   library-directory names are in LIBDIRS; and the library names themselves
   are in LIBS.
   """
-  spec = '%s >= %s' % (pkg, version)
+
   def weird(what, word):
   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.
 
 
 ###--------------------------------------------------------------------------
 ### 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.
   """
   """
   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'
-  ft = open(temp, 'w')
-  try:
-    fs = open(source, 'r')
-    try:
-      for line in fs:
-        ft.write(RX_SUBST.sub((lambda m: substmap[m.group(1)]), line))
-    finally:
-      fs.close()
-  finally:
-    ft.close()
-  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'.
   """
   """
   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'
-  ft = open(temp, 'w')
-  try:
-    rc = SUB.call([SYS.executable, source], stdout = ft)
-  finally:
-    ft.close()
-  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.
 
 ###--------------------------------------------------------------------------
 ### Discovering version numbers.
@@ -185,15 +200,165 @@ 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.
 
   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:
   """
   version = progoutput(['./auto-version'])
   if writep:
-    ft = open('RELEASE.new', 'w')
-    try:
-      ft.write('%s\n' % version)
-    finally:
-      ft.close()
+    with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % version)
     OS.rename('RELEASE.new', 'RELEASE')
   return version
 
     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 --------------------------------------------------
 ###----- That's all, folks --------------------------------------------------