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