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
):
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
))
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
)
78 out
= kid
.stdout
.readline()
79 junk
= kid
.stdout
.read(1)
82 if junk
!= '': raise ValueError \
83 ("Child process `%s' produced unspected output %r" %
(command
, junk
))
85 if rc
!= 0: raise SubprocessFailure(command
, rc
)
86 return out
.rstrip('\n')
88 ###--------------------------------------------------------------------------
89 ### External library packages.
95 def pkg_config(pkg
, version
):
97 Find the external package PKG and store the necessary compiler flags.
99 The include-directory names are stored in INCLUDEDIRS; the
100 library-directory names are in LIBDIRS; and the library names themselves
104 def weird(what
, word
):
106 ("Unexpected `%s' item `%s' from package `%s'" %
(what
, word
, pkg
))
108 spec
= '%s >= %s' %
(pkg
, version
)
110 try: cflags
= OS
.environ
["%s_CFLAGS" % pkg
]
111 except KeyError: cflags
= progoutput(['pkg-config', '--cflags', spec
])
112 for word
in cflags
.split():
113 if word
.startswith('-I'): INCLUDEDIRS
.append(word
[2:])
114 else: weird('CFLAGS', word
)
115 try: libs
= OS
.environ
["%s_LIBS" % pkg
]
116 except KeyError: libs
= progoutput(['pkg-config', '--libs', spec
])
117 for word
in libs
.split():
118 if word
.startswith('-L'): LIBDIRS
.append(word
[2:])
119 elif word
.startswith('-l'): LIBS
.append(word
[2:])
120 else: weird('LIBS', word
)
122 ###--------------------------------------------------------------------------
123 ### Substituting variables in files.
125 class BaseGenFile (object):
127 A base class for file generators.
129 Instances of subclasses are suitable for listing in the `genfiles'
130 attribute, passed to `setup'.
132 Subclasses need to implement `_gen', which should simply do the work of
133 generating the target file from its sources. This class will print
134 progress messages and check whether the target actually needs regenerating.
136 def __init__(me
, target
, sources
= []):
139 def _needs_update_p(me
):
140 if not OS
.path
.exists(me
.target
): return True
141 t_target
= OS
.stat(me
.target
).st_mtime
143 if OS
.stat(s
).st_mtime
>= t_target
: return True
145 def gen(me
, dry_run_p
= False):
146 if not me
._needs_update_p(): return
147 DL
.log(DL
.INFO
, "generate `%s' from %s", me
.target
,
148 ', '.join("`%s'" % s for s in me
.sources
))
149 if not dry_run_p
: me
._gen()
150 def clean(me
, dry_run_p
):
151 if not OS
.path
.exists(me
.target
): return
152 DL
.log(DL
.INFO
, "delete `%s'", me
.target
)
153 if not dry_run_p
: OS
.remove(me
.target
)
155 class Derive (BaseGenFile
):
157 Derive TARGET from SOURCE by making simple substitutions.
159 The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
162 RX_SUBST
= RE
.compile(r
'\%(\w+)\%')
163 def __init__(me
, target
, source
, substmap
):
164 BaseGenFile
.__init__(me
, target
, [source
])
167 temp
= me
.target
+ '.new'
168 with
open(temp
, 'w') as ft
:
169 with
open(me
.sources
[0], 'r') as fs
:
171 ft
.write(me
.RX_SUBST
.sub((lambda m
: me
._map
[m
.group(1)]), line
))
172 OS
.rename(temp
, me
.target
)
174 class Generate (BaseGenFile
):
176 Generate TARGET by running the SOURCE Python script.
178 If SOURCE is omitted, replace the extension of TARGET by `.py'.
180 def __init__(me
, target
, source
= None):
181 if source
is None: source
= OS
.path
.splitext(target
)[0] + '.py'
182 BaseGenFile
.__init__(me
, target
, [source
])
184 temp
= me
.target
+ '.new'
185 with
open(temp
, 'w') as ft
:
186 rc
= SUB
.call([SYS
.executable
, me
.sources
[0]], stdout
= ft
)
187 if rc
!= 0: raise SubprocessFailure(me
.sources
[0], rc
<< 8)
188 OS
.rename(temp
, me
.target
)
190 ## Backward compatibility.
191 def derive(target
, source
, substmap
): Derive(target
, source
, substmap
).gen()
192 def generate(target
, source
= None): Generate(target
, source
).gen()
194 ###--------------------------------------------------------------------------
195 ### Discovering version numbers.
197 def auto_version(writep
= True):
199 Returns the package version number.
201 As a side-effect, if WRITEP is true, then write the version number to the
202 RELEASE file so that it gets included in distributions.
204 All of this is for backwards compatibility. New projects should omit the
205 `version' keyword entirely and let `setup' discover it and write it into
206 tarballs automatically.
208 version
= progoutput(['./auto-version'])
210 with
open('RELEASE.new', 'w') as ft
: ft
.write('%s\n' % version
)
211 OS
.rename('RELEASE.new', 'RELEASE')
214 ###--------------------------------------------------------------------------
215 ### Adding new commands.
219 class CommandClass (type):
221 Metaclass for command classes: automatically adds them to the `CMDS' map.
223 def __new__(cls
, name
, supers
, dict):
224 c
= super(CommandClass
, cls
).__new__(cls
, name
, supers
, dict)
226 except AttributeError: pass
230 class Command (DC
.Command
, object):
232 Base class for `mdwsetup' command classes.
234 This provides the automatic registration machinery, via the metaclass, and
235 also trivial implementations of various responsibilities of `DC.Command'
236 methods and attributes.
238 __metaclass__
= CommandClass
240 def initialize_options(me
): pass
241 def finalize_options(me
): pass
243 for s
in me
.get_sub_commands(): me
.run_command(s
)
245 ###--------------------------------------------------------------------------
246 ### Some handy new commands.
248 class distdir (Command
):
250 description
= "print the distribution directory name to stdout"
253 print '%s-%s' %
(d
.get_name(), d
.get_version())
255 class build_gen(Command
):
257 Generate files, according to the `genfiles'.
259 The `genfiles' keyword argument to `setup' lists a number of objects which
260 guide the generation of output files. These objects must implement the
263 clean(DRY_RUN_P) Remove the output files.
265 gen(DRY_RUN_P) Generate the output files, if they don't exist or are
266 out of date with respect to their prerequisites.
268 If DRY_RUN_P is true then the methods must not actually do anything with a
269 lasting effect, but should print progress messages as usual.
272 description
= "build generated source files"
275 for g
in d
.genfiles
: g
.gen(dry_run_p
= me
.dry_run
)
277 from distutils
.command
.build
import build
as _build
278 class build (_build
, Command
):
279 ## Add `build_gen' early in the list of subcommands.
281 sub_commands
= [('build_gen', lambda me
: me
.distribution
.genfiles
)]
282 sub_commands
+= _build
.sub_commands
284 class clean_gen(Command
):
286 Remove the generated files, as listed in `genfiles'.
288 See the `build_gen' command for more detailed information.
291 description
= "clean generated source files"
294 for g
in d
.genfiles
: g
.clean(dry_run_p
= me
.dry_run
)
296 class clean_others(Command
):
298 Remove the files listed in the `cleanfiles' argument to `setup'.
300 NAME
= 'clean_others'
301 description
= "clean miscellaneous output files"
304 for f
in d
.cleanfiles
:
305 if not OS
.path
.exists(f
): continue
306 DL
.log(DL
.INFO
, "delete `%s'", f
)
307 if not me
.dry_run
: OS
.remove(f
)
309 from distutils
.command
.clean
import clean
as _clean
310 class clean (_clean
, Command
):
311 ## Add `clean_gen' and `clean_others' to the list of subcommands.
313 sub_commands
= [('clean_gen', lambda me
: me
.distribution
.genfiles
),
314 ('clean_others', lambda me
: me
.distribution
.cleanfiles
)]
315 sub_commands
+= _clean
.sub_commands
320 from distutils
.command
.sdist
import sdist
as _sdist
321 class sdist (_sdist
, Command
):
322 ## Write a `RELEASE' file to the output, if we extracted the version number
323 ## from version control. Also arrange to dereference symbolic links while
324 ## copying. Symlinks to directories will go horribly wrong, so don't do
327 def make_release_tree(me
, base_dir
, files
):
328 _sdist
.make_release_tree(me
, base_dir
, files
)
330 if d
._auto_version_p
:
331 v
= d
.metadata
.get_version()
332 DL
.log(DL
.INFO
, "write `RELEASE' file: %s" % v
)
333 with
open(OS
.path
.join(base_dir
, 'RELEASE'), 'w') as f
:
335 def copy_file(me
, infile
, outfile
, link
= None, *args
, **kw
):
336 if OS
.path
.islink(infile
): link
= None
337 return _sdist
.copy_file(me
, infile
, outfile
, link
= link
, *args
, **kw
)
339 ###--------------------------------------------------------------------------
340 ### Our own version of `setup'.
342 class Dist (DC
.Distribution
):
343 ## Like the usual version, but with some additional attributes to support
344 ## our enhanced commands.
345 def __init__(me
, attrs
= None):
348 me
._auto_version_p
= False
349 DC
.Distribution
.__init__(me
, attrs
)
350 if me
.metadata
.version
is None:
351 me
.metadata
.version
= auto_version(writep
= False)
352 me
._auto_version_p
= True
353 me
.cleanfiles
= set(me
.cleanfiles
)
354 me
.cleanfiles
.add('MANIFEST')
356 def setup(cmdclass
= {}, distclass
= Dist
, **kw
):
357 ## Like the usual version, but provides defaults more suited to our
361 cmds
.update(cmdclass
)
362 DC
.setup(cmdclass
= cmds
, distclass
= distclass
, **kw
)
364 ###----- That's all, folks --------------------------------------------------