codekingpro/portable-devtools
114k
1"""distutils.command.build_py2 3Implements the Distutils 'build_py' command."""4 5import glob6import importlib.util7import os8import sys9from distutils._log import log10from typing import ClassVar11 12from ..core import Command13from ..errors import DistutilsFileError, DistutilsOptionError14from ..util import convert_path15 16 17class build_py(Command):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: ClassVar[list[str]] = ['compile', 'force']34 negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}35 36 def initialize_options(self):37 self.build_lib = None38 self.py_modules = None39 self.package = None40 self.package_data = None41 self.package_dir = None42 self.compile = False43 self.optimize = 044 self.force = None45 46 def finalize_options(self) -> None: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_py52 # options -- list of packages and list of modules.53 self.packages = self.distribution.packages54 self.py_modules = self.distribution.py_modules55 self.package_data = self.distribution.package_data56 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 a63 # 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 <= 268 except (ValueError, AssertionError):69 raise DistutilsOptionError("optimize must be 0, 1, or 2")70 71 def run(self) -> None:72 # XXX copy_file by default preserves atime and mtime. IMHO this is73 # the right thing to do, but perhaps it should be an option -- in74 # particular, a site administrator might want installed files to75 # reflect the time of installation rather than the last76 # modification time before the installed release.77 78 # XXX copy_file by default preserves mode, which appears to be the79 # wrong thing to do: if a file is read-only in the working80 # directory, we want it to be installed read/write so that the next81 # installation of the same module distribution can overwrite it82 # without problems. (This might be a Unix-specific issue.) Thus83 # we turn off 'preserve_mode' when copying to the build directory,84 # since the build directory is supposed to be exactly what the85 # installation will look like (ie. we preserve mode when86 # installing).87 88 # Two options control which modules will be installed: 'packages'89 # and 'py_modules'. The former lets us work with whole packages, not90 # specifying individual modules at all; the latter is for91 # 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=False))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 data106 for package in self.packages:107 # Locate package source directory108 src_dir = self.get_package_dir(package)109 110 # Compute package build directory111 build_dir = os.path.join(*([self.build_lib] + package.split('.')))112 113 # Length of path to strip from found files114 plen = 0115 if src_dir:116 plen = len(src_dir) + 1117 118 # Strip directory from globbed filenames119 filenames = [file[plen:] for file in self.find_data_files(package, src_dir)]120 data.append((package, src_dir, build_dir, filenames))121 return data122 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 path129 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 once133 files.extend([134 fn for fn in filelist if fn not in files and os.path.isfile(fn)135 ])136 return files137 138 def build_package_data(self) -> None: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=False146 )147 148 def get_package_dir(self, package):149 """Return the directory, relative to the top of the source150 distribution, where package 'package' should be found151 (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 a172 # match in package_dir. If package_dir defines a directory173 # for the root (nameless) package, then fallback on it;174 # otherwise, we might as well have not consulted175 # package_dir at all, as we just use the directory implied176 # by 'tail' (which should be the same as the original value177 # 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 probably189 # assume exists. Also, os.path.exists and isdir don't know about190 # my "empty string means current dir" convention, so we have to191 # circumvent them.192 if package_dir != "":193 if not os.path.exists(package_dir):194 raise DistutilsFileError(195 f"package directory '{package_dir}' does not exist"196 )197 if not os.path.isdir(package_dir):198 raise DistutilsFileError(199 f"supposed package directory '{package_dir}' exists, "200 "but is not a directory"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_py208 209 # Either not in a package at all (__init__.py not expected), or210 # __init__.py doesn't exist -- so don't return the filename.211 return None212 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 False217 else:218 return True219 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(f"excluding {setup_script}")233 return modules234 235 def find_modules(self):236 """Finds individually-specified Python modules, ie. those listed by237 module name in 'self.py_modules'. Returns a list of tuples (package,238 module_base, filename): 'package' is a tuple of the path through239 package-space to the module; 'module_base' is the bare (no240 packages, no dots) module name, and 'filename' is the path to the241 ".py" file (relative to the distribution root) that implements the242 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 for247 # this package248 # checked - true if we have checked that the package directory249 # is valid (exists, contains __init__.py, ... ?)250 packages = {}251 252 # List of (package, module, filename) tuples to return253 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 empty257 # string or empty list, depending on context). Differences:258 # - don't check for __init__.py in directory for empty package259 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 = False269 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 files277 # (so greedy closed-source bastards can distribute Python278 # modules too)279 module_file = os.path.join(package_dir, module_base + ".py")280 if not self.check_module(module, module_file):281 continue282 283 modules.append((package, module_base, module_file))284 285 return modules286 287 def find_all_modules(self):288 """Compute the list of all modules that will be built, whether289 they are specified one-module-at-a-time ('self.py_modules') or290 by whole packages ('self.packages'). Return a list of tuples291 (package, module, module_file), just like 'find_modules()' and292 '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 modules302 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: bool = True) -> list[str]: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.optimize326 )327 )328 329 outputs += [330 os.path.join(build_dir, filename)331 for package, src_dir, build_dir, filenames in self.data_files332 for filename in filenames333 ]334 335 return outputs336 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 is346 # easy, we just copy it somewhere under self.build_lib (the build347 # 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=False)352 353 def build_modules(self) -> None:354 modules = self.find_modules()355 for package, module, module_file in modules:356 # Now "build" the module -- ie. copy the source file to357 # self.build_lib (the build directory for Python source).358 # (Actually, it gets copied to the directory for this package359 # under self.build_lib.)360 self.build_module(module, module_file, package)361 362 def build_packages(self) -> None:363 for package in self.packages:364 # Get list of (package, module, module_file) tuples based on365 # scanning the package directory. 'package' is only included366 # in the tuple so that 'find_modules()' and367 # 'find_package_tuples()' have a consistent interface; it's368 # ignored here (apart from a sanity check). Also, 'module' is369 # the *unqualified* module name (ie. no dots, no package -- we370 # already know its package!), and 'module_file' is the path to371 # the .py file, relative to the current directory372 # (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 (just377 # 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) -> None:383 if sys.dont_write_bytecode:384 self.warn('byte-compiling is disabled, skipping.')385 return386 387 from ..util import byte_compile388 389 prefix = self.build_lib390 if prefix[-1] != os.sep:391 prefix = prefix + os.sep392 393 # XXX this code is essentially the same as the 'byte_compile()394 # method of the "install_lib" command, except for the determination395 # of the 'prefix' string. Hmmm.396 if self.compile:397 byte_compile(files, optimize=0, force=self.force, prefix=prefix)398 if self.optimize > 0:399 byte_compile(400 files,401 optimize=self.optimize,402 force=self.force,403 prefix=prefix,404 )405 