OLD | NEW |
| (Empty) |
1 """ | |
2 Distutils convenience functionality. | |
3 | |
4 Don't use this outside of Twisted. | |
5 | |
6 Maintainer: U{Christopher Armstrong<mailto:radix@twistedmatrix.com>} | |
7 """ | |
8 | |
9 import sys, os | |
10 from distutils.command import build_scripts, install_data, build_ext, build_py | |
11 from distutils.errors import CompileError | |
12 from distutils import core | |
13 from distutils.core import Extension | |
14 | |
15 twisted_subprojects = ["conch", "flow", "lore", "mail", "names", | |
16 "news", "pair", "runner", "web", "web2", | |
17 "words", "vfs"] | |
18 | |
19 | |
20 class ConditionalExtension(Extension): | |
21 """ | |
22 An extension module that will only be compiled if certain conditions are | |
23 met. | |
24 | |
25 @param condition: A callable of one argument which returns True or False to | |
26 indicate whether the extension should be built. The argument is an | |
27 instance of L{build_ext_twisted}, which has useful methods for checking | |
28 things about the platform. | |
29 """ | |
30 def __init__(self, *args, **kwargs): | |
31 self.condition = kwargs.pop("condition", lambda builder: True) | |
32 Extension.__init__(self, *args, **kwargs) | |
33 | |
34 | |
35 | |
36 def setup(**kw): | |
37 """ | |
38 An alternative to distutils' setup() which is specially designed | |
39 for Twisted subprojects. | |
40 | |
41 Pass twisted_subproject=projname if you want package and data | |
42 files to automatically be found for you. | |
43 | |
44 @param conditionalExtensions: Extensions to optionally build. | |
45 @type conditionalExtensions: C{list} of L{ConditionalExtension} | |
46 """ | |
47 return core.setup(**get_setup_args(**kw)) | |
48 | |
49 def get_setup_args(**kw): | |
50 if 'twisted_subproject' in kw: | |
51 if 'twisted' not in os.listdir('.'): | |
52 raise RuntimeError("Sorry, you need to run setup.py from the " | |
53 "toplevel source directory.") | |
54 projname = kw['twisted_subproject'] | |
55 projdir = os.path.join('twisted', projname) | |
56 | |
57 kw['packages'] = getPackages(projdir, parent='twisted') | |
58 kw['version'] = getVersion(projname) | |
59 | |
60 plugin = "twisted/plugins/twisted_" + projname + ".py" | |
61 if os.path.exists(plugin): | |
62 kw.setdefault('py_modules', []).append( | |
63 plugin.replace("/", ".")[:-3]) | |
64 | |
65 kw['data_files'] = getDataFiles(projdir, parent='twisted') | |
66 | |
67 del kw['twisted_subproject'] | |
68 else: | |
69 if 'plugins' in kw: | |
70 py_modules = [] | |
71 for plg in kw['plugins']: | |
72 py_modules.append("twisted.plugins." + plg) | |
73 kw.setdefault('py_modules', []).extend(py_modules) | |
74 del kw['plugins'] | |
75 | |
76 if 'cmdclass' not in kw: | |
77 kw['cmdclass'] = { | |
78 'install_data': install_data_twisted, | |
79 'build_scripts': build_scripts_twisted} | |
80 if sys.version_info[:3] < (2, 3, 0): | |
81 kw['cmdclass']['build_py'] = build_py_twisted | |
82 | |
83 if "conditionalExtensions" in kw: | |
84 extensions = kw["conditionalExtensions"] | |
85 del kw["conditionalExtensions"] | |
86 | |
87 if 'ext_modules' not in kw: | |
88 # This is a workaround for distutils behavior; ext_modules isn't | |
89 # actually used by our custom builder. distutils deep-down checks | |
90 # to see if there are any ext_modules defined before invoking | |
91 # the build_ext command. We need to trigger build_ext regardless | |
92 # because it is the thing that does the conditional checks to see | |
93 # if it should build any extensions. The reason we have to delay | |
94 # the conditional checks until then is that the compiler objects | |
95 # are not yet set up when this code is executed. | |
96 kw["ext_modules"] = extensions | |
97 | |
98 class my_build_ext(build_ext_twisted): | |
99 conditionalExtensions = extensions | |
100 kw.setdefault('cmdclass', {})['build_ext'] = my_build_ext | |
101 return kw | |
102 | |
103 def getVersion(proj, base="twisted"): | |
104 """ | |
105 Extract the version number for a given project. | |
106 | |
107 @param proj: the name of the project. Examples are "core", | |
108 "conch", "words", "mail". | |
109 | |
110 @rtype: str | |
111 @returns: The version number of the project, as a string like | |
112 "2.0.0". | |
113 """ | |
114 if proj == 'core': | |
115 vfile = os.path.join(base, '_version.py') | |
116 else: | |
117 vfile = os.path.join(base, proj, '_version.py') | |
118 ns = {'__name__': 'Nothing to see here'} | |
119 execfile(vfile, ns) | |
120 return ns['version'].base() | |
121 | |
122 | |
123 # Names that are exluded from globbing results: | |
124 EXCLUDE_NAMES = ["{arch}", "CVS", ".cvsignore", "_darcs", | |
125 "RCS", "SCCS", ".svn"] | |
126 EXCLUDE_PATTERNS = ["*.py[cdo]", "*.s[ol]", ".#*", "*~", "*.py"] | |
127 | |
128 import fnmatch | |
129 | |
130 def _filterNames(names): | |
131 """Given a list of file names, return those names that should be copied. | |
132 """ | |
133 names = [n for n in names | |
134 if n not in EXCLUDE_NAMES] | |
135 # This is needed when building a distro from a working | |
136 # copy (likely a checkout) rather than a pristine export: | |
137 for pattern in EXCLUDE_PATTERNS: | |
138 names = [n for n in names | |
139 if (not fnmatch.fnmatch(n, pattern)) | |
140 and (not n.endswith('.py'))] | |
141 return names | |
142 | |
143 def relativeTo(base, relativee): | |
144 """ | |
145 Gets 'relativee' relative to 'basepath'. | |
146 | |
147 i.e., | |
148 | |
149 >>> relativeTo('/home/', '/home/radix/') | |
150 'radix' | |
151 >>> relativeTo('.', '/home/radix/Projects/Twisted') # curdir is /home/radix | |
152 'Projects/Twisted' | |
153 | |
154 The 'relativee' must be a child of 'basepath'. | |
155 """ | |
156 basepath = os.path.abspath(base) | |
157 relativee = os.path.abspath(relativee) | |
158 if relativee.startswith(basepath): | |
159 relative = relativee[len(basepath):] | |
160 if relative.startswith(os.sep): | |
161 relative = relative[1:] | |
162 return os.path.join(base, relative) | |
163 raise ValueError("%s is not a subpath of %s" % (relativee, basepath)) | |
164 | |
165 | |
166 def getDataFiles(dname, ignore=None, parent=None): | |
167 """ | |
168 Get all the data files that should be included in this distutils Project. | |
169 | |
170 'dname' should be the path to the package that you're distributing. | |
171 | |
172 'ignore' is a list of sub-packages to ignore. This facilitates | |
173 disparate package hierarchies. That's a fancy way of saying that | |
174 the 'twisted' package doesn't want to include the 'twisted.conch' | |
175 package, so it will pass ['conch'] as the value. | |
176 | |
177 'parent' is necessary if you're distributing a subpackage like | |
178 twisted.conch. 'dname' should point to 'twisted/conch' and 'parent' | |
179 should point to 'twisted'. This ensures that your data_files are | |
180 generated correctly, only using relative paths for the first element | |
181 of the tuple ('twisted/conch/*'). | |
182 The default 'parent' is the current working directory. | |
183 """ | |
184 parent = parent or "." | |
185 ignore = ignore or [] | |
186 result = [] | |
187 for directory, subdirectories, filenames in os.walk(dname): | |
188 resultfiles = [] | |
189 for exname in EXCLUDE_NAMES: | |
190 if exname in subdirectories: | |
191 subdirectories.remove(exname) | |
192 for ig in ignore: | |
193 if ig in subdirectories: | |
194 subdirectories.remove(ig) | |
195 for filename in _filterNames(filenames): | |
196 resultfiles.append(filename) | |
197 if resultfiles: | |
198 result.append((relativeTo(parent, directory), | |
199 [relativeTo(parent, | |
200 os.path.join(directory, filename)) | |
201 for filename in resultfiles])) | |
202 return result | |
203 | |
204 def getPackages(dname, pkgname=None, results=None, ignore=None, parent=None): | |
205 """ | |
206 Get all packages which are under dname. This is necessary for | |
207 Python 2.2's distutils. Pretty similar arguments to getDataFiles, | |
208 including 'parent'. | |
209 """ | |
210 parent = parent or "" | |
211 prefix = [] | |
212 if parent: | |
213 prefix = [parent] | |
214 bname = os.path.basename(dname) | |
215 ignore = ignore or [] | |
216 if bname in ignore: | |
217 return [] | |
218 if results is None: | |
219 results = [] | |
220 if pkgname is None: | |
221 pkgname = [] | |
222 subfiles = os.listdir(dname) | |
223 abssubfiles = [os.path.join(dname, x) for x in subfiles] | |
224 if '__init__.py' in subfiles: | |
225 results.append(prefix + pkgname + [bname]) | |
226 for subdir in filter(os.path.isdir, abssubfiles): | |
227 getPackages(subdir, pkgname=pkgname + [bname], | |
228 results=results, ignore=ignore, | |
229 parent=parent) | |
230 res = ['.'.join(result) for result in results] | |
231 return res | |
232 | |
233 | |
234 | |
235 def getScripts(projname, basedir=''): | |
236 """ | |
237 Returns a list of scripts for a Twisted subproject; this works in | |
238 any of an SVN checkout, a project-specific tarball. | |
239 """ | |
240 scriptdir = os.path.join(basedir, 'bin', projname) | |
241 if not os.path.isdir(scriptdir): | |
242 # Probably a project-specific tarball, in which case only this | |
243 # project's bins are included in 'bin' | |
244 scriptdir = os.path.join(basedir, 'bin') | |
245 if not os.path.isdir(scriptdir): | |
246 return [] | |
247 thingies = os.listdir(scriptdir) | |
248 if '.svn' in thingies: | |
249 thingies.remove('.svn') | |
250 return filter(os.path.isfile, | |
251 [os.path.join(scriptdir, x) for x in thingies]) | |
252 | |
253 | |
254 ## Helpers and distutil tweaks | |
255 | |
256 class build_py_twisted(build_py.build_py): | |
257 """ | |
258 Changes behavior in Python 2.2 to support simultaneous specification of | |
259 `packages' and `py_modules'. | |
260 """ | |
261 def run(self): | |
262 if self.py_modules: | |
263 self.build_modules() | |
264 if self.packages: | |
265 self.build_packages() | |
266 self.byte_compile(self.get_outputs(include_bytecode=0)) | |
267 | |
268 | |
269 | |
270 class build_scripts_twisted(build_scripts.build_scripts): | |
271 """Renames scripts so they end with '.py' on Windows.""" | |
272 | |
273 def run(self): | |
274 build_scripts.build_scripts.run(self) | |
275 if not os.name == "nt": | |
276 return | |
277 for f in os.listdir(self.build_dir): | |
278 fpath=os.path.join(self.build_dir, f) | |
279 if not fpath.endswith(".py"): | |
280 try: | |
281 os.unlink(fpath + ".py") | |
282 except EnvironmentError, e: | |
283 if e.args[1]=='No such file or directory': | |
284 pass | |
285 os.rename(fpath, fpath + ".py") | |
286 | |
287 | |
288 | |
289 class install_data_twisted(install_data.install_data): | |
290 """I make sure data files are installed in the package directory.""" | |
291 def finalize_options(self): | |
292 self.set_undefined_options('install', | |
293 ('install_lib', 'install_dir') | |
294 ) | |
295 install_data.install_data.finalize_options(self) | |
296 | |
297 | |
298 | |
299 class build_ext_twisted(build_ext.build_ext): | |
300 """ | |
301 Allow subclasses to easily detect and customize Extensions to | |
302 build at install-time. | |
303 """ | |
304 | |
305 def prepare_extensions(self): | |
306 """ | |
307 Prepare the C{self.extensions} attribute (used by | |
308 L{build_ext.build_ext}) by checking which extensions in | |
309 L{conditionalExtensions} should be built. In addition, if we are | |
310 building on NT, define the WIN32 macro to 1. | |
311 """ | |
312 # always define WIN32 under Windows | |
313 if os.name == 'nt': | |
314 self.define_macros = [("WIN32", 1)] | |
315 else: | |
316 self.define_macros = [] | |
317 self.extensions = [x for x in self.conditionalExtensions | |
318 if x.condition(self)] | |
319 for ext in self.extensions: | |
320 ext.define_macros.extend(self.define_macros) | |
321 | |
322 | |
323 def build_extensions(self): | |
324 """ | |
325 Check to see which extension modules to build and then build them. | |
326 """ | |
327 self.prepare_extensions() | |
328 build_ext.build_ext.build_extensions(self) | |
329 | |
330 | |
331 def _remove_conftest(self): | |
332 for filename in ("conftest.c", "conftest.o", "conftest.obj"): | |
333 try: | |
334 os.unlink(filename) | |
335 except EnvironmentError: | |
336 pass | |
337 | |
338 | |
339 def _compile_helper(self, content): | |
340 conftest = open("conftest.c", "w") | |
341 try: | |
342 conftest.write(content) | |
343 conftest.close() | |
344 | |
345 try: | |
346 self.compiler.compile(["conftest.c"], output_dir='') | |
347 except CompileError: | |
348 return False | |
349 return True | |
350 finally: | |
351 self._remove_conftest() | |
352 | |
353 | |
354 def _check_header(self, header_name): | |
355 """ | |
356 Check if the given header can be included by trying to compile a file | |
357 that contains only an #include line. | |
358 """ | |
359 self.compiler.announce("checking for %s ..." % header_name, 0) | |
360 return self._compile_helper("#include <%s>\n" % header_name) | |
361 | |
OLD | NEW |