codekingpro/portable-devtools
115k
1"""distutils.command.install_lib2 3Implements the Distutils 'install_lib' command4(install all Python modules)."""5 6from __future__ import annotations7 8import importlib.util9import os10import sys11from typing import Any, ClassVar12 13from ..core import Command14from ..errors import DistutilsOptionError15 16# Extension for Python source files.17PYTHON_SOURCE_EXTENSION = ".py"18 19 20class install_lib(Command):21 description = "install all Python modules (extensions and pure Python)"22 23 # The byte-compilation options are a tad confusing. Here are the24 # possible scenarios:25 # 1) no compilation at all (--no-compile --no-optimize)26 # 2) compile .pyc only (--compile --no-optimize; default)27 # 3) compile .pyc and "opt-1" .pyc (--compile --optimize)28 # 4) compile "opt-1" .pyc only (--no-compile --optimize)29 # 5) compile .pyc and "opt-2" .pyc (--compile --optimize-more)30 # 6) compile "opt-2" .pyc only (--no-compile --optimize-more)31 #32 # The UI for this is two options, 'compile' and 'optimize'.33 # 'compile' is strictly boolean, and only decides whether to34 # generate .pyc files. 'optimize' is three-way (0, 1, or 2), and35 # decides both whether to generate .pyc files and what level of36 # optimization to use.37 38 user_options = [39 ('install-dir=', 'd', "directory to install to"),40 ('build-dir=', 'b', "build directory (where to install from)"),41 ('force', 'f', "force installation (overwrite existing files)"),42 ('compile', 'c', "compile .py to .pyc [default]"),43 ('no-compile', None, "don't compile .py files"),44 (45 'optimize=',46 'O',47 "also compile with optimization: -O1 for \"python -O\", "48 "-O2 for \"python -OO\", and -O0 to disable [default: -O0]",49 ),50 ('skip-build', None, "skip the build steps"),51 ]52 53 boolean_options: ClassVar[list[str]] = ['force', 'compile', 'skip-build']54 negative_opt: ClassVar[dict[str, str]] = {'no-compile': 'compile'}55 56 def initialize_options(self):57 # let the 'install' command dictate our installation directory58 self.install_dir = None59 self.build_dir = None60 self.force = False61 self.compile = None62 self.optimize = None63 self.skip_build = None64 65 def finalize_options(self) -> None:66 # Get all the information we need to install pure Python modules67 # from the umbrella 'install' command -- build (source) directory,68 # install (target) directory, and whether to compile .py files.69 self.set_undefined_options(70 'install',71 ('build_lib', 'build_dir'),72 ('install_lib', 'install_dir'),73 ('force', 'force'),74 ('compile', 'compile'),75 ('optimize', 'optimize'),76 ('skip_build', 'skip_build'),77 )78 79 if self.compile is None:80 self.compile = True81 if self.optimize is None:82 self.optimize = False83 84 if not isinstance(self.optimize, int):85 try:86 self.optimize = int(self.optimize)87 except ValueError:88 pass89 if self.optimize not in (0, 1, 2):90 raise DistutilsOptionError("optimize must be 0, 1, or 2")91 92 def run(self) -> None:93 # Make sure we have built everything we need first94 self.build()95 96 # Install everything: simply dump the entire contents of the build97 # directory to the installation directory (that's the beauty of98 # having a build directory!)99 outfiles = self.install()100 101 # (Optionally) compile .py to .pyc102 if outfiles is not None and self.distribution.has_pure_modules():103 self.byte_compile(outfiles)104 105 # -- Top-level worker functions ------------------------------------106 # (called from 'run()')107 108 def build(self) -> None:109 if not self.skip_build:110 if self.distribution.has_pure_modules():111 self.run_command('build_py')112 if self.distribution.has_ext_modules():113 self.run_command('build_ext')114 115 # Any: https://typing.readthedocs.io/en/latest/guides/writing_stubs.html#the-any-trick116 def install(self) -> list[str] | Any:117 if os.path.isdir(self.build_dir):118 outfiles = self.copy_tree(self.build_dir, self.install_dir)119 else:120 self.warn(121 f"'{self.build_dir}' does not exist -- no Python modules to install"122 )123 return124 return outfiles125 126 def byte_compile(self, files) -> None:127 if sys.dont_write_bytecode:128 self.warn('byte-compiling is disabled, skipping.')129 return130 131 from ..util import byte_compile132 133 # Get the "--root" directory supplied to the "install" command,134 # and use it as a prefix to strip off the purported filename135 # encoded in bytecode files. This is far from complete, but it136 # should at least generate usable bytecode in RPM distributions.137 install_root = self.get_finalized_command('install').root138 139 if self.compile:140 byte_compile(141 files,142 optimize=0,143 force=self.force,144 prefix=install_root,145 )146 if self.optimize > 0:147 byte_compile(148 files,149 optimize=self.optimize,150 force=self.force,151 prefix=install_root,152 verbose=self.verbose,153 )154 155 # -- Utility methods -----------------------------------------------156 157 def _mutate_outputs(self, has_any, build_cmd, cmd_option, output_dir):158 if not has_any:159 return []160 161 build_cmd = self.get_finalized_command(build_cmd)162 build_files = build_cmd.get_outputs()163 build_dir = getattr(build_cmd, cmd_option)164 165 prefix_len = len(build_dir) + len(os.sep)166 outputs = [os.path.join(output_dir, file[prefix_len:]) for file in build_files]167 168 return outputs169 170 def _bytecode_filenames(self, py_filenames):171 bytecode_files = []172 for py_file in py_filenames:173 # Since build_py handles package data installation, the174 # list of outputs can contain more than just .py files.175 # Make sure we only report bytecode for the .py files.176 ext = os.path.splitext(os.path.normcase(py_file))[1]177 if ext != PYTHON_SOURCE_EXTENSION:178 continue179 if self.compile:180 bytecode_files.append(181 importlib.util.cache_from_source(py_file, optimization='')182 )183 if self.optimize > 0:184 bytecode_files.append(185 importlib.util.cache_from_source(186 py_file, optimization=self.optimize187 )188 )189 190 return bytecode_files191 192 # -- External interface --------------------------------------------193 # (called by outsiders)194 195 def get_outputs(self):196 """Return the list of files that would be installed if this command197 were actually run. Not affected by the "dry-run" flag or whether198 modules have actually been built yet.199 """200 pure_outputs = self._mutate_outputs(201 self.distribution.has_pure_modules(),202 'build_py',203 'build_lib',204 self.install_dir,205 )206 if self.compile:207 bytecode_outputs = self._bytecode_filenames(pure_outputs)208 else:209 bytecode_outputs = []210 211 ext_outputs = self._mutate_outputs(212 self.distribution.has_ext_modules(),213 'build_ext',214 'build_lib',215 self.install_dir,216 )217 218 return pure_outputs + bytecode_outputs + ext_outputs219 220 def get_inputs(self):221 """Get the list of files that are input to this command, ie. the222 files that get installed as they are named in the build tree.223 The files in this list correspond one-to-one to the output224 filenames returned by 'get_outputs()'.225 """226 inputs = []227 228 if self.distribution.has_pure_modules():229 build_py = self.get_finalized_command('build_py')230 inputs.extend(build_py.get_outputs())231 232 if self.distribution.has_ext_modules():233 build_ext = self.get_finalized_command('build_ext')234 inputs.extend(build_ext.get_outputs())235 236 return inputs237 