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 ###--------------------------------------------------------------------------
41 Return a list of the elements of SEQ, with duplicates removed.
43 Only the first occurrence (according to `==') is left.
53 ###--------------------------------------------------------------------------
54 ### Subprocess hacking.
56 class SubprocessFailure (Exception):
57 def __init__(me
, file, rc
):
63 return '%s failed (rc = %d)' %
(me
.file, WEXITSTATUS(me
.rc
))
64 elif WIFSIGNALED(me
.rc
):
65 return '%s died (signal %d)' %
(me
.file, WTERMSIG(me
.rc
))
67 return '%s died inexplicably' %
(me
.file)
69 def progoutput(command
):
71 Run the shell COMMAND and return its standard output.
73 The COMMAND must produce exactly one line of output, and must exit with
76 kid
= SUB
.Popen(command
, stdout
= SUB
.PIPE
)
77 out
= kid
.stdout
.readline()
78 junk
= kid
.stdout
.read()
81 "Child process `%s' produced unspected output %r" %
(command
, junk
)
84 raise SubprocessFailure
, (command
, rc
)
85 return out
.rstrip('\n')
87 ###--------------------------------------------------------------------------
88 ### External library packages.
94 def pkg_config(pkg
, version
):
96 Find the external package PKG and store the necessary compiler flags.
98 The include-directory names are stored in INCLUDEDIRS; the
99 library-directory names are in LIBDIRS; and the library names themselves
102 spec
= '%s >= %s' %
(pkg
, version
)
103 def weird(what
, word
):
105 "Unexpected `%s' item `%s' from package `%s'" %
(what
, word
, pkg
)
106 for word
in progoutput(['pkg-config', '--cflags', spec
]).split():
107 if word
.startswith('-I'):
108 INCLUDEDIRS
.append(word
[2:])
110 weird('--cflags', word
)
111 for word
in progoutput(['pkg-config', '--libs', spec
]).split():
112 if word
.startswith('-L'):
113 LIBDIRS
.append(word
[2:])
114 elif word
.startswith('-l'):
115 LIBS
.append(word
[2:])
117 weird('--libs', word
)
119 ###--------------------------------------------------------------------------
120 ### Substituting variables in files.
122 class BaseGenFile (object):
124 A base class for file generators.
126 Instances of subclasses are suitable for listing in the `genfiles'
127 attribute, passed to `setup'.
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.
133 def __init__(me
, target
, 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
140 if OS
.stat(s
).st_mtime
>= t_target
: return True
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
)
152 class Derive (BaseGenFile
):
154 Derive TARGET from SOURCE by making simple substitutions.
156 The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
159 RX_SUBST
= RE
.compile(r
'\%(\w+)\%')
160 def __init__(me
, target
, source
, substmap
):
161 BaseGenFile
.__init__(me
, target
, [source
])
164 temp
= me
.target
+ '.new'
165 with
open(temp
, 'w') as ft
:
166 with
open(me
.sources
[0], 'r') as fs
:
168 ft
.write(me
.RX_SUBST
.sub((lambda m
: me
._map
[m
.group(1)]), line
))
169 OS
.rename(temp
, me
.target
)
171 class Generate (BaseGenFile
):
173 Generate TARGET by running the SOURCE Python script.
175 If SOURCE is omitted, replace the extension of TARGET by `.py'.
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
])
181 temp
= me
.target
+ '.new'
182 with
open(temp
, 'w') as ft
:
183 rc
= SUB
.call([SYS
.executable
, me
.sources
[0]], stdout
= ft
)
184 if rc
!= 0: raise SubprocessFailure
, (source
, rc
)
185 OS
.rename(temp
, me
.target
)
187 ## Backward compatibility.
188 def derive(target
, source
, substmap
): Derive(target
, source
, substmap
).gen()
189 def generate(target
, source
= None): Generate(target
, source
).gen()
191 ###--------------------------------------------------------------------------
192 ### Discovering version numbers.
194 def auto_version(writep
= True):
196 Returns the package version number.
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.
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.
205 version
= progoutput(['./auto-version'])
207 with
open('RELEASE.new', 'w') as ft
: ft
.write('%s\n' % version
)
208 OS
.rename('RELEASE.new', 'RELEASE')
211 ###--------------------------------------------------------------------------
212 ### Adding new commands.
216 class CommandClass (type):
218 Metaclass for command classes: automatically adds them to the `CMDS' map.
220 def __new__(cls
, name
, supers
, dict):
221 c
= super(CommandClass
, cls
).__new__(cls
, name
, supers
, dict)
223 except AttributeError: pass
227 class Command (DC
.Command
, object):
229 Base class for `mdwsetup' command classes.
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.
235 __metaclass__
= CommandClass
237 def initialize_options(me
): pass
238 def finalize_options(me
): pass
240 for s
in me
.get_sub_commands(): me
.run_command(s
)
242 ###--------------------------------------------------------------------------
243 ### Some handy new commands.
245 class distdir (Command
):
247 description
= "print the distribution directory name to stdout"
250 print '%s-%s' %
(d
.get_name(), d
.get_version())
252 class build_gen(Command
):
254 Generate files, according to the `genfiles'.
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
260 clean(DRY_RUN_P) Remove the output files.
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.
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.
269 description
= "build generated source files"
272 for g
in d
.genfiles
: g
.gen(dry_run_p
= me
.dry_run
)
274 from distutils
.command
.build
import build
as _build
275 class build (_build
, Command
):
276 ## Add `build_gen' early in the list of subcommands.
278 sub_commands
= [('build_gen', lambda me
: me
.distribution
.genfiles
)]
279 sub_commands
+= _build
.sub_commands
281 class clean_gen(Command
):
283 Remove the generated files, as listed in `genfiles'.
285 See the `build_gen' command for more detailed information.
288 description
= "clean generated source files"
291 for g
in d
.genfiles
: g
.clean(dry_run_p
= me
.dry_run
)
293 class clean_others(Command
):
295 Remove the files listed in the `cleanfiles' argument to `setup'.
297 NAME
= 'clean_others'
298 description
= "clean miscellaneous output files"
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
)
306 from distutils
.command
.clean
import clean
as _clean
307 class clean (_clean
, Command
):
308 ## Add `clean_gen' and `clean_others' to the list of subcommands.
310 sub_commands
= [('clean_gen', lambda me
: me
.distribution
.genfiles
),
311 ('clean_others', lambda me
: me
.distribution
.cleanfiles
)]
312 sub_commands
+= _clean
.sub_commands
317 from distutils
.command
.sdist
import sdist
as _sdist
318 class 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
324 def make_release_tree(me
, base_dir
, files
):
325 _sdist
.make_release_tree(me
, base_dir
, files
)
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
:
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
)
336 ###--------------------------------------------------------------------------
337 ### Our own version of `setup'.
339 class 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):
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')
353 def setup(cmdclass
= {}, distclass
= Dist
, **kw
):
354 ## Like the usual version, but provides defaults more suited to our
358 cmds
.update(cmdclass
)
359 DC
.setup(cmdclass
= cmds
, distclass
= distclass
, **kw
)
361 ###----- That's all, folks --------------------------------------------------