--- /dev/null
+### -*-python-*-
+###
+### Utility module for Python build systems
+###
+### (c) 2009 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Common Files Distribution (`common')
+###
+### `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.
+###
+### `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
+### along with `common'; if not, write to the Free Software Foundation,
+### 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 signal as SIG
+import subprocess as SUB
+
+import distutils.core as DC
+import distutils.log as DL
+
+###--------------------------------------------------------------------------
+### Preliminaries.
+
+## Turn off Python's `SIGINT' handler. If we get stuck in a native-code loop
+## then ^C will just set a flag that will be noticed by the main interpreter
+## loop if we ever get to it again. And raising `SIGINT' is how Emacs `C-c
+## C-k' aborts a compilation, so this is really unsatisfactory.
+SIG.signal(SIG.SIGINT, SIG.SIG_DFL)
+
+###--------------------------------------------------------------------------
+### Compatibility hacks.
+
+def with_metaclass(meta, *supers):
+ return meta("#<anonymous base %s>" % meta.__name__,
+ supers or (object,), dict())
+
+###--------------------------------------------------------------------------
+### Random utilities.
+
+def uniquify(seq):
+ """
+ Return a list of the elements of SEQ, with duplicates removed.
+
+ Only the first occurrence (according to `==') is left.
+ """
+ seen = {}
+ out = []
+ for item in seq:
+ if item not in seen:
+ seen[item] = True
+ out.append(item)
+ return out
+
+###--------------------------------------------------------------------------
+### Subprocess hacking.
+
+class SubprocessFailure (Exception):
+ def __init__(me, file, rc):
+ me.args = (file, rc)
+ me.file = file
+ me.rc = rc
+ def __str__(me):
+ 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)
+
+def progoutput(command):
+ """
+ Run the shell COMMAND and return its standard output.
+
+ The COMMAND must produce exactly one line of output, and must exit with
+ status zero.
+ """
+ kid = SUB.Popen(command, stdout = SUB.PIPE, universal_newlines = True)
+ 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)
+ return out.rstrip('\n')
+
+###--------------------------------------------------------------------------
+### External library packages.
+
+INCLUDEDIRS = []
+LIBDIRS = []
+LIBS = []
+
+def pkg_config(pkg, version):
+ """
+ Find the external package PKG and store the necessary compiler flags.
+
+ The include-directory names are stored in INCLUDEDIRS; the
+ library-directory names are in LIBDIRS; and the library names themselves
+ are in LIBS.
+ """
+
+ def weird(what, 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.
+
+class BaseGenFile (object):
+ """
+ A base class for file generators.
+
+ Instances of subclasses are suitable for listing in the `genfiles'
+ attribute, passed to `setup'.
+
+ 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.
+ """
+ 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)
+
+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.
+ """
+ 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)
+
+class Generate (BaseGenFile):
+ """
+ Generate TARGET by running the SOURCE Python script.
+
+ If SOURCE is omitted, replace the extension of TARGET by `.py'.
+ """
+ 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.
+
+def auto_version(writep = True):
+ """
+ Returns the package version number.
+
+ 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:
+ with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % 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 (with_metaclass(CommandClass, 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 test (Command):
+ """
+ Run unit tests, according to the `unittests'.
+
+ The `unittests' keyword argument to `setup' lists module names (or other
+ things acceptable to the `loadTestsFromNames' test-loader method) to be
+ run. The build library directory is prepended to the load path before
+ running the tests to ensure that the newly built modules are tested. If
+ `unittest_dir' is set, then this is appended to the load path so that test
+ modules can be found there.
+ """
+ NAME = "test"
+ description = "run the included test suite"
+
+ user_options = \
+ [('build-lib=', 'b', "directory containing compiled moules"),
+ ('tests=', 't', "tests to run"),
+ ('verbose-test', 'V', "run tests verbosely")]
+
+ def initialize_options(me):
+ me.build_lib = None
+ me.verbose_test = False
+ me.tests = None
+ def finalize_options(me):
+ me.set_undefined_options('build', ('build_lib', 'build_lib'))
+ def run(me):
+ import unittest as U
+ d = me.distribution
+ SYS.path = [me.build_lib] + SYS.path
+ if d.unittest_dir is not None: SYS.path.append(d.unittest_dir)
+ if me.tests is not None: tests = me.tests.split(",")
+ else: tests = d.unittests
+ suite = U.defaultTestLoader.loadTestsFromNames(tests)
+ runner = U.TextTestRunner(verbosity = me.verbose_test and 2 or 1)
+ if me.dry_run: return
+ result = runner.run(suite)
+ if result.errors or result.failures or \
+ getattr(result, "unexpectedSuccesses", 0):
+ SYS.exit(2)
+
+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.unittest_dir = None
+ me.unittests = []
+ 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 --------------------------------------------------