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