chiark / gitweb /
mdwsetup.py: Common utilities for Python module build systems.
[cfd] / mdwsetup.py
1 ### -*-python-*-
2 ###
3 ### Utility module for Python build systems
4 ###
5 ### (c) 2009 Straylight/Edgeware
6 ###
7
8 ###----- Licensing notice ---------------------------------------------------
9 ###
10 ### This file is part of the Python interface to mLib.
11 ###
12 ### mLib/Python 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.
16 ###
17 ### mLib/Python 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.
21 ###
22 ### You should have received a copy of the GNU General Public License
23 ### along with mLib/Python; if not, write to the Free Software Foundation,
24 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 import sys as SYS
27 import os as OS
28 import re as RE
29 import subprocess as SUB
30
31 import distutils.core as DC
32
33 ###--------------------------------------------------------------------------
34 ### Random utilities.
35
36 def uniquify(seq):
37   """
38   Return a list of the elements of SEQ, with duplicates removed.
39
40   Only the first occurrence (according to `==') is left.
41   """
42   seen = {}
43   out = []
44   for item in seq:
45     if item not in seen:
46       seen[item] = True
47       out.append(item)
48   return out
49
50 ###--------------------------------------------------------------------------
51 ### Subprocess hacking.
52
53 class SubprocessFailure (Exception):
54   def __init__(me, file, rc):
55     me.args = (file, rc)
56     me.file = file
57     me.rc = rc
58   def __str__(me):
59     if WIFEXITED(me.rc):
60       return '%s failed (rc = %d)' % (me.file, WEXITSTATUS(me.rc))
61     elif WIFSIGNALED(me.rc):
62       return '%s died (signal %d)' % (me.file, WTERMSIG(me.rc))
63     else:
64       return '%s died inexplicably' % (me.file)
65
66 def progoutput(command):
67   """
68   Run the shell COMMAND and return its standard output.
69
70   The COMMAND must produce exactly one line of output, and must exit with
71   status zero.
72   """
73   kid = SUB.Popen(command, stdout = SUB.PIPE)
74   out = kid.stdout.readline()
75   junk = kid.stdout.read()
76   if junk != '':
77     raise ValueError, \
78           "Child process `%s' produced unspected output %r" % (command, junk)
79   rc = kid.wait()
80   if rc != 0:
81     raise SubprocessFailure, (command, rc)
82   return out.rstrip('\n')
83
84 ###--------------------------------------------------------------------------
85 ### External library packages.
86
87 INCLUDEDIRS = []
88 LIBDIRS = []
89 LIBS = []
90
91 def pkg_config(pkg, version):
92   """
93   Find the external package PKG and store the necessary compiler flags.
94
95   The include-directory names are stored in INCLUDEDIRS; the
96   library-directory names are in LIBDIRS; and the library names themselves
97   are in LIBS.
98   """
99   spec = '%s >= %s' % (pkg, version)
100   def weird(what, word):
101     raise ValueError, \
102           "Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg)
103   for word in progoutput(['pkg-config', '--cflags', spec]).split():
104     if word.startswith('-I'):
105       INCLUDEDIRS.append(word[2:])
106     else:
107       weird('--cflags', word)
108   for word in progoutput(['pkg-config', '--libs', spec]).split():
109     if word.startswith('-L'):
110       LIBDIRS.append(word[2:])
111     elif word.startswith('-l'):
112       LIBS.append(word[2:])
113     else:
114       weird('--libs', word)
115
116 ###--------------------------------------------------------------------------
117 ### Substituting variables in files.
118
119 def needs_update_p(target, sources):
120   """
121   Returns whether TARGET is out of date relative to its SOURCES.
122
123   If TARGET exists and was modified more recentently than any of its SOURCES
124   then it doesn't need updating.
125   """
126   if not OS.path.exists(target):
127     return True
128   t_target = OS.stat(target).st_mtime
129   for source in sources:
130     if OS.stat(source).st_mtime >= t_target:
131       return True
132   return False
133
134 RX_SUBST = RE.compile(r'\%(\w+)\%')
135 def derive(target, source, substmap):
136   """
137   Derive TARGET from SOURCE by making simple substitutions.
138
139   The SOURCE may contain markers %FOO%; these are replaced by SUBSTMAP['FOO']
140   in the TARGET file.
141   """
142   if not needs_update_p(target, [source]):
143     return False
144   print "making `%s' from `%s'" % (target, source)
145   temp = target + '.new'
146   ft = open(temp, 'w')
147   try:
148     fs = open(source, 'r')
149     try:
150       for line in fs:
151         ft.write(RX_SUBST.sub((lambda m: substmap[m.group(1)]), line))
152     finally:
153       fs.close()
154   finally:
155     ft.close()
156   OS.rename(temp, target)
157
158 def generate(target, source = None):
159   """
160   Generate TARGET by running the SOURCE Python script.
161
162   If SOURCE is omitted, replace the extension of TARGET by `.py'.
163   """
164   if source is None:
165     source = OS.path.splitext(target)[0] + '.py'
166   if not needs_update_p(target, [source]):
167     return
168   print "making `%s' using `%s'" % (target, source)
169   temp = target + '.new'
170   ft = open(temp, 'w')
171   try:
172     rc = SUB.call([SYS.executable, source], stdout = ft)
173   finally:
174     ft.close()
175   if rc != 0:
176     raise SubprocessFailure, (source, rc)
177   OS.rename(temp, target)
178
179 ###--------------------------------------------------------------------------
180 ### Discovering version numbers.
181
182 def auto_version(writep = True):
183   """
184   Returns the package version number.
185
186   As a side-effect, if WRITEP is true, then write the version number to the
187   RELEASE file so that it gets included in distributions.
188   """
189   version = progoutput(['./auto-version'])
190   if writep:
191     ft = open('RELEASE.new', 'w')
192     try:
193       ft.write('%s\n' % version)
194     finally:
195       ft.close()
196     OS.rename('RELEASE.new', 'RELEASE')
197   return version
198
199 ###----- That's all, folks --------------------------------------------------