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