chiark / gitweb /
mdwsetup.py (pkg_config): Rearrange and reformat.
[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 OS.WIFEXITED(me.rc):
63       return '%s failed (rc = %d)' % (me.file, OS.WEXITSTATUS(me.rc))
64     elif OS.WIFSIGNALED(me.rc):
65       return '%s died (signal %d)' % (me.file, OS.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 != '': raise ValueError \
80     ("Child process `%s' produced unspected output %r" % (command, junk))
81   rc = kid.wait()
82   if rc != 0: raise SubprocessFailure(command, rc)
83   return out.rstrip('\n')
84
85 ###--------------------------------------------------------------------------
86 ### External library packages.
87
88 INCLUDEDIRS = []
89 LIBDIRS = []
90 LIBS = []
91
92 def pkg_config(pkg, version):
93   """
94   Find the external package PKG and store the necessary compiler flags.
95
96   The include-directory names are stored in INCLUDEDIRS; the
97   library-directory names are in LIBDIRS; and the library names themselves
98   are in LIBS.
99   """
100
101   def weird(what, word):
102     raise ValueError \
103       ("Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg))
104
105   spec = '%s >= %s' % (pkg, version)
106
107   for word in progoutput(['pkg-config', '--cflags', spec]).split():
108     if word.startswith('-I'): INCLUDEDIRS.append(word[2:])
109     else: weird('--cflags', word)
110
111   for word in progoutput(['pkg-config', '--libs', spec]).split():
112     if word.startswith('-L'): LIBDIRS.append(word[2:])
113     elif word.startswith('-l'): LIBS.append(word[2:])
114     else: weird('--libs', word)
115
116 ###--------------------------------------------------------------------------
117 ### Substituting variables in files.
118
119 class BaseGenFile (object):
120   """
121   A base class for file generators.
122
123   Instances of subclasses are suitable for listing in the `genfiles'
124   attribute, passed to `setup'.
125
126   Subclasses need to implement `_gen', which should simply do the work of
127   generating the target file from its sources.  This class will print
128   progress messages and check whether the target actually needs regenerating.
129   """
130   def __init__(me, target, sources = []):
131     me.target = target
132     me.sources = sources
133   def _needs_update_p(me):
134     if not OS.path.exists(me.target): return True
135     t_target = OS.stat(me.target).st_mtime
136     for s in me.sources:
137       if OS.stat(s).st_mtime >= t_target: return True
138     return False
139   def gen(me, dry_run_p = False):
140     if not me._needs_update_p(): return
141     DL.log(DL.INFO, "generate `%s' from %s", me.target,
142            ', '.join("`%s'" % s for s in me.sources))
143     if not dry_run_p: me._gen()
144   def clean(me, dry_run_p):
145     if not OS.path.exists(me.target): return
146     DL.log(DL.INFO, "delete `%s'", me.target)
147     if not dry_run_p: OS.remove(me.target)
148
149 class Derive (BaseGenFile):
150   """
151   Derive TARGET from SOURCE by making simple substitutions.
152
153   The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
154   in the TARGET file.
155   """
156   RX_SUBST = RE.compile(r'\%(\w+)\%')
157   def __init__(me, target, source, substmap):
158     BaseGenFile.__init__(me, target, [source])
159     me._map = substmap
160   def _gen(me):
161     temp = me.target + '.new'
162     with open(temp, 'w') as ft:
163       with open(me.sources[0], 'r') as fs:
164         for line in fs:
165           ft.write(me.RX_SUBST.sub((lambda m: me._map[m.group(1)]), line))
166     OS.rename(temp, me.target)
167
168 class Generate (BaseGenFile):
169   """
170   Generate TARGET by running the SOURCE Python script.
171
172   If SOURCE is omitted, replace the extension of TARGET by `.py'.
173   """
174   def __init__(me, target, source = None):
175     if source is None: source = OS.path.splitext(target)[0] + '.py'
176     BaseGenFile.__init__(me, target, [source])
177   def _gen(me):
178     temp = me.target + '.new'
179     with open(temp, 'w') as ft:
180       rc = SUB.call([SYS.executable, me.sources[0]], stdout = ft)
181     if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8)
182     OS.rename(temp, me.target)
183
184 ## Backward compatibility.
185 def derive(target, source, substmap): Derive(target, source, substmap).gen()
186 def generate(target, source = None): Generate(target, source).gen()
187
188 ###--------------------------------------------------------------------------
189 ### Discovering version numbers.
190
191 def auto_version(writep = True):
192   """
193   Returns the package version number.
194
195   As a side-effect, if WRITEP is true, then write the version number to the
196   RELEASE file so that it gets included in distributions.
197
198   All of this is for backwards compatibility.  New projects should omit the
199   `version' keyword entirely and let `setup' discover it and write it into
200   tarballs automatically.
201   """
202   version = progoutput(['./auto-version'])
203   if writep:
204     with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % version)
205     OS.rename('RELEASE.new', 'RELEASE')
206   return version
207
208 ###--------------------------------------------------------------------------
209 ### Adding new commands.
210
211 CMDS = {}
212
213 class CommandClass (type):
214   """
215   Metaclass for command classes: automatically adds them to the `CMDS' map.
216   """
217   def __new__(cls, name, supers, dict):
218     c = super(CommandClass, cls).__new__(cls, name, supers, dict)
219     try: name = c.NAME
220     except AttributeError: pass
221     else: CMDS[name] = c
222     return c
223
224 class Command (DC.Command, object):
225   """
226   Base class for `mdwsetup' command classes.
227
228   This provides the automatic registration machinery, via the metaclass, and
229   also trivial implementations of various responsibilities of `DC.Command'
230   methods and attributes.
231   """
232   __metaclass__ = CommandClass
233   user_options = []
234   def initialize_options(me): pass
235   def finalize_options(me): pass
236   def run_subs(me):
237     for s in me.get_sub_commands(): me.run_command(s)
238
239 ###--------------------------------------------------------------------------
240 ### Some handy new commands.
241
242 class distdir (Command):
243   NAME = 'distdir'
244   description = "print the distribution directory name to stdout"
245   def run(me):
246     d = me.distribution
247     print '%s-%s' % (d.get_name(), d.get_version())
248
249 class build_gen(Command):
250   """
251   Generate files, according to the `genfiles'.
252
253   The `genfiles' keyword argument to `setup' lists a number of objects which
254   guide the generation of output files.  These objects must implement the
255   following methods.
256
257   clean(DRY_RUN_P)      Remove the output files.
258
259   gen(DRY_RUN_P)        Generate the output files, if they don't exist or are
260                         out of date with respect to their prerequisites.
261
262   If DRY_RUN_P is true then the methods must not actually do anything with a
263   lasting effect, but should print progress messages as usual.
264   """
265   NAME = 'build_gen'
266   description = "build generated source files"
267   def run(me):
268     d = me.distribution
269     for g in d.genfiles: g.gen(dry_run_p = me.dry_run)
270
271 from distutils.command.build import build as _build
272 class build (_build, Command):
273   ## Add `build_gen' early in the list of subcommands.
274   NAME = 'build'
275   sub_commands = [('build_gen', lambda me: me.distribution.genfiles)]
276   sub_commands += _build.sub_commands
277
278 class clean_gen(Command):
279   """
280   Remove the generated files, as listed in `genfiles'.
281
282   See the `build_gen' command for more detailed information.
283   """
284   NAME = 'clean_gen'
285   description = "clean generated source files"
286   def run(me):
287     d = me.distribution
288     for g in d.genfiles: g.clean(dry_run_p = me.dry_run)
289
290 class clean_others(Command):
291   """
292   Remove the files listed in the `cleanfiles' argument to `setup'.
293   """
294   NAME = 'clean_others'
295   description = "clean miscellaneous output files"
296   def run(me):
297     d = me.distribution
298     for f in d.cleanfiles:
299       if not OS.path.exists(f): continue
300       DL.log(DL.INFO, "delete `%s'", f)
301       if not me.dry_run: OS.remove(f)
302
303 from distutils.command.clean import clean as _clean
304 class clean (_clean, Command):
305   ## Add `clean_gen' and `clean_others' to the list of subcommands.
306   NAME = 'clean'
307   sub_commands = [('clean_gen', lambda me: me.distribution.genfiles),
308                   ('clean_others', lambda me: me.distribution.cleanfiles)]
309   sub_commands += _clean.sub_commands
310   def run(me):
311     me.run_subs()
312     _clean.run(me)
313
314 from distutils.command.sdist import sdist as _sdist
315 class sdist (_sdist, Command):
316   ## Write a `RELEASE' file to the output, if we extracted the version number
317   ## from version control.  Also arrange to dereference symbolic links while
318   ## copying.  Symlinks to directories will go horribly wrong, so don't do
319   ## that.
320   NAME = 'sdist'
321   def make_release_tree(me, base_dir, files):
322     _sdist.make_release_tree(me, base_dir, files)
323     d = me.distribution
324     if d._auto_version_p:
325       v = d.metadata.get_version()
326       DL.log(DL.INFO, "write `RELEASE' file: %s" % v)
327       with open(OS.path.join(base_dir, 'RELEASE'), 'w') as f:
328         f.write('%s\n' % v)
329   def copy_file(me, infile, outfile, link = None, *args, **kw):
330     if OS.path.islink(infile): link = None
331     return _sdist.copy_file(me, infile, outfile, link = link, *args, **kw)
332
333 ###--------------------------------------------------------------------------
334 ### Our own version of `setup'.
335
336 class Dist (DC.Distribution):
337   ## Like the usual version, but with some additional attributes to support
338   ## our enhanced commands.
339   def __init__(me, attrs = None):
340     me.genfiles = []
341     me.cleanfiles = []
342     me._auto_version_p = False
343     DC.Distribution.__init__(me, attrs)
344     if me.metadata.version is None:
345       me.metadata.version = auto_version(writep = False)
346       me._auto_version_p = True
347     me.cleanfiles = set(me.cleanfiles)
348     me.cleanfiles.add('MANIFEST')
349
350 def setup(cmdclass = {}, distclass = Dist, **kw):
351   ## Like the usual version, but provides defaults more suited to our
352   ## purposes.
353   cmds = dict()
354   cmds.update(CMDS)
355   cmds.update(cmdclass)
356   DC.setup(cmdclass = cmds, distclass = distclass, **kw)
357
358 ###----- That's all, folks --------------------------------------------------