408 lines
16 KiB
Python
408 lines
16 KiB
Python
|
"""distutils.command.build_py
|
||
|
|
||
|
Implements the Distutils 'build_py' command."""
|
||
|
|
||
|
import os
|
||
|
import importlib.util
|
||
|
import sys
|
||
|
import glob
|
||
|
|
||
|
from ..core import Command
|
||
|
from ..errors import DistutilsOptionError, DistutilsFileError
|
||
|
from ..util import convert_path
|
||
|
from distutils._log import log
|
||
|
|
||
|
|
||
|
class build_py(Command):
|
||
|
|
||
|
description = "\"build\" pure Python modules (copy to build directory)"
|
||
|
|
||
|
user_options = [
|
||
|
('build-lib=', 'd', "directory to \"build\" (copy) to"),
|
||
|
('compile', 'c', "compile .py to .pyc"),
|
||
|
('no-compile', None, "don't compile .py files [default]"),
|
||
|
(
|
||
|
'optimize=',
|
||
|
'O',
|
||
|
"also compile with optimization: -O1 for \"python -O\", "
|
||
|
"-O2 for \"python -OO\", and -O0 to disable [default: -O0]",
|
||
|
),
|
||
|
('force', 'f', "forcibly build everything (ignore file timestamps)"),
|
||
|
]
|
||
|
|
||
|
boolean_options = ['compile', 'force']
|
||
|
negative_opt = {'no-compile': 'compile'}
|
||
|
|
||
|
def initialize_options(self):
|
||
|
self.build_lib = None
|
||
|
self.py_modules = None
|
||
|
self.package = None
|
||
|
self.package_data = None
|
||
|
self.package_dir = None
|
||
|
self.compile = 0
|
||
|
self.optimize = 0
|
||
|
self.force = None
|
||
|
|
||
|
def finalize_options(self):
|
||
|
self.set_undefined_options(
|
||
|
'build', ('build_lib', 'build_lib'), ('force', 'force')
|
||
|
)
|
||
|
|
||
|
# Get the distribution options that are aliases for build_py
|
||
|
# options -- list of packages and list of modules.
|
||
|
self.packages = self.distribution.packages
|
||
|
self.py_modules = self.distribution.py_modules
|
||
|
self.package_data = self.distribution.package_data
|
||
|
self.package_dir = {}
|
||
|
if self.distribution.package_dir:
|
||
|
for name, path in self.distribution.package_dir.items():
|
||
|
self.package_dir[name] = convert_path(path)
|
||
|
self.data_files = self.get_data_files()
|
||
|
|
||
|
# Ick, copied straight from install_lib.py (fancy_getopt needs a
|
||
|
# type system! Hell, *everything* needs a type system!!!)
|
||
|
if not isinstance(self.optimize, int):
|
||
|
try:
|
||
|
self.optimize = int(self.optimize)
|
||
|
assert 0 <= self.optimize <= 2
|
||
|
except (ValueError, AssertionError):
|
||
|
raise DistutilsOptionError("optimize must be 0, 1, or 2")
|
||
|
|
||
|
def run(self):
|
||
|
# XXX copy_file by default preserves atime and mtime. IMHO this is
|
||
|
# the right thing to do, but perhaps it should be an option -- in
|
||
|
# particular, a site administrator might want installed files to
|
||
|
# reflect the time of installation rather than the last
|
||
|
# modification time before the installed release.
|
||
|
|
||
|
# XXX copy_file by default preserves mode, which appears to be the
|
||
|
# wrong thing to do: if a file is read-only in the working
|
||
|
# directory, we want it to be installed read/write so that the next
|
||
|
# installation of the same module distribution can overwrite it
|
||
|
# without problems. (This might be a Unix-specific issue.) Thus
|
||
|
# we turn off 'preserve_mode' when copying to the build directory,
|
||
|
# since the build directory is supposed to be exactly what the
|
||
|
# installation will look like (ie. we preserve mode when
|
||
|
# installing).
|
||
|
|
||
|
# Two options control which modules will be installed: 'packages'
|
||
|
# and 'py_modules'. The former lets us work with whole packages, not
|
||
|
# specifying individual modules at all; the latter is for
|
||
|
# specifying modules one-at-a-time.
|
||
|
|
||
|
if self.py_modules:
|
||
|
self.build_modules()
|
||
|
if self.packages:
|
||
|
self.build_packages()
|
||
|
self.build_package_data()
|
||
|
|
||
|
self.byte_compile(self.get_outputs(include_bytecode=0))
|
||
|
|
||
|
def get_data_files(self):
|
||
|
"""Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
|
||
|
data = []
|
||
|
if not self.packages:
|
||
|
return data
|
||
|
for package in self.packages:
|
||
|
# Locate package source directory
|
||
|
src_dir = self.get_package_dir(package)
|
||
|
|
||
|
# Compute package build directory
|
||
|
build_dir = os.path.join(*([self.build_lib] + package.split('.')))
|
||
|
|
||
|
# Length of path to strip from found files
|
||
|
plen = 0
|
||
|
if src_dir:
|
||
|
plen = len(src_dir) + 1
|
||
|
|
||
|
# Strip directory from globbed filenames
|
||
|
filenames = [file[plen:] for file in self.find_data_files(package, src_dir)]
|
||
|
data.append((package, src_dir, build_dir, filenames))
|
||
|
return data
|
||
|
|
||
|
def find_data_files(self, package, src_dir):
|
||
|
"""Return filenames for package's data files in 'src_dir'"""
|
||
|
globs = self.package_data.get('', []) + self.package_data.get(package, [])
|
||
|
files = []
|
||
|
for pattern in globs:
|
||
|
# Each pattern has to be converted to a platform-specific path
|
||
|
filelist = glob.glob(
|
||
|
os.path.join(glob.escape(src_dir), convert_path(pattern))
|
||
|
)
|
||
|
# Files that match more than one pattern are only added once
|
||
|
files.extend(
|
||
|
[fn for fn in filelist if fn not in files and os.path.isfile(fn)]
|
||
|
)
|
||
|
return files
|
||
|
|
||
|
def build_package_data(self):
|
||
|
"""Copy data files into build directory"""
|
||
|
for package, src_dir, build_dir, filenames in self.data_files:
|
||
|
for filename in filenames:
|
||
|
target = os.path.join(build_dir, filename)
|
||
|
self.mkpath(os.path.dirname(target))
|
||
|
self.copy_file(
|
||
|
os.path.join(src_dir, filename), target, preserve_mode=False
|
||
|
)
|
||
|
|
||
|
def get_package_dir(self, package):
|
||
|
"""Return the directory, relative to the top of the source
|
||
|
distribution, where package 'package' should be found
|
||
|
(at least according to the 'package_dir' option, if any)."""
|
||
|
path = package.split('.')
|
||
|
|
||
|
if not self.package_dir:
|
||
|
if path:
|
||
|
return os.path.join(*path)
|
||
|
else:
|
||
|
return ''
|
||
|
else:
|
||
|
tail = []
|
||
|
while path:
|
||
|
try:
|
||
|
pdir = self.package_dir['.'.join(path)]
|
||
|
except KeyError:
|
||
|
tail.insert(0, path[-1])
|
||
|
del path[-1]
|
||
|
else:
|
||
|
tail.insert(0, pdir)
|
||
|
return os.path.join(*tail)
|
||
|
else:
|
||
|
# Oops, got all the way through 'path' without finding a
|
||
|
# match in package_dir. If package_dir defines a directory
|
||
|
# for the root (nameless) package, then fallback on it;
|
||
|
# otherwise, we might as well have not consulted
|
||
|
# package_dir at all, as we just use the directory implied
|
||
|
# by 'tail' (which should be the same as the original value
|
||
|
# of 'path' at this point).
|
||
|
pdir = self.package_dir.get('')
|
||
|
if pdir is not None:
|
||
|
tail.insert(0, pdir)
|
||
|
|
||
|
if tail:
|
||
|
return os.path.join(*tail)
|
||
|
else:
|
||
|
return ''
|
||
|
|
||
|
def check_package(self, package, package_dir):
|
||
|
# Empty dir name means current directory, which we can probably
|
||
|
# assume exists. Also, os.path.exists and isdir don't know about
|
||
|
# my "empty string means current dir" convention, so we have to
|
||
|
# circumvent them.
|
||
|
if package_dir != "":
|
||
|
if not os.path.exists(package_dir):
|
||
|
raise DistutilsFileError(
|
||
|
"package directory '%s' does not exist" % package_dir
|
||
|
)
|
||
|
if not os.path.isdir(package_dir):
|
||
|
raise DistutilsFileError(
|
||
|
"supposed package directory '%s' exists, "
|
||
|
"but is not a directory" % package_dir
|
||
|
)
|
||
|
|
||
|
# Directories without __init__.py are namespace packages (PEP 420).
|
||
|
if package:
|
||
|
init_py = os.path.join(package_dir, "__init__.py")
|
||
|
if os.path.isfile(init_py):
|
||
|
return init_py
|
||
|
|
||
|
# Either not in a package at all (__init__.py not expected), or
|
||
|
# __init__.py doesn't exist -- so don't return the filename.
|
||
|
return None
|
||
|
|
||
|
def check_module(self, module, module_file):
|
||
|
if not os.path.isfile(module_file):
|
||
|
log.warning("file %s (for module %s) not found", module_file, module)
|
||
|
return False
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
def find_package_modules(self, package, package_dir):
|
||
|
self.check_package(package, package_dir)
|
||
|
module_files = glob.glob(os.path.join(glob.escape(package_dir), "*.py"))
|
||
|
modules = []
|
||
|
setup_script = os.path.abspath(self.distribution.script_name)
|
||
|
|
||
|
for f in module_files:
|
||
|
abs_f = os.path.abspath(f)
|
||
|
if abs_f != setup_script:
|
||
|
module = os.path.splitext(os.path.basename(f))[0]
|
||
|
modules.append((package, module, f))
|
||
|
else:
|
||
|
self.debug_print("excluding %s" % setup_script)
|
||
|
return modules
|
||
|
|
||
|
def find_modules(self):
|
||
|
"""Finds individually-specified Python modules, ie. those listed by
|
||
|
module name in 'self.py_modules'. Returns a list of tuples (package,
|
||
|
module_base, filename): 'package' is a tuple of the path through
|
||
|
package-space to the module; 'module_base' is the bare (no
|
||
|
packages, no dots) module name, and 'filename' is the path to the
|
||
|
".py" file (relative to the distribution root) that implements the
|
||
|
module.
|
||
|
"""
|
||
|
# Map package names to tuples of useful info about the package:
|
||
|
# (package_dir, checked)
|
||
|
# package_dir - the directory where we'll find source files for
|
||
|
# this package
|
||
|
# checked - true if we have checked that the package directory
|
||
|
# is valid (exists, contains __init__.py, ... ?)
|
||
|
packages = {}
|
||
|
|
||
|
# List of (package, module, filename) tuples to return
|
||
|
modules = []
|
||
|
|
||
|
# We treat modules-in-packages almost the same as toplevel modules,
|
||
|
# just the "package" for a toplevel is empty (either an empty
|
||
|
# string or empty list, depending on context). Differences:
|
||
|
# - don't check for __init__.py in directory for empty package
|
||
|
for module in self.py_modules:
|
||
|
path = module.split('.')
|
||
|
package = '.'.join(path[0:-1])
|
||
|
module_base = path[-1]
|
||
|
|
||
|
try:
|
||
|
(package_dir, checked) = packages[package]
|
||
|
except KeyError:
|
||
|
package_dir = self.get_package_dir(package)
|
||
|
checked = 0
|
||
|
|
||
|
if not checked:
|
||
|
init_py = self.check_package(package, package_dir)
|
||
|
packages[package] = (package_dir, 1)
|
||
|
if init_py:
|
||
|
modules.append((package, "__init__", init_py))
|
||
|
|
||
|
# XXX perhaps we should also check for just .pyc files
|
||
|
# (so greedy closed-source bastards can distribute Python
|
||
|
# modules too)
|
||
|
module_file = os.path.join(package_dir, module_base + ".py")
|
||
|
if not self.check_module(module, module_file):
|
||
|
continue
|
||
|
|
||
|
modules.append((package, module_base, module_file))
|
||
|
|
||
|
return modules
|
||
|
|
||
|
def find_all_modules(self):
|
||
|
"""Compute the list of all modules that will be built, whether
|
||
|
they are specified one-module-at-a-time ('self.py_modules') or
|
||
|
by whole packages ('self.packages'). Return a list of tuples
|
||
|
(package, module, module_file), just like 'find_modules()' and
|
||
|
'find_package_modules()' do."""
|
||
|
modules = []
|
||
|
if self.py_modules:
|
||
|
modules.extend(self.find_modules())
|
||
|
if self.packages:
|
||
|
for package in self.packages:
|
||
|
package_dir = self.get_package_dir(package)
|
||
|
m = self.find_package_modules(package, package_dir)
|
||
|
modules.extend(m)
|
||
|
return modules
|
||
|
|
||
|
def get_source_files(self):
|
||
|
return [module[-1] for module in self.find_all_modules()]
|
||
|
|
||
|
def get_module_outfile(self, build_dir, package, module):
|
||
|
outfile_path = [build_dir] + list(package) + [module + ".py"]
|
||
|
return os.path.join(*outfile_path)
|
||
|
|
||
|
def get_outputs(self, include_bytecode=1):
|
||
|
modules = self.find_all_modules()
|
||
|
outputs = []
|
||
|
for (package, module, module_file) in modules:
|
||
|
package = package.split('.')
|
||
|
filename = self.get_module_outfile(self.build_lib, package, module)
|
||
|
outputs.append(filename)
|
||
|
if include_bytecode:
|
||
|
if self.compile:
|
||
|
outputs.append(
|
||
|
importlib.util.cache_from_source(filename, optimization='')
|
||
|
)
|
||
|
if self.optimize > 0:
|
||
|
outputs.append(
|
||
|
importlib.util.cache_from_source(
|
||
|
filename, optimization=self.optimize
|
||
|
)
|
||
|
)
|
||
|
|
||
|
outputs += [
|
||
|
os.path.join(build_dir, filename)
|
||
|
for package, src_dir, build_dir, filenames in self.data_files
|
||
|
for filename in filenames
|
||
|
]
|
||
|
|
||
|
return outputs
|
||
|
|
||
|
def build_module(self, module, module_file, package):
|
||
|
if isinstance(package, str):
|
||
|
package = package.split('.')
|
||
|
elif not isinstance(package, (list, tuple)):
|
||
|
raise TypeError(
|
||
|
"'package' must be a string (dot-separated), list, or tuple"
|
||
|
)
|
||
|
|
||
|
# Now put the module source file into the "build" area -- this is
|
||
|
# easy, we just copy it somewhere under self.build_lib (the build
|
||
|
# directory for Python source).
|
||
|
outfile = self.get_module_outfile(self.build_lib, package, module)
|
||
|
dir = os.path.dirname(outfile)
|
||
|
self.mkpath(dir)
|
||
|
return self.copy_file(module_file, outfile, preserve_mode=0)
|
||
|
|
||
|
def build_modules(self):
|
||
|
modules = self.find_modules()
|
||
|
for (package, module, module_file) in modules:
|
||
|
# Now "build" the module -- ie. copy the source file to
|
||
|
# self.build_lib (the build directory for Python source).
|
||
|
# (Actually, it gets copied to the directory for this package
|
||
|
# under self.build_lib.)
|
||
|
self.build_module(module, module_file, package)
|
||
|
|
||
|
def build_packages(self):
|
||
|
for package in self.packages:
|
||
|
# Get list of (package, module, module_file) tuples based on
|
||
|
# scanning the package directory. 'package' is only included
|
||
|
# in the tuple so that 'find_modules()' and
|
||
|
# 'find_package_tuples()' have a consistent interface; it's
|
||
|
# ignored here (apart from a sanity check). Also, 'module' is
|
||
|
# the *unqualified* module name (ie. no dots, no package -- we
|
||
|
# already know its package!), and 'module_file' is the path to
|
||
|
# the .py file, relative to the current directory
|
||
|
# (ie. including 'package_dir').
|
||
|
package_dir = self.get_package_dir(package)
|
||
|
modules = self.find_package_modules(package, package_dir)
|
||
|
|
||
|
# Now loop over the modules we found, "building" each one (just
|
||
|
# copy it to self.build_lib).
|
||
|
for (package_, module, module_file) in modules:
|
||
|
assert package == package_
|
||
|
self.build_module(module, module_file, package)
|
||
|
|
||
|
def byte_compile(self, files):
|
||
|
if sys.dont_write_bytecode:
|
||
|
self.warn('byte-compiling is disabled, skipping.')
|
||
|
return
|
||
|
|
||
|
from ..util import byte_compile
|
||
|
|
||
|
prefix = self.build_lib
|
||
|
if prefix[-1] != os.sep:
|
||
|
prefix = prefix + os.sep
|
||
|
|
||
|
# XXX this code is essentially the same as the 'byte_compile()
|
||
|
# method of the "install_lib" command, except for the determination
|
||
|
# of the 'prefix' string. Hmmm.
|
||
|
if self.compile:
|
||
|
byte_compile(
|
||
|
files, optimize=0, force=self.force, prefix=prefix, dry_run=self.dry_run
|
||
|
)
|
||
|
if self.optimize > 0:
|
||
|
byte_compile(
|
||
|
files,
|
||
|
optimize=self.optimize,
|
||
|
force=self.force,
|
||
|
prefix=prefix,
|
||
|
dry_run=self.dry_run,
|
||
|
)
|