mdwsetup.py: Adjust exit status from `subprocess'.
[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 != '':
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, (me.sources[0], rc << 8)
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 --------------------------------------------------