chiark / gitweb /
debian/control: Recommend the other build tools we usually need.
[cfd] / mdwsetup.py
1 ### -*-python-*-
2 ###
3 ### Utility module for Python build systems
4 ###
5 ### (c) 2009 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Common Files Distribution (`common')
11 ###
12 ### `Common' is free software; you can redistribute it and/or modify
13 ### it under the terms of the GNU General Public License as published by
14 ### the Free Software Foundation; either version 2 of the License, or
15 ### (at your option) any later version.
16 ###
17 ### `Common' is distributed in the hope that it will be useful,
18 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 ### GNU General Public License for more details.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with `common'; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 from __future__ import with_statement
27
28 import sys as SYS
29 import os as OS
30 import re as RE
31 import subprocess as SUB
32
33 import distutils.core as DC
34 import distutils.log as DL
35
36 ###--------------------------------------------------------------------------
37 ### Random utilities.
38
39 def uniquify(seq):
40   """
41   Return a list of the elements of SEQ, with duplicates removed.
42
43   Only the first occurrence (according to `==') is left.
44   """
45   seen = {}
46   out = []
47   for item in seq:
48     if item not in seen:
49       seen[item] = True
50       out.append(item)
51   return out
52
53 ###--------------------------------------------------------------------------
54 ### Subprocess hacking.
55
56 class SubprocessFailure (Exception):
57   def __init__(me, file, rc):
58     me.args = (file, rc)
59     me.file = file
60     me.rc = rc
61   def __str__(me):
62     if WIFEXITED(me.rc):
63       return '%s failed (rc = %d)' % (me.file, WEXITSTATUS(me.rc))
64     elif WIFSIGNALED(me.rc):
65       return '%s died (signal %d)' % (me.file, WTERMSIG(me.rc))
66     else:
67       return '%s died inexplicably' % (me.file)
68
69 def progoutput(command):
70   """
71   Run the shell COMMAND and return its standard output.
72
73   The COMMAND must produce exactly one line of output, and must exit with
74   status zero.
75   """
76   kid = SUB.Popen(command, stdout = SUB.PIPE)
77   out = kid.stdout.readline()
78   junk = kid.stdout.read()
79   if junk != '':
80     raise ValueError, \
81           "Child process `%s' produced unspected output %r" % (command, junk)
82   rc = kid.wait()
83   if rc != 0:
84     raise SubprocessFailure, (command, rc)
85   return out.rstrip('\n')
86
87 ###--------------------------------------------------------------------------
88 ### External library packages.
89
90 INCLUDEDIRS = []
91 LIBDIRS = []
92 LIBS = []
93
94 def pkg_config(pkg, version):
95   """
96   Find the external package PKG and store the necessary compiler flags.
97
98   The include-directory names are stored in INCLUDEDIRS; the
99   library-directory names are in LIBDIRS; and the library names themselves
100   are in LIBS.
101   """
102   spec = '%s >= %s' % (pkg, version)
103   def weird(what, word):
104     raise ValueError, \
105           "Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg)
106   for word in progoutput(['pkg-config', '--cflags', spec]).split():
107     if word.startswith('-I'):
108       INCLUDEDIRS.append(word[2:])
109     else:
110       weird('--cflags', word)
111   for word in progoutput(['pkg-config', '--libs', spec]).split():
112     if word.startswith('-L'):
113       LIBDIRS.append(word[2:])
114     elif word.startswith('-l'):
115       LIBS.append(word[2:])
116     else:
117       weird('--libs', word)
118
119 ###--------------------------------------------------------------------------
120 ### Substituting variables in files.
121
122 class BaseGenFile (object):
123   """
124   A base class for file generators.
125
126   Instances of subclasses are suitable for listing in the `genfiles'
127   attribute, passed to `setup'.
128
129   Subclasses need to implement `_gen', which should simply do the work of
130   generating the target file from its sources.  This class will print
131   progress messages and check whether the target actually needs regenerating.
132   """
133   def __init__(me, target, sources = []):
134     me.target = target
135     me.sources = sources
136   def _needs_update_p(me):
137     if not OS.path.exists(me.target): return True
138     t_target = OS.stat(me.target).st_mtime
139     for s in me.sources:
140       if OS.stat(s).st_mtime >= t_target: return True
141     return False
142   def gen(me, dry_run_p = False):
143     if not me._needs_update_p(): return
144     DL.log(DL.INFO, "generate `%s' from %s", me.target,
145            ', '.join("`%s'" % s for s in me.sources))
146     if not dry_run_p: me._gen()
147   def clean(me, dry_run_p):
148     if not OS.path.exists(me.target): return
149     DL.log(DL.INFO, "delete `%s'", me.target)
150     if not dry_run_p: OS.remove(me.target)
151
152 class Derive (BaseGenFile):
153   """
154   Derive TARGET from SOURCE by making simple substitutions.
155
156   The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
157   in the TARGET file.
158   """
159   RX_SUBST = RE.compile(r'\%(\w+)\%')
160   def __init__(me, target, source, substmap):
161     BaseGenFile.__init__(me, target, [source])
162     me._map = substmap
163   def _gen(me):
164     temp = me.target + '.new'
165     with open(temp, 'w') as ft:
166       with open(me.sources[0], 'r') as fs:
167         for line in fs:
168           ft.write(me.RX_SUBST.sub((lambda m: me._map[m.group(1)]), line))
169     OS.rename(temp, me.target)
170
171 class Generate (BaseGenFile):
172   """
173   Generate TARGET by running the SOURCE Python script.
174
175   If SOURCE is omitted, replace the extension of TARGET by `.py'.
176   """
177   def __init__(me, target, source = None):
178     if source is None: source = OS.path.splitext(target)[0] + '.py'
179     BaseGenFile.__init__(me, target, [source])
180   def _gen(me):
181     temp = me.target + '.new'
182     with open(temp, 'w') as ft:
183       rc = SUB.call([SYS.executable, me.sources[0]], stdout = ft)
184     if rc != 0: raise SubprocessFailure, (source, rc)
185     OS.rename(temp, me.target)
186
187 ## Backward compatibility.
188 def derive(target, source, substmap): Derive(target, source, substmap).gen()
189 def generate(target, source = None): Generate(target, source).gen()
190
191 ###--------------------------------------------------------------------------
192 ### Discovering version numbers.
193
194 def auto_version(writep = True):
195   """
196   Returns the package version number.
197
198   As a side-effect, if WRITEP is true, then write the version number to the
199   RELEASE file so that it gets included in distributions.
200
201   All of this is for backwards compatibility.  New projects should omit the
202   `version' keyword entirely and let `setup' discover it and write it into
203   tarballs automatically.
204   """
205   version = progoutput(['./auto-version'])
206   if writep:
207     with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % version)
208     OS.rename('RELEASE.new', 'RELEASE')
209   return version
210
211 ###--------------------------------------------------------------------------
212 ### Adding new commands.
213
214 CMDS = {}
215
216 class CommandClass (type):
217   """
218   Metaclass for command classes: automatically adds them to the `CMDS' map.
219   """
220   def __new__(cls, name, supers, dict):
221     c = super(CommandClass, cls).__new__(cls, name, supers, dict)
222     try: name = c.NAME
223     except AttributeError: pass
224     else: CMDS[name] = c
225     return c
226
227 class Command (DC.Command, object):
228   """
229   Base class for `mdwsetup' command classes.
230
231   This provides the automatic registration machinery, via the metaclass, and
232   also trivial implementations of various responsibilities of `DC.Command'
233   methods and attributes.
234   """
235   __metaclass__ = CommandClass
236   user_options = []
237   def initialize_options(me): pass
238   def finalize_options(me): pass
239   def run_subs(me):
240     for s in me.get_sub_commands(): me.run_command(s)
241
242 ###--------------------------------------------------------------------------
243 ### Some handy new commands.
244
245 class distdir (Command):
246   NAME = 'distdir'
247   description = "print the distribution directory name to stdout"
248   def run(me):
249     d = me.distribution
250     print '%s-%s' % (d.get_name(), d.get_version())
251
252 class build_gen(Command):
253   """
254   Generate files, according to the `genfiles'.
255
256   The `genfiles' keyword argument to `setup' lists a number of objects which
257   guide the generation of output files.  These objects must implement the
258   following methods.
259
260   clean(DRY_RUN_P)      Remove the output files.
261
262   gen(DRY_RUN_P)        Generate the output files, if they don't exist or are
263                         out of date with respect to their prerequisites.
264
265   If DRY_RUN_P is true then the methods must not actually do anything with a
266   lasting effect, but should print progress messages as usual.
267   """
268   NAME = 'build_gen'
269   description = "build generated source files"
270   def run(me):
271     d = me.distribution
272     for g in d.genfiles: g.gen(dry_run_p = me.dry_run)
273
274 from distutils.command.build import build as _build
275 class build (_build, Command):
276   ## Add `build_gen' early in the list of subcommands.
277   NAME = 'build'
278   sub_commands = [('build_gen', lambda me: me.distribution.genfiles)]
279   sub_commands += _build.sub_commands
280
281 class clean_gen(Command):
282   """
283   Remove the generated files, as listed in `genfiles'.
284
285   See the `build_gen' command for more detailed information.
286   """
287   NAME = 'clean_gen'
288   description = "clean generated source files"
289   def run(me):
290     d = me.distribution
291     for g in d.genfiles: g.clean(dry_run_p = me.dry_run)
292
293 class clean_others(Command):
294   """
295   Remove the files listed in the `cleanfiles' argument to `setup'.
296   """
297   NAME = 'clean_others'
298   description = "clean miscellaneous output files"
299   def run(me):
300     d = me.distribution
301     for f in d.cleanfiles:
302       if not OS.path.exists(f): continue
303       DL.log(DL.INFO, "delete `%s'", f)
304       if not me.dry_run: OS.remove(f)
305
306 from distutils.command.clean import clean as _clean
307 class clean (_clean, Command):
308   ## Add `clean_gen' and `clean_others' to the list of subcommands.
309   NAME = 'clean'
310   sub_commands = [('clean_gen', lambda me: me.distribution.genfiles),
311                   ('clean_others', lambda me: me.distribution.cleanfiles)]
312   sub_commands += _clean.sub_commands
313   def run(me):
314     me.run_subs()
315     _clean.run(me)
316
317 from distutils.command.sdist import sdist as _sdist
318 class sdist (_sdist, Command):
319   ## Write a `RELEASE' file to the output, if we extracted the version number
320   ## from version control.  Also arrange to dereference symbolic links while
321   ## copying.  Symlinks to directories will go horribly wrong, so don't do
322   ## that.
323   NAME = 'sdist'
324   def make_release_tree(me, base_dir, files):
325     _sdist.make_release_tree(me, base_dir, files)
326     d = me.distribution
327     if d._auto_version_p:
328       v = d.metadata.get_version()
329       DL.log(DL.INFO, "write `RELEASE' file: %s" % v)
330       with open(OS.path.join(base_dir, 'RELEASE'), 'w') as f:
331         f.write('%s\n' % v)
332   def copy_file(me, infile, outfile, link = None, *args, **kw):
333     if OS.path.islink(infile): link = None
334     return _sdist.copy_file(me, infile, outfile, link = link, *args, **kw)
335
336 ###--------------------------------------------------------------------------
337 ### Our own version of `setup'.
338
339 class Dist (DC.Distribution):
340   ## Like the usual version, but with some additional attributes to support
341   ## our enhanced commands.
342   def __init__(me, attrs = None):
343     me.genfiles = []
344     me.cleanfiles = []
345     me._auto_version_p = False
346     DC.Distribution.__init__(me, attrs)
347     if me.metadata.version is None:
348       me.metadata.version = auto_version(writep = False)
349       me._auto_version_p = True
350     me.cleanfiles = set(me.cleanfiles)
351     me.cleanfiles.add('MANIFEST')
352
353 def setup(cmdclass = {}, distclass = Dist, **kw):
354   ## Like the usual version, but provides defaults more suited to our
355   ## purposes.
356   cmds = dict()
357   cmds.update(CMDS)
358   cmds.update(cmdclass)
359   DC.setup(cmdclass = cmds, distclass = distclass, **kw)
360
361 ###----- That's all, folks --------------------------------------------------