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): | |
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 | ||
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() | |
207202a3 MW |
79 | if junk != '': raise ValueError \ |
80 | ("Child process `%s' produced unspected output %r" % (command, junk)) | |
a6bb85c1 | 81 | rc = kid.wait() |
207202a3 | 82 | if rc != 0: raise SubprocessFailure(command, rc) |
a6bb85c1 MW |
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 | """ | |
6f9cf579 | 100 | |
a6bb85c1 | 101 | def weird(what, word): |
207202a3 MW |
102 | raise ValueError \ |
103 | ("Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg)) | |
6f9cf579 MW |
104 | |
105 | spec = '%s >= %s' % (pkg, version) | |
106 | ||
a6bb85c1 | 107 | for word in progoutput(['pkg-config', '--cflags', spec]).split(): |
6f9cf579 MW |
108 | if word.startswith('-I'): INCLUDEDIRS.append(word[2:]) |
109 | else: weird('--cflags', word) | |
110 | ||
a6bb85c1 | 111 | for word in progoutput(['pkg-config', '--libs', spec]).split(): |
6f9cf579 MW |
112 | if word.startswith('-L'): LIBDIRS.append(word[2:]) |
113 | elif word.startswith('-l'): LIBS.append(word[2:]) | |
114 | else: weird('--libs', word) | |
a6bb85c1 MW |
115 | |
116 | ###-------------------------------------------------------------------------- | |
117 | ### Substituting variables in files. | |
118 | ||
f74ba2bb | 119 | class BaseGenFile (object): |
a6bb85c1 | 120 | """ |
f74ba2bb | 121 | A base class for file generators. |
a6bb85c1 | 122 | |
f74ba2bb MW |
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. | |
a6bb85c1 | 129 | """ |
f74ba2bb MW |
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) | |
a6bb85c1 | 148 | |
f74ba2bb | 149 | class Derive (BaseGenFile): |
a6bb85c1 MW |
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 | """ | |
f74ba2bb MW |
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) | |
a6bb85c1 | 167 | |
f74ba2bb | 168 | class Generate (BaseGenFile): |
a6bb85c1 MW |
169 | """ |
170 | Generate TARGET by running the SOURCE Python script. | |
171 | ||
172 | If SOURCE is omitted, replace the extension of TARGET by `.py'. | |
173 | """ | |
f74ba2bb MW |
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) | |
207202a3 | 181 | if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8) |
f74ba2bb MW |
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() | |
a6bb85c1 MW |
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. | |
f74ba2bb MW |
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. | |
a6bb85c1 MW |
201 | """ |
202 | version = progoutput(['./auto-version']) | |
203 | if writep: | |
2f8140f9 | 204 | with open('RELEASE.new', 'w') as ft: ft.write('%s\n' % version) |
a6bb85c1 MW |
205 | OS.rename('RELEASE.new', 'RELEASE') |
206 | return version | |
207 | ||
f74ba2bb MW |
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 | ||
a6bb85c1 | 358 | ###----- That's all, folks -------------------------------------------------- |