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