App Engine Python SDK version 1.9.9
[gae.git] / python / lib / distutils / distutils / command / build_py.py
blob04c455f0eb9a2efda0d6d199010f96bff26f3fe5
1 """distutils.command.build_py
3 Implements the Distutils 'build_py' command."""
5 __revision__ = "$Id$"
7 import os
8 import sys
9 from glob import glob
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
16 class build_py(Command):
18 description = "\"build\" pure Python modules (copy to build directory)"
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)"),
30 boolean_options = ['compile', 'force']
31 negative_opt = {'no-compile' : 'compile'}
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
43 def finalize_options(self):
44 self.set_undefined_options('build',
45 ('build_lib', 'build_lib'),
46 ('force', 'force'))
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()
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")
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.
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).
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.
90 if self.py_modules:
91 self.build_modules()
92 if self.packages:
93 self.build_packages()
94 self.build_package_data()
96 self.byte_compile(self.get_outputs(include_bytecode=0))
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)
107 # Compute package build directory
108 build_dir = os.path.join(*([self.build_lib] + package.split('.')))
110 # Length of path to strip from found files
111 plen = 0
112 if src_dir:
113 plen = len(src_dir)+1
115 # Strip directory from globbed filenames
116 filenames = [
117 file[plen:] for file in self.find_data_files(package, src_dir)
119 data.append((package, src_dir, build_dir, filenames))
120 return data
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
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)
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)."""
148 path = package.split('.')
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)
178 if tail:
179 return os.path.join(*tail)
180 else:
181 return ''
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)
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)
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
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
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)
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
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.
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 = {}
249 # List of (package, module, filename) tuples to return
250 modules = []
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]
261 try:
262 (package_dir, checked) = packages[package]
263 except KeyError:
264 package_dir = self.get_package_dir(package)
265 checked = 0
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))
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
280 modules.append((package, module_base, module_file))
282 return modules
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
300 def get_source_files(self):
301 return [module[-1] for module in self.find_all_modules()]
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)
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")
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
326 return outputs
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")
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)
343 def build_modules(self):
344 modules = self.find_modules()
345 for (package, module, module_file) in modules:
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)
353 def build_packages(self):
354 for package in self.packages:
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)
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)
374 def byte_compile(self, files):
375 if sys.dont_write_bytecode:
376 self.warn('byte-compiling is disabled, skipping.')
377 return
379 from distutils.util import byte_compile
380 prefix = self.build_lib
381 if prefix[-1] != os.sep:
382 prefix = prefix + os.sep
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.
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)