322e194b6614de559c1d3037c811e419fe292302
[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 --------------------------------------------------