mdwsetup.py (pkg_config): Check environment for settings.
[cfd] / 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)
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
88INCLUDEDIRS = []
89LIBDIRS = []
90LIBS = []
91
92def 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
ab5cf08d
MW
107 try: cflags = OS.environ["%s_CFLAGS" % pkg]
108 except KeyError: cflags = progoutput(['pkg-config', '--cflags', spec])
109 for word in cflags.split():
6f9cf579 110 if word.startswith('-I'): INCLUDEDIRS.append(word[2:])
94291754 111 else: weird('CFLAGS', word)
ab5cf08d
MW
112 try: libs = OS.environ["%s_LIBS" % pkg]
113 except KeyError: libs = progoutput(['pkg-config', '--libs', spec])
114 for word in libs.split():
6f9cf579
MW
115 if word.startswith('-L'): LIBDIRS.append(word[2:])
116 elif word.startswith('-l'): LIBS.append(word[2:])
94291754 117 else: weird('LIBS', word)
a6bb85c1
MW
118
119###--------------------------------------------------------------------------
120### Substituting variables in files.
121
f74ba2bb 122class 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 152class 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 171class 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)
207202a3 184 if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8)
f74ba2bb
MW
185 OS.rename(temp, me.target)
186
187## Backward compatibility.
188def derive(target, source, substmap): Derive(target, source, substmap).gen()
189def generate(target, source = None): Generate(target, source).gen()
a6bb85c1
MW
190
191###--------------------------------------------------------------------------
192### Discovering version numbers.
193
194def 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
214CMDS = {}
215
216class 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
227class 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
245class 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
252class 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
274from distutils.command.build import build as _build
275class 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
281class 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
293class 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
306from distutils.command.clean import clean as _clean
307class 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
317from distutils.command.sdist import sdist as _sdist
318class 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
339class 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
353def 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 --------------------------------------------------