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