3 ### Utility module for Python build systems
5 ### (c) 2009 Straylight/Edgeware
8 ###----- Licensing notice ---------------------------------------------------
10 ### This file is part of the Common Files Distribution (`common')
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.
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.
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.
26 from __future__
import with_statement
31 import subprocess
as SUB
33 import distutils
.core
as DC
34 import distutils
.log
as DL
36 ###--------------------------------------------------------------------------
37 ### Compatibility hacks.
39 def with_metaclass(meta
, *supers
):
40 return meta("#<anonymous base %s>" % meta
.__name__
,
41 supers
or (object,), dict())
43 ###--------------------------------------------------------------------------
48 Return a list of the elements of SEQ, with duplicates removed.
50 Only the first occurrence (according to `==') is left.
60 ###--------------------------------------------------------------------------
61 ### Subprocess hacking.
63 class SubprocessFailure (Exception):
64 def __init__(me
, file, rc
):
69 if OS
.WIFEXITED(me
.rc
):
70 return '%s failed (rc = %d)' %
(me
.file, OS
.WEXITSTATUS(me
.rc
))
71 elif OS
.WIFSIGNALED(me
.rc
):
72 return '%s died (signal %d)' %
(me
.file, OS
.WTERMSIG(me
.rc
))
74 return '%s died inexplicably' %
(me
.file)
76 def progoutput(command
):
78 Run the shell COMMAND and return its standard output.
80 The COMMAND must produce exactly one line of output, and must exit with
83 kid
= SUB
.Popen(command
, stdout
= SUB
.PIPE
, universal_newlines
= True)
85 out
= kid
.stdout
.readline()
86 junk
= kid
.stdout
.read(1)
89 if junk
!= '': raise ValueError \
90 ("Child process `%s' produced unspected output %r" %
(command
, junk
))
92 if rc
!= 0: raise SubprocessFailure(command
, rc
)
93 return out
.rstrip('\n')
95 ###--------------------------------------------------------------------------
96 ### External library packages.
102 def pkg_config(pkg
, version
):
104 Find the external package PKG and store the necessary compiler flags.
106 The include-directory names are stored in INCLUDEDIRS; the
107 library-directory names are in LIBDIRS; and the library names themselves
111 def weird(what
, word
):
113 ("Unexpected `%s' item `%s' from package `%s'" %
(what
, word
, pkg
))
115 spec
= '%s >= %s' %
(pkg
, version
)
117 try: cflags
= OS
.environ
["%s_CFLAGS" % pkg
]
118 except KeyError: cflags
= progoutput(['pkg-config', '--cflags', spec
])
119 for word
in cflags
.split():
120 if word
.startswith('-I'): INCLUDEDIRS
.append(word
[2:])
121 else: weird('CFLAGS', word
)
122 try: libs
= OS
.environ
["%s_LIBS" % pkg
]
123 except KeyError: libs
= progoutput(['pkg-config', '--libs', spec
])
124 for word
in libs
.split():
125 if word
.startswith('-L'): LIBDIRS
.append(word
[2:])
126 elif word
.startswith('-l'): LIBS
.append(word
[2:])
127 else: weird('LIBS', word
)
129 ###--------------------------------------------------------------------------
130 ### Substituting variables in files.
132 class BaseGenFile (object):
134 A base class for file generators.
136 Instances of subclasses are suitable for listing in the `genfiles'
137 attribute, passed to `setup'.
139 Subclasses need to implement `_gen', which should simply do the work of
140 generating the target file from its sources. This class will print
141 progress messages and check whether the target actually needs regenerating.
143 def __init__(me
, target
, sources
= []):
146 def _needs_update_p(me
):
147 if not OS
.path
.exists(me
.target
): return True
148 t_target
= OS
.stat(me
.target
).st_mtime
150 if OS
.stat(s
).st_mtime
>= t_target
: return True
152 def gen(me
, dry_run_p
= False):
153 if not me
._needs_update_p(): return
154 DL
.log(DL
.INFO
, "generate `%s' from %s", me
.target
,
155 ', '.join("`%s'" % s for s in me
.sources
))
156 if not dry_run_p
: me
._gen()
157 def clean(me
, dry_run_p
):
158 if not OS
.path
.exists(me
.target
): return
159 DL
.log(DL
.INFO
, "delete `%s'", me
.target
)
160 if not dry_run_p
: OS
.remove(me
.target
)
162 class Derive (BaseGenFile
):
164 Derive TARGET from SOURCE by making simple substitutions.
166 The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
169 RX_SUBST
= RE
.compile(r
'\%(\w+)\%')
170 def __init__(me
, target
, source
, substmap
):
171 BaseGenFile
.__init__(me
, target
, [source
])
174 temp
= me
.target
+ '.new'
175 with
open(temp
, 'w') as ft
:
176 with
open(me
.sources
[0], 'r') as fs
:
178 ft
.write(me
.RX_SUBST
.sub((lambda m
: me
._map
[m
.group(1)]), line
))
179 OS
.rename(temp
, me
.target
)
181 class Generate (BaseGenFile
):
183 Generate TARGET by running the SOURCE Python script.
185 If SOURCE is omitted, replace the extension of TARGET by `.py'.
187 def __init__(me
, target
, source
= None):
188 if source
is None: source
= OS
.path
.splitext(target
)[0] + '.py'
189 BaseGenFile
.__init__(me
, target
, [source
])
191 temp
= me
.target
+ '.new'
192 with
open(temp
, 'w') as ft
:
193 rc
= SUB
.call([SYS
.executable
, me
.sources
[0]], stdout
= ft
)
194 if rc
!= 0: raise SubprocessFailure(me
.sources
[0], rc
<< 8)
195 OS
.rename(temp
, me
.target
)
197 ## Backward compatibility.
198 def derive(target
, source
, substmap
): Derive(target
, source
, substmap
).gen()
199 def generate(target
, source
= None): Generate(target
, source
).gen()
201 ###--------------------------------------------------------------------------
202 ### Discovering version numbers.
204 def auto_version(writep
= True):
206 Returns the package version number.
208 As a side-effect, if WRITEP is true, then write the version number to the
209 RELEASE file so that it gets included in distributions.
211 All of this is for backwards compatibility. New projects should omit the
212 `version' keyword entirely and let `setup' discover it and write it into
213 tarballs automatically.
215 version
= progoutput(['./auto-version'])
217 with
open('RELEASE.new', 'w') as ft
: ft
.write('%s\n' % version
)
218 OS
.rename('RELEASE.new', 'RELEASE')
221 ###--------------------------------------------------------------------------
222 ### Adding new commands.
226 class CommandClass (type):
228 Metaclass for command classes: automatically adds them to the `CMDS' map.
230 def __new__(cls
, name
, supers
, dict):
231 c
= super(CommandClass
, cls
).__new__(cls
, name
, supers
, dict)
233 except AttributeError: pass
237 class Command (with_metaclass(CommandClass
, DC
.Command
, object)):
239 Base class for `mdwsetup' command classes.
241 This provides the automatic registration machinery, via the metaclass, and
242 also trivial implementations of various responsibilities of `DC.Command'
243 methods and attributes.
245 __metaclass__
= CommandClass
247 def initialize_options(me
): pass
248 def finalize_options(me
): pass
250 for s
in me
.get_sub_commands(): me
.run_command(s
)
252 ###--------------------------------------------------------------------------
253 ### Some handy new commands.
255 class distdir (Command
):
257 description
= "print the distribution directory name to stdout"
260 print('%s-%s' %
(d
.get_name(), d
.get_version()))
262 class build_gen (Command
):
264 Generate files, according to the `genfiles'.
266 The `genfiles' keyword argument to `setup' lists a number of objects which
267 guide the generation of output files. These objects must implement the
270 clean(DRY_RUN_P) Remove the output files.
272 gen(DRY_RUN_P) Generate the output files, if they don't exist or are
273 out of date with respect to their prerequisites.
275 If DRY_RUN_P is true then the methods must not actually do anything with a
276 lasting effect, but should print progress messages as usual.
279 description
= "build generated source files"
282 for g
in d
.genfiles
: g
.gen(dry_run_p
= me
.dry_run
)
284 from distutils
.command
.build
import build
as _build
285 class build (_build
, Command
):
286 ## Add `build_gen' early in the list of subcommands.
288 sub_commands
= [('build_gen', lambda me
: me
.distribution
.genfiles
)]
289 sub_commands
+= _build
.sub_commands
291 class test (Command
):
293 Run unit tests, according to the `unittests'.
295 The `unittests' keyword argument to `setup' lists module names (or other
296 things acceptable to the `loadTestsFromNames' test-loader method) to be
297 run. The build library directory is prepended to the load path before
298 running the tests to ensure that the newly built modules are tested. If
299 `unittest_dir' is set, then this is appended to the load path so that test
300 modules can be found there.
303 description
= "run the included test suite"
306 [('build-lib=', 'b', "directory containing compiled moules"),
307 ('tests=', 't', "tests to run"),
308 ('verbose-test', 'V', "run tests verbosely")]
310 def initialize_options(me
):
312 me
.verbose_test
= False
314 def finalize_options(me
):
315 me
.set_undefined_options('build', ('build_lib', 'build_lib'))
319 SYS
.path
= [me
.build_lib
] + SYS
.path
320 if d
.unittest_dir
is not None: SYS
.path
.append(d
.unittest_dir
)
321 if me
.tests
is not None: tests
= me
.tests
.split(",")
322 else: tests
= d
.unittests
323 suite
= U
.defaultTestLoader
.loadTestsFromNames(tests
)
324 runner
= U
.TextTestRunner(verbosity
= me
.verbose_test
and 2 or 1)
325 if me
.dry_run
: return
326 result
= runner
.run(suite
)
327 if result
.errors
or result
.failures
or \
328 getattr(result
, "unexpectedSuccesses", 0):
331 class clean_gen (Command
):
333 Remove the generated files, as listed in `genfiles'.
335 See the `build_gen' command for more detailed information.
338 description
= "clean generated source files"
341 for g
in d
.genfiles
: g
.clean(dry_run_p
= me
.dry_run
)
343 class clean_others (Command
):
345 Remove the files listed in the `cleanfiles' argument to `setup'.
347 NAME
= 'clean_others'
348 description
= "clean miscellaneous output files"
351 for f
in d
.cleanfiles
:
352 if not OS
.path
.exists(f
): continue
353 DL
.log(DL
.INFO
, "delete `%s'", f
)
354 if not me
.dry_run
: OS
.remove(f
)
356 from distutils
.command
.clean
import clean
as _clean
357 class clean (_clean
, Command
):
358 ## Add `clean_gen' and `clean_others' to the list of subcommands.
360 sub_commands
= [('clean_gen', lambda me
: me
.distribution
.genfiles
),
361 ('clean_others', lambda me
: me
.distribution
.cleanfiles
)]
362 sub_commands
+= _clean
.sub_commands
367 from distutils
.command
.sdist
import sdist
as _sdist
368 class sdist (_sdist
, Command
):
369 ## Write a `RELEASE' file to the output, if we extracted the version number
370 ## from version control. Also arrange to dereference symbolic links while
371 ## copying. Symlinks to directories will go horribly wrong, so don't do
374 def make_release_tree(me
, base_dir
, files
):
375 _sdist
.make_release_tree(me
, base_dir
, files
)
377 if d
._auto_version_p
:
378 v
= d
.metadata
.get_version()
379 DL
.log(DL
.INFO
, "write `RELEASE' file: %s" % v
)
380 with
open(OS
.path
.join(base_dir
, 'RELEASE'), 'w') as f
:
382 def copy_file(me
, infile
, outfile
, link
= None, *args
, **kw
):
383 if OS
.path
.islink(infile
): link
= None
384 return _sdist
.copy_file(me
, infile
, outfile
, link
= link
, *args
, **kw
)
386 ###--------------------------------------------------------------------------
387 ### Our own version of `setup'.
389 class Dist (DC
.Distribution
):
390 ## Like the usual version, but with some additional attributes to support
391 ## our enhanced commands.
392 def __init__(me
, attrs
= None):
394 me
.unittest_dir
= None
397 me
._auto_version_p
= False
398 DC
.Distribution
.__init__(me
, attrs
)
399 if me
.metadata
.version
is None:
400 me
.metadata
.version
= auto_version(writep
= False)
401 me
._auto_version_p
= True
402 me
.cleanfiles
= set(me
.cleanfiles
)
403 me
.cleanfiles
.add('MANIFEST')
405 def setup(cmdclass
= {}, distclass
= Dist
, **kw
):
406 ## Like the usual version, but provides defaults more suited to our
410 cmds
.update(cmdclass
)
411 DC
.setup(cmdclass
= cmds
, distclass
= distclass
, **kw
)
413 ###----- That's all, folks --------------------------------------------------