Commit | Line | Data |
---|---|---|
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 |
26 | from __future__ import with_statement |
27 | ||
a6bb85c1 MW |
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 | |
f74ba2bb | 34 | import distutils.log as DL |
a6bb85c1 MW |
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 WIFEXITED(me.rc): | |
63 | return '%s failed (rc = %d)' % (me.file, WEXITSTATUS(me.rc)) | |
64 | elif WIFSIGNALED(me.rc): | |
65 | return '%s died (signal %d)' % (me.file, 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 | ||
f74ba2bb | 122 | class BaseGenFile (object): |
a6bb85c1 | 123 | """ |
f74ba2bb | 124 | A base class for file generators. |
a6bb85c1 | 125 | |
f74ba2bb MW |
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. | |
a6bb85c1 | 132 | """ |
f74ba2bb MW |
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) | |
a6bb85c1 | 151 | |
f74ba2bb | 152 | class Derive (BaseGenFile): |
a6bb85c1 MW |
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 | """ | |
f74ba2bb MW |
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) | |
a6bb85c1 | 170 | |
f74ba2bb | 171 | class Generate (BaseGenFile): |
a6bb85c1 MW |
172 | """ |
173 | Generate TARGET by running the SOURCE Python script. | |
174 | ||
175 | If SOURCE is omitted, replace the extension of TARGET by `.py'. | |
176 | """ | |
f74ba2bb MW |
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, (source, rc) | |
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() | |
a6bb85c1 MW |
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. | |
f74ba2bb MW |
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. | |
a6bb85c1 MW |
204 | """ |
205 | version = progoutput(['./auto-version']) | |
206 | if writep: | |
2f8140f9 | 207 | with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % version) |
a6bb85c1 MW |
208 | OS.rename('RELEASE.new', 'RELEASE') |
209 | return version | |
210 | ||
f74ba2bb MW |
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 | ||
a6bb85c1 | 361 | ###----- That's all, folks -------------------------------------------------- |