mdwsetup.py (progoutput): Only read one byte to decide whether there is more.
[runlisp] / mdwsetup.py
CommitLineData
a6bb85c1
MW
1### -*-python-*-
2###
3### Utility module for Python build systems
4###
5### (c) 2009 Straylight/Edgeware
6###
7
8###----- Licensing notice ---------------------------------------------------
9###
f0c2c8a0 10### This file is part of the Common Files Distribution (`common')
a6bb85c1 11###
f0c2c8a0 12### `Common' is free software; you can redistribute it and/or modify
a6bb85c1
MW
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###
f0c2c8a0 17### `Common' is distributed in the hope that it will be useful,
a6bb85c1
MW
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
f0c2c8a0 23### along with `common'; if not, write to the Free Software Foundation,
a6bb85c1
MW
24### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
2f8140f9
MW
26from __future__ import with_statement
27
a6bb85c1
MW
28import sys as SYS
29import os as OS
30import re as RE
31import subprocess as SUB
32
33import distutils.core as DC
f74ba2bb 34import distutils.log as DL
a6bb85c1
MW
35
36###--------------------------------------------------------------------------
37### Random utilities.
38
39def 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
56class 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):
25d89871
MW
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))
a6bb85c1
MW
66 else:
67 return '%s died inexplicably' % (me.file)
68
69def 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)
f3c13bfa
MW
77 try:
78 out = kid.stdout.readline()
514f1de6 79 junk = kid.stdout.read(1)
f3c13bfa
MW
80 finally:
81 kid.stdout.close()
207202a3
MW
82 if junk != '': raise ValueError \
83 ("Child process `%s' produced unspected output %r" % (command, junk))
a6bb85c1 84 rc = kid.wait()
207202a3 85 if rc != 0: raise SubprocessFailure(command, rc)
a6bb85c1
MW
86 return out.rstrip('\n')
87
88###--------------------------------------------------------------------------
89### External library packages.
90
91INCLUDEDIRS = []
92LIBDIRS = []
93LIBS = []
94
95def 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 """
6f9cf579 103
a6bb85c1 104 def weird(what, word):
207202a3
MW
105 raise ValueError \
106 ("Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg))
6f9cf579
MW
107
108 spec = '%s >= %s' % (pkg, version)
109
ab5cf08d
MW
110 try: cflags = OS.environ["%s_CFLAGS" % pkg]
111 except KeyError: cflags = progoutput(['pkg-config', '--cflags', spec])
112 for word in cflags.split():
6f9cf579 113 if word.startswith('-I'): INCLUDEDIRS.append(word[2:])
94291754 114 else: weird('CFLAGS', word)
ab5cf08d
MW
115 try: libs = OS.environ["%s_LIBS" % pkg]
116 except KeyError: libs = progoutput(['pkg-config', '--libs', spec])
117 for word in libs.split():
6f9cf579
MW
118 if word.startswith('-L'): LIBDIRS.append(word[2:])
119 elif word.startswith('-l'): LIBS.append(word[2:])
94291754 120 else: weird('LIBS', word)
a6bb85c1
MW
121
122###--------------------------------------------------------------------------
123### Substituting variables in files.
124
f74ba2bb 125class BaseGenFile (object):
a6bb85c1 126 """
f74ba2bb 127 A base class for file generators.
a6bb85c1 128
f74ba2bb
MW
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.
a6bb85c1 135 """
f74ba2bb
MW
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)
a6bb85c1 154
f74ba2bb 155class Derive (BaseGenFile):
a6bb85c1
MW
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 """
f74ba2bb
MW
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)
a6bb85c1 173
f74ba2bb 174class Generate (BaseGenFile):
a6bb85c1
MW
175 """
176 Generate TARGET by running the SOURCE Python script.
177
178 If SOURCE is omitted, replace the extension of TARGET by `.py'.
179 """
f74ba2bb
MW
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)
207202a3 187 if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8)
f74ba2bb
MW
188 OS.rename(temp, me.target)
189
190## Backward compatibility.
191def derive(target, source, substmap): Derive(target, source, substmap).gen()
192def generate(target, source = None): Generate(target, source).gen()
a6bb85c1
MW
193
194###--------------------------------------------------------------------------
195### Discovering version numbers.
196
197def 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.
f74ba2bb
MW
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.
a6bb85c1
MW
207 """
208 version = progoutput(['./auto-version'])
209 if writep:
2f8140f9 210 with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % version)
a6bb85c1
MW
211 OS.rename('RELEASE.new', 'RELEASE')
212 return version
213
f74ba2bb
MW
214###--------------------------------------------------------------------------
215### Adding new commands.
216
217CMDS = {}
218
219class 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
230class 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
248class 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
255class 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
277from distutils.command.build import build as _build
278class 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
284class 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
296class 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
309from distutils.command.clean import clean as _clean
310class 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
320from distutils.command.sdist import sdist as _sdist
321class 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
342class 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
356def 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
a6bb85c1 364###----- That's all, folks --------------------------------------------------