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
32 import subprocess
as SUB
34 import distutils
.core
as DC
35 import distutils
.log
as DL
37 ###--------------------------------------------------------------------------
40 ## Turn off Python's `SIGINT' handler. If we get stuck in a native-code loop
41 ## then ^C will just set a flag that will be noticed by the main interpreter
42 ## loop if we ever get to it again. And raising `SIGINT' is how Emacs `C-c
43 ## C-k' aborts a compilation, so this is really unsatisfactory.
44 SIG
.signal(SIG
.SIGINT
, SIG
.SIG_DFL
)
46 ###--------------------------------------------------------------------------
47 ### Compatibility hacks.
49 def with_metaclass(meta
, *supers
):
50 return meta("#<anonymous base %s>" % meta
.__name__
,
51 supers
or (object,), dict())
53 ###--------------------------------------------------------------------------
58 Return a list of the elements of SEQ, with duplicates removed.
60 Only the first occurrence (according to `==') is left.
70 ###--------------------------------------------------------------------------
71 ### Subprocess hacking.
73 class SubprocessFailure (Exception):
74 def __init__(me
, file, rc
):
79 if OS
.WIFEXITED(me
.rc
):
80 return '%s failed (rc = %d)' %
(me
.file, OS
.WEXITSTATUS(me
.rc
))
81 elif OS
.WIFSIGNALED(me
.rc
):
82 return '%s died (signal %d)' %
(me
.file, OS
.WTERMSIG(me
.rc
))
84 return '%s died inexplicably' %
(me
.file)
86 def progoutput(command
):
88 Run the shell COMMAND and return its standard output.
90 The COMMAND must produce exactly one line of output, and must exit with
93 kid
= SUB
.Popen(command
, stdout
= SUB
.PIPE
, universal_newlines
= True)
95 out
= kid
.stdout
.readline()
96 junk
= kid
.stdout
.read(1)
99 if junk
!= '': raise ValueError \
100 ("Child process `%s' produced unspected output %r" %
(command
, junk
))
102 if rc
!= 0: raise SubprocessFailure(command
, rc
)
103 return out
.rstrip('\n')
105 ###--------------------------------------------------------------------------
106 ### External library packages.
112 def pkg_config(pkg
, version
):
114 Find the external package PKG and store the necessary compiler flags.
116 The include-directory names are stored in INCLUDEDIRS; the
117 library-directory names are in LIBDIRS; and the library names themselves
121 def weird(what
, word
):
123 ("Unexpected `%s' item `%s' from package `%s'" %
(what
, word
, pkg
))
125 spec
= '%s >= %s' %
(pkg
, version
)
127 try: cflags
= OS
.environ
["%s_CFLAGS" % pkg
]
128 except KeyError: cflags
= progoutput(['pkg-config', '--cflags', spec
])
129 for word
in cflags
.split():
130 if word
.startswith('-I'): INCLUDEDIRS
.append(word
[2:])
131 else: weird('CFLAGS', word
)
132 try: libs
= OS
.environ
["%s_LIBS" % pkg
]
133 except KeyError: libs
= progoutput(['pkg-config', '--libs', spec
])
134 for word
in libs
.split():
135 if word
.startswith('-L'): LIBDIRS
.append(word
[2:])
136 elif word
.startswith('-l'): LIBS
.append(word
[2:])
137 else: weird('LIBS', word
)
139 ###--------------------------------------------------------------------------
140 ### Substituting variables in files.
142 class BaseGenFile (object):
144 A base class for file generators.
146 Instances of subclasses are suitable for listing in the `genfiles'
147 attribute, passed to `setup'.
149 Subclasses need to implement `_gen', which should simply do the work of
150 generating the target file from its sources. This class will print
151 progress messages and check whether the target actually needs regenerating.
153 def __init__(me
, target
, sources
= []):
156 def _needs_update_p(me
):
157 if not OS
.path
.exists(me
.target
): return True
158 t_target
= OS
.stat(me
.target
).st_mtime
160 if OS
.stat(s
).st_mtime
>= t_target
: return True
162 def gen(me
, dry_run_p
= False):
163 if not me
._needs_update_p(): return
164 DL
.log(DL
.INFO
, "generate `%s' from %s", me
.target
,
165 ', '.join("`%s'" % s for s in me
.sources
))
166 if not dry_run_p
: me
._gen()
167 def clean(me
, dry_run_p
):
168 if not OS
.path
.exists(me
.target
): return
169 DL
.log(DL
.INFO
, "delete `%s'", me
.target
)
170 if not dry_run_p
: OS
.remove(me
.target
)
172 class Derive (BaseGenFile
):
174 Derive TARGET from SOURCE by making simple substitutions.
176 The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
179 RX_SUBST
= RE
.compile(r
'\%(\w+)\%')
180 def __init__(me
, target
, source
, substmap
):
181 BaseGenFile
.__init__(me
, target
, [source
])
184 temp
= me
.target
+ '.new'
185 with
open(temp
, 'w') as ft
:
186 with
open(me
.sources
[0], 'r') as fs
:
188 ft
.write(me
.RX_SUBST
.sub((lambda m
: me
._map
[m
.group(1)]), line
))
189 OS
.rename(temp
, me
.target
)
191 class Generate (BaseGenFile
):
193 Generate TARGET by running the SOURCE Python script.
195 If SOURCE is omitted, replace the extension of TARGET by `.py'.
197 def __init__(me
, target
, source
= None):
198 if source
is None: source
= OS
.path
.splitext(target
)[0] + '.py'
199 BaseGenFile
.__init__(me
, target
, [source
])
201 temp
= me
.target
+ '.new'
202 with
open(temp
, 'w') as ft
:
203 rc
= SUB
.call([SYS
.executable
, me
.sources
[0]], stdout
= ft
)
204 if rc
!= 0: raise SubprocessFailure(me
.sources
[0], rc
<< 8)
205 OS
.rename(temp
, me
.target
)
207 ## Backward compatibility.
208 def derive(target
, source
, substmap
): Derive(target
, source
, substmap
).gen()
209 def generate(target
, source
= None): Generate(target
, source
).gen()
211 ###--------------------------------------------------------------------------
212 ### Discovering version numbers.
214 def auto_version(writep
= True):
216 Returns the package version number.
218 As a side-effect, if WRITEP is true, then write the version number to the
219 RELEASE file so that it gets included in distributions.
221 All of this is for backwards compatibility. New projects should omit the
222 `version' keyword entirely and let `setup' discover it and write it into
223 tarballs automatically.
225 version
= progoutput(['./auto-version'])
227 with
open('RELEASE.new', 'w') as ft
: ft
.write('%s\n' % version
)
228 OS
.rename('RELEASE.new', 'RELEASE')
231 ###--------------------------------------------------------------------------
232 ### Adding new commands.
236 class CommandClass (type):
238 Metaclass for command classes: automatically adds them to the `CMDS' map.
240 def __new__(cls
, name
, supers
, dict):
241 c
= super(CommandClass
, cls
).__new__(cls
, name
, supers
, dict)
243 except AttributeError: pass
247 class Command (with_metaclass(CommandClass
, DC
.Command
, object)):
249 Base class for `mdwsetup' command classes.
251 This provides the automatic registration machinery, via the metaclass, and
252 also trivial implementations of various responsibilities of `DC.Command'
253 methods and attributes.
255 __metaclass__
= CommandClass
257 def initialize_options(me
): pass
258 def finalize_options(me
): pass
260 for s
in me
.get_sub_commands(): me
.run_command(s
)
262 ###--------------------------------------------------------------------------
263 ### Some handy new commands.
265 class distdir (Command
):
267 description
= "print the distribution directory name to stdout"
270 print('%s-%s' %
(d
.get_name(), d
.get_version()))
272 class build_gen (Command
):
274 Generate files, according to the `genfiles'.
276 The `genfiles' keyword argument to `setup' lists a number of objects which
277 guide the generation of output files. These objects must implement the
280 clean(DRY_RUN_P) Remove the output files.
282 gen(DRY_RUN_P) Generate the output files, if they don't exist or are
283 out of date with respect to their prerequisites.
285 If DRY_RUN_P is true then the methods must not actually do anything with a
286 lasting effect, but should print progress messages as usual.
289 description
= "build generated source files"
292 for g
in d
.genfiles
: g
.gen(dry_run_p
= me
.dry_run
)
294 from distutils
.command
.build
import build
as _build
295 class build (_build
, Command
):
296 ## Add `build_gen' early in the list of subcommands.
298 sub_commands
= [('build_gen', lambda me
: me
.distribution
.genfiles
)]
299 sub_commands
+= _build
.sub_commands
301 class test (Command
):
303 Run unit tests, according to the `unittests'.
305 The `unittests' keyword argument to `setup' lists module names (or other
306 things acceptable to the `loadTestsFromNames' test-loader method) to be
307 run. The build library directory is prepended to the load path before
308 running the tests to ensure that the newly built modules are tested. If
309 `unittest_dir' is set, then this is appended to the load path so that test
310 modules can be found there.
313 description
= "run the included test suite"
316 [('build-lib=', 'b', "directory containing compiled moules"),
317 ('tests=', 't', "tests to run"),
318 ('verbose-test', 'V', "run tests verbosely")]
320 def initialize_options(me
):
322 me
.verbose_test
= False
324 def finalize_options(me
):
325 me
.set_undefined_options('build', ('build_lib', 'build_lib'))
329 SYS
.path
= [me
.build_lib
] + SYS
.path
330 if d
.unittest_dir
is not None: SYS
.path
.append(d
.unittest_dir
)
331 if me
.tests
is not None: tests
= me
.tests
.split(",")
332 else: tests
= d
.unittests
333 suite
= U
.defaultTestLoader
.loadTestsFromNames(tests
)
334 runner
= U
.TextTestRunner(verbosity
= me
.verbose_test
and 2 or 1)
335 if me
.dry_run
: return
336 result
= runner
.run(suite
)
337 if result
.errors
or result
.failures
or \
338 getattr(result
, "unexpectedSuccesses", 0):
341 class clean_gen (Command
):
343 Remove the generated files, as listed in `genfiles'.
345 See the `build_gen' command for more detailed information.
348 description
= "clean generated source files"
351 for g
in d
.genfiles
: g
.clean(dry_run_p
= me
.dry_run
)
353 class clean_others (Command
):
355 Remove the files listed in the `cleanfiles' argument to `setup'.
357 NAME
= 'clean_others'
358 description
= "clean miscellaneous output files"
361 for f
in d
.cleanfiles
:
362 if not OS
.path
.exists(f
): continue
363 DL
.log(DL
.INFO
, "delete `%s'", f
)
364 if not me
.dry_run
: OS
.remove(f
)
366 from distutils
.command
.clean
import clean
as _clean
367 class clean (_clean
, Command
):
368 ## Add `clean_gen' and `clean_others' to the list of subcommands.
370 sub_commands
= [('clean_gen', lambda me
: me
.distribution
.genfiles
),
371 ('clean_others', lambda me
: me
.distribution
.cleanfiles
)]
372 sub_commands
+= _clean
.sub_commands
377 from distutils
.command
.sdist
import sdist
as _sdist
378 class sdist (_sdist
, Command
):
379 ## Write a `RELEASE' file to the output, if we extracted the version number
380 ## from version control. Also arrange to dereference symbolic links while
381 ## copying. Symlinks to directories will go horribly wrong, so don't do
384 def make_release_tree(me
, base_dir
, files
):
385 _sdist
.make_release_tree(me
, base_dir
, files
)
387 if d
._auto_version_p
:
388 v
= d
.metadata
.get_version()
389 DL
.log(DL
.INFO
, "write `RELEASE' file: %s" % v
)
390 with
open(OS
.path
.join(base_dir
, 'RELEASE'), 'w') as f
:
392 def copy_file(me
, infile
, outfile
, link
= None, *args
, **kw
):
393 if OS
.path
.islink(infile
): link
= None
394 return _sdist
.copy_file(me
, infile
, outfile
, link
= link
, *args
, **kw
)
396 ###--------------------------------------------------------------------------
397 ### Our own version of `setup'.
399 class Dist (DC
.Distribution
):
400 ## Like the usual version, but with some additional attributes to support
401 ## our enhanced commands.
402 def __init__(me
, attrs
= None):
404 me
.unittest_dir
= None
407 me
._auto_version_p
= False
408 DC
.Distribution
.__init__(me
, attrs
)
409 if me
.metadata
.version
is None:
410 me
.metadata
.version
= auto_version(writep
= False)
411 me
._auto_version_p
= True
412 me
.cleanfiles
= set(me
.cleanfiles
)
413 me
.cleanfiles
.add('MANIFEST')
415 def setup(cmdclass
= {}, distclass
= Dist
, **kw
):
416 ## Like the usual version, but provides defaults more suited to our
420 cmds
.update(cmdclass
)
421 DC
.setup(cmdclass
= cmds
, distclass
= distclass
, **kw
)
423 ###----- That's all, folks --------------------------------------------------