Commit | Line | Data |
---|---|---|
86530b38 AT |
1 | """distutils.command.build_py |
2 | ||
3 | Implements the Distutils 'build_py' command.""" | |
4 | ||
5 | # This module should be kept compatible with Python 2.1. | |
6 | ||
7 | __revision__ = "$Id: build_py.py,v 1.46 2004/11/10 22:23:15 loewis Exp $" | |
8 | ||
9 | import sys, string, os | |
10 | from types import * | |
11 | from glob import glob | |
12 | ||
13 | from distutils.core import Command | |
14 | from distutils.errors import * | |
15 | from distutils.util import convert_path | |
16 | from distutils import log | |
17 | ||
18 | class build_py (Command): | |
19 | ||
20 | description = "\"build\" pure Python modules (copy to build directory)" | |
21 | ||
22 | user_options = [ | |
23 | ('build-lib=', 'd', "directory to \"build\" (copy) to"), | |
24 | ('compile', 'c', "compile .py to .pyc"), | |
25 | ('no-compile', None, "don't compile .py files [default]"), | |
26 | ('optimize=', 'O', | |
27 | "also compile with optimization: -O1 for \"python -O\", " | |
28 | "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), | |
29 | ('force', 'f', "forcibly build everything (ignore file timestamps)"), | |
30 | ] | |
31 | ||
32 | boolean_options = ['compile', 'force'] | |
33 | negative_opt = {'no-compile' : 'compile'} | |
34 | ||
35 | ||
36 | def initialize_options (self): | |
37 | self.build_lib = None | |
38 | self.py_modules = None | |
39 | self.package = None | |
40 | self.package_data = None | |
41 | self.package_dir = None | |
42 | self.compile = 0 | |
43 | self.optimize = 0 | |
44 | self.force = None | |
45 | ||
46 | def finalize_options (self): | |
47 | self.set_undefined_options('build', | |
48 | ('build_lib', 'build_lib'), | |
49 | ('force', 'force')) | |
50 | ||
51 | # Get the distribution options that are aliases for build_py | |
52 | # options -- list of packages and list of modules. | |
53 | self.packages = self.distribution.packages | |
54 | self.py_modules = self.distribution.py_modules | |
55 | self.package_data = self.distribution.package_data | |
56 | self.package_dir = {} | |
57 | if self.distribution.package_dir: | |
58 | for name, path in self.distribution.package_dir.items(): | |
59 | self.package_dir[name] = convert_path(path) | |
60 | self.data_files = self.get_data_files() | |
61 | ||
62 | # Ick, copied straight from install_lib.py (fancy_getopt needs a | |
63 | # type system! Hell, *everything* needs a type system!!!) | |
64 | if type(self.optimize) is not IntType: | |
65 | try: | |
66 | self.optimize = int(self.optimize) | |
67 | assert 0 <= self.optimize <= 2 | |
68 | except (ValueError, AssertionError): | |
69 | raise DistutilsOptionError, "optimize must be 0, 1, or 2" | |
70 | ||
71 | def run (self): | |
72 | ||
73 | # XXX copy_file by default preserves atime and mtime. IMHO this is | |
74 | # the right thing to do, but perhaps it should be an option -- in | |
75 | # particular, a site administrator might want installed files to | |
76 | # reflect the time of installation rather than the last | |
77 | # modification time before the installed release. | |
78 | ||
79 | # XXX copy_file by default preserves mode, which appears to be the | |
80 | # wrong thing to do: if a file is read-only in the working | |
81 | # directory, we want it to be installed read/write so that the next | |
82 | # installation of the same module distribution can overwrite it | |
83 | # without problems. (This might be a Unix-specific issue.) Thus | |
84 | # we turn off 'preserve_mode' when copying to the build directory, | |
85 | # since the build directory is supposed to be exactly what the | |
86 | # installation will look like (ie. we preserve mode when | |
87 | # installing). | |
88 | ||
89 | # Two options control which modules will be installed: 'packages' | |
90 | # and 'py_modules'. The former lets us work with whole packages, not | |
91 | # specifying individual modules at all; the latter is for | |
92 | # specifying modules one-at-a-time. | |
93 | ||
94 | if self.py_modules: | |
95 | self.build_modules() | |
96 | if self.packages: | |
97 | self.build_packages() | |
98 | self.build_package_data() | |
99 | ||
100 | self.byte_compile(self.get_outputs(include_bytecode=0)) | |
101 | ||
102 | # run () | |
103 | ||
104 | def get_data_files (self): | |
105 | """Generate list of '(package,src_dir,build_dir,filenames)' tuples""" | |
106 | data = [] | |
107 | if not self.packages: | |
108 | return data | |
109 | for package in self.packages: | |
110 | # Locate package source directory | |
111 | src_dir = self.get_package_dir(package) | |
112 | ||
113 | # Compute package build directory | |
114 | build_dir = os.path.join(*([self.build_lib] + package.split('.'))) | |
115 | ||
116 | # Length of path to strip from found files | |
117 | plen = len(src_dir)+1 | |
118 | ||
119 | # Strip directory from globbed filenames | |
120 | filenames = [ | |
121 | file[plen:] for file in self.find_data_files(package, src_dir) | |
122 | ] | |
123 | data.append((package, src_dir, build_dir, filenames)) | |
124 | return data | |
125 | ||
126 | def find_data_files (self, package, src_dir): | |
127 | """Return filenames for package's data files in 'src_dir'""" | |
128 | globs = (self.package_data.get('', []) | |
129 | + self.package_data.get(package, [])) | |
130 | files = [] | |
131 | for pattern in globs: | |
132 | # Each pattern has to be converted to a platform-specific path | |
133 | filelist = glob(os.path.join(src_dir, convert_path(pattern))) | |
134 | # Files that match more than one pattern are only added once | |
135 | files.extend([fn for fn in filelist if fn not in files]) | |
136 | return files | |
137 | ||
138 | def build_package_data (self): | |
139 | """Copy data files into build directory""" | |
140 | lastdir = None | |
141 | for package, src_dir, build_dir, filenames in self.data_files: | |
142 | for filename in filenames: | |
143 | target = os.path.join(build_dir, filename) | |
144 | self.mkpath(os.path.dirname(target)) | |
145 | self.copy_file(os.path.join(src_dir, filename), target, | |
146 | preserve_mode=False) | |
147 | ||
148 | def get_package_dir (self, package): | |
149 | """Return the directory, relative to the top of the source | |
150 | distribution, where package 'package' should be found | |
151 | (at least according to the 'package_dir' option, if any).""" | |
152 | ||
153 | path = string.split(package, '.') | |
154 | ||
155 | if not self.package_dir: | |
156 | if path: | |
157 | return apply(os.path.join, path) | |
158 | else: | |
159 | return '' | |
160 | else: | |
161 | tail = [] | |
162 | while path: | |
163 | try: | |
164 | pdir = self.package_dir[string.join(path, '.')] | |
165 | except KeyError: | |
166 | tail.insert(0, path[-1]) | |
167 | del path[-1] | |
168 | else: | |
169 | tail.insert(0, pdir) | |
170 | return apply(os.path.join, tail) | |
171 | else: | |
172 | # Oops, got all the way through 'path' without finding a | |
173 | # match in package_dir. If package_dir defines a directory | |
174 | # for the root (nameless) package, then fallback on it; | |
175 | # otherwise, we might as well have not consulted | |
176 | # package_dir at all, as we just use the directory implied | |
177 | # by 'tail' (which should be the same as the original value | |
178 | # of 'path' at this point). | |
179 | pdir = self.package_dir.get('') | |
180 | if pdir is not None: | |
181 | tail.insert(0, pdir) | |
182 | ||
183 | if tail: | |
184 | return apply(os.path.join, tail) | |
185 | else: | |
186 | return '' | |
187 | ||
188 | # get_package_dir () | |
189 | ||
190 | ||
191 | def check_package (self, package, package_dir): | |
192 | ||
193 | # Empty dir name means current directory, which we can probably | |
194 | # assume exists. Also, os.path.exists and isdir don't know about | |
195 | # my "empty string means current dir" convention, so we have to | |
196 | # circumvent them. | |
197 | if package_dir != "": | |
198 | if not os.path.exists(package_dir): | |
199 | raise DistutilsFileError, \ | |
200 | "package directory '%s' does not exist" % package_dir | |
201 | if not os.path.isdir(package_dir): | |
202 | raise DistutilsFileError, \ | |
203 | ("supposed package directory '%s' exists, " + | |
204 | "but is not a directory") % package_dir | |
205 | ||
206 | # Require __init__.py for all but the "root package" | |
207 | if package: | |
208 | init_py = os.path.join(package_dir, "__init__.py") | |
209 | if os.path.isfile(init_py): | |
210 | return init_py | |
211 | else: | |
212 | log.warn(("package init file '%s' not found " + | |
213 | "(or not a regular file)"), init_py) | |
214 | ||
215 | # Either not in a package at all (__init__.py not expected), or | |
216 | # __init__.py doesn't exist -- so don't return the filename. | |
217 | return None | |
218 | ||
219 | # check_package () | |
220 | ||
221 | ||
222 | def check_module (self, module, module_file): | |
223 | if not os.path.isfile(module_file): | |
224 | log.warn("file %s (for module %s) not found", module_file, module) | |
225 | return 0 | |
226 | else: | |
227 | return 1 | |
228 | ||
229 | # check_module () | |
230 | ||
231 | ||
232 | def find_package_modules (self, package, package_dir): | |
233 | self.check_package(package, package_dir) | |
234 | module_files = glob(os.path.join(package_dir, "*.py")) | |
235 | modules = [] | |
236 | setup_script = os.path.abspath(self.distribution.script_name) | |
237 | ||
238 | for f in module_files: | |
239 | abs_f = os.path.abspath(f) | |
240 | if abs_f != setup_script: | |
241 | module = os.path.splitext(os.path.basename(f))[0] | |
242 | modules.append((package, module, f)) | |
243 | else: | |
244 | self.debug_print("excluding %s" % setup_script) | |
245 | return modules | |
246 | ||
247 | ||
248 | def find_modules (self): | |
249 | """Finds individually-specified Python modules, ie. those listed by | |
250 | module name in 'self.py_modules'. Returns a list of tuples (package, | |
251 | module_base, filename): 'package' is a tuple of the path through | |
252 | package-space to the module; 'module_base' is the bare (no | |
253 | packages, no dots) module name, and 'filename' is the path to the | |
254 | ".py" file (relative to the distribution root) that implements the | |
255 | module. | |
256 | """ | |
257 | ||
258 | # Map package names to tuples of useful info about the package: | |
259 | # (package_dir, checked) | |
260 | # package_dir - the directory where we'll find source files for | |
261 | # this package | |
262 | # checked - true if we have checked that the package directory | |
263 | # is valid (exists, contains __init__.py, ... ?) | |
264 | packages = {} | |
265 | ||
266 | # List of (package, module, filename) tuples to return | |
267 | modules = [] | |
268 | ||
269 | # We treat modules-in-packages almost the same as toplevel modules, | |
270 | # just the "package" for a toplevel is empty (either an empty | |
271 | # string or empty list, depending on context). Differences: | |
272 | # - don't check for __init__.py in directory for empty package | |
273 | ||
274 | for module in self.py_modules: | |
275 | path = string.split(module, '.') | |
276 | package = string.join(path[0:-1], '.') | |
277 | module_base = path[-1] | |
278 | ||
279 | try: | |
280 | (package_dir, checked) = packages[package] | |
281 | except KeyError: | |
282 | package_dir = self.get_package_dir(package) | |
283 | checked = 0 | |
284 | ||
285 | if not checked: | |
286 | init_py = self.check_package(package, package_dir) | |
287 | packages[package] = (package_dir, 1) | |
288 | if init_py: | |
289 | modules.append((package, "__init__", init_py)) | |
290 | ||
291 | # XXX perhaps we should also check for just .pyc files | |
292 | # (so greedy closed-source bastards can distribute Python | |
293 | # modules too) | |
294 | module_file = os.path.join(package_dir, module_base + ".py") | |
295 | if not self.check_module(module, module_file): | |
296 | continue | |
297 | ||
298 | modules.append((package, module_base, module_file)) | |
299 | ||
300 | return modules | |
301 | ||
302 | # find_modules () | |
303 | ||
304 | ||
305 | def find_all_modules (self): | |
306 | """Compute the list of all modules that will be built, whether | |
307 | they are specified one-module-at-a-time ('self.py_modules') or | |
308 | by whole packages ('self.packages'). Return a list of tuples | |
309 | (package, module, module_file), just like 'find_modules()' and | |
310 | 'find_package_modules()' do.""" | |
311 | ||
312 | modules = [] | |
313 | if self.py_modules: | |
314 | modules.extend(self.find_modules()) | |
315 | if self.packages: | |
316 | for package in self.packages: | |
317 | package_dir = self.get_package_dir(package) | |
318 | m = self.find_package_modules(package, package_dir) | |
319 | modules.extend(m) | |
320 | ||
321 | return modules | |
322 | ||
323 | # find_all_modules () | |
324 | ||
325 | ||
326 | def get_source_files (self): | |
327 | ||
328 | modules = self.find_all_modules() | |
329 | filenames = [] | |
330 | for module in modules: | |
331 | filenames.append(module[-1]) | |
332 | ||
333 | return filenames | |
334 | ||
335 | ||
336 | def get_module_outfile (self, build_dir, package, module): | |
337 | outfile_path = [build_dir] + list(package) + [module + ".py"] | |
338 | return apply(os.path.join, outfile_path) | |
339 | ||
340 | ||
341 | def get_outputs (self, include_bytecode=1): | |
342 | modules = self.find_all_modules() | |
343 | outputs = [] | |
344 | for (package, module, module_file) in modules: | |
345 | package = string.split(package, '.') | |
346 | filename = self.get_module_outfile(self.build_lib, package, module) | |
347 | outputs.append(filename) | |
348 | if include_bytecode: | |
349 | if self.compile: | |
350 | outputs.append(filename + "c") | |
351 | if self.optimize > 0: | |
352 | outputs.append(filename + "o") | |
353 | ||
354 | outputs += [ | |
355 | os.path.join(build_dir, filename) | |
356 | for package, src_dir, build_dir, filenames in self.data_files | |
357 | for filename in filenames | |
358 | ] | |
359 | ||
360 | return outputs | |
361 | ||
362 | ||
363 | def build_module (self, module, module_file, package): | |
364 | if type(package) is StringType: | |
365 | package = string.split(package, '.') | |
366 | elif type(package) not in (ListType, TupleType): | |
367 | raise TypeError, \ | |
368 | "'package' must be a string (dot-separated), list, or tuple" | |
369 | ||
370 | # Now put the module source file into the "build" area -- this is | |
371 | # easy, we just copy it somewhere under self.build_lib (the build | |
372 | # directory for Python source). | |
373 | outfile = self.get_module_outfile(self.build_lib, package, module) | |
374 | dir = os.path.dirname(outfile) | |
375 | self.mkpath(dir) | |
376 | return self.copy_file(module_file, outfile, preserve_mode=0) | |
377 | ||
378 | ||
379 | def build_modules (self): | |
380 | ||
381 | modules = self.find_modules() | |
382 | for (package, module, module_file) in modules: | |
383 | ||
384 | # Now "build" the module -- ie. copy the source file to | |
385 | # self.build_lib (the build directory for Python source). | |
386 | # (Actually, it gets copied to the directory for this package | |
387 | # under self.build_lib.) | |
388 | self.build_module(module, module_file, package) | |
389 | ||
390 | # build_modules () | |
391 | ||
392 | ||
393 | def build_packages (self): | |
394 | ||
395 | for package in self.packages: | |
396 | ||
397 | # Get list of (package, module, module_file) tuples based on | |
398 | # scanning the package directory. 'package' is only included | |
399 | # in the tuple so that 'find_modules()' and | |
400 | # 'find_package_tuples()' have a consistent interface; it's | |
401 | # ignored here (apart from a sanity check). Also, 'module' is | |
402 | # the *unqualified* module name (ie. no dots, no package -- we | |
403 | # already know its package!), and 'module_file' is the path to | |
404 | # the .py file, relative to the current directory | |
405 | # (ie. including 'package_dir'). | |
406 | package_dir = self.get_package_dir(package) | |
407 | modules = self.find_package_modules(package, package_dir) | |
408 | ||
409 | # Now loop over the modules we found, "building" each one (just | |
410 | # copy it to self.build_lib). | |
411 | for (package_, module, module_file) in modules: | |
412 | assert package == package_ | |
413 | self.build_module(module, module_file, package) | |
414 | ||
415 | # build_packages () | |
416 | ||
417 | ||
418 | def byte_compile (self, files): | |
419 | from distutils.util import byte_compile | |
420 | prefix = self.build_lib | |
421 | if prefix[-1] != os.sep: | |
422 | prefix = prefix + os.sep | |
423 | ||
424 | # XXX this code is essentially the same as the 'byte_compile() | |
425 | # method of the "install_lib" command, except for the determination | |
426 | # of the 'prefix' string. Hmmm. | |
427 | ||
428 | if self.compile: | |
429 | byte_compile(files, optimize=0, | |
430 | force=self.force, prefix=prefix, dry_run=self.dry_run) | |
431 | if self.optimize > 0: | |
432 | byte_compile(files, optimize=self.optimize, | |
433 | force=self.force, prefix=prefix, dry_run=self.dry_run) | |
434 | ||
435 | # class build_py |