From f74ba2bb507cfeadd5518d5468c7ab7281b581b7 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Sat, 15 Jun 2013 22:08:36 +0100 Subject: [PATCH] mdwsetup.py: Integrate better with `distutils'. * Move the file-generation functionality into new commands, and arrange to have them cleaned. * Override `sdist' to dereference symbolic links, rather than including them literally into tarballs (which breaks for out-of-tree links), and to write `RELEASE' into the tarball. * Add a command to extract the archive name, for release management. --- mdwsetup.py | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 207 insertions(+), 34 deletions(-) diff --git a/mdwsetup.py b/mdwsetup.py index 7b864c8..a6ca3f4 100644 --- a/mdwsetup.py +++ b/mdwsetup.py @@ -31,6 +31,7 @@ import re as RE import subprocess as SUB import distutils.core as DC +import distutils.log as DL ###-------------------------------------------------------------------------- ### Random utilities. @@ -118,56 +119,74 @@ def pkg_config(pkg, version): ###-------------------------------------------------------------------------- ### 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. - If TARGET exists and was modified more recentently than any of its SOURCES - then it doesn't need updating. + 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. """ - 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, (source, rc) + 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 +197,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 +208,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 -------------------------------------------------- -- 2.11.0