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