(root)/
Python-3.11.7/
Lib/
distutils/
command/
build_py.py
       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 distutils.core import Command
      11  from distutils.errors import *
      12  from distutils.util import convert_path, Mixin2to3
      13  from distutils import log
      14  
      15  class ESC[4;38;5;81mbuild_py (ESC[4;38;5;149mCommand):
      16  
      17      description = "\"build\" pure Python modules (copy to build directory)"
      18  
      19      user_options = [
      20          ('build-lib=', 'd', "directory to \"build\" (copy) to"),
      21          ('compile', 'c', "compile .py to .pyc"),
      22          ('no-compile', None, "don't compile .py files [default]"),
      23          ('optimize=', 'O',
      24           "also compile with optimization: -O1 for \"python -O\", "
      25           "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
      26          ('force', 'f', "forcibly build everything (ignore file timestamps)"),
      27          ]
      28  
      29      boolean_options = ['compile', 'force']
      30      negative_opt = {'no-compile' : 'compile'}
      31  
      32      def initialize_options(self):
      33          self.build_lib = None
      34          self.py_modules = None
      35          self.package = None
      36          self.package_data = None
      37          self.package_dir = None
      38          self.compile = 0
      39          self.optimize = 0
      40          self.force = None
      41  
      42      def finalize_options(self):
      43          self.set_undefined_options('build',
      44                                     ('build_lib', 'build_lib'),
      45                                     ('force', 'force'))
      46  
      47          # Get the distribution options that are aliases for build_py
      48          # options -- list of packages and list of modules.
      49          self.packages = self.distribution.packages
      50          self.py_modules = self.distribution.py_modules
      51          self.package_data = self.distribution.package_data
      52          self.package_dir = {}
      53          if self.distribution.package_dir:
      54              for name, path in self.distribution.package_dir.items():
      55                  self.package_dir[name] = convert_path(path)
      56          self.data_files = self.get_data_files()
      57  
      58          # Ick, copied straight from install_lib.py (fancy_getopt needs a
      59          # type system!  Hell, *everything* needs a type system!!!)
      60          if not isinstance(self.optimize, int):
      61              try:
      62                  self.optimize = int(self.optimize)
      63                  assert 0 <= self.optimize <= 2
      64              except (ValueError, AssertionError):
      65                  raise DistutilsOptionError("optimize must be 0, 1, or 2")
      66  
      67      def run(self):
      68          # XXX copy_file by default preserves atime and mtime.  IMHO this is
      69          # the right thing to do, but perhaps it should be an option -- in
      70          # particular, a site administrator might want installed files to
      71          # reflect the time of installation rather than the last
      72          # modification time before the installed release.
      73  
      74          # XXX copy_file by default preserves mode, which appears to be the
      75          # wrong thing to do: if a file is read-only in the working
      76          # directory, we want it to be installed read/write so that the next
      77          # installation of the same module distribution can overwrite it
      78          # without problems.  (This might be a Unix-specific issue.)  Thus
      79          # we turn off 'preserve_mode' when copying to the build directory,
      80          # since the build directory is supposed to be exactly what the
      81          # installation will look like (ie. we preserve mode when
      82          # installing).
      83  
      84          # Two options control which modules will be installed: 'packages'
      85          # and 'py_modules'.  The former lets us work with whole packages, not
      86          # specifying individual modules at all; the latter is for
      87          # specifying modules one-at-a-time.
      88  
      89          if self.py_modules:
      90              self.build_modules()
      91          if self.packages:
      92              self.build_packages()
      93              self.build_package_data()
      94  
      95          self.byte_compile(self.get_outputs(include_bytecode=0))
      96  
      97      def get_data_files(self):
      98          """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
      99          data = []
     100          if not self.packages:
     101              return data
     102          for package in self.packages:
     103              # Locate package source directory
     104              src_dir = self.get_package_dir(package)
     105  
     106              # Compute package build directory
     107              build_dir = os.path.join(*([self.build_lib] + package.split('.')))
     108  
     109              # Length of path to strip from found files
     110              plen = 0
     111              if src_dir:
     112                  plen = len(src_dir)+1
     113  
     114              # Strip directory from globbed filenames
     115              filenames = [
     116                  file[plen:] for file in self.find_data_files(package, src_dir)
     117                  ]
     118              data.append((package, src_dir, build_dir, filenames))
     119          return data
     120  
     121      def find_data_files(self, package, src_dir):
     122          """Return filenames for package's data files in 'src_dir'"""
     123          globs = (self.package_data.get('', [])
     124                   + self.package_data.get(package, []))
     125          files = []
     126          for pattern in globs:
     127              # Each pattern has to be converted to a platform-specific path
     128              filelist = glob.glob(os.path.join(glob.escape(src_dir), convert_path(pattern)))
     129              # Files that match more than one pattern are only added once
     130              files.extend([fn for fn in filelist if fn not in files
     131                  and os.path.isfile(fn)])
     132          return files
     133  
     134      def build_package_data(self):
     135          """Copy data files into build directory"""
     136          lastdir = None
     137          for package, src_dir, build_dir, filenames in self.data_files:
     138              for filename in filenames:
     139                  target = os.path.join(build_dir, filename)
     140                  self.mkpath(os.path.dirname(target))
     141                  self.copy_file(os.path.join(src_dir, filename), target,
     142                                 preserve_mode=False)
     143  
     144      def get_package_dir(self, package):
     145          """Return the directory, relative to the top of the source
     146             distribution, where package 'package' should be found
     147             (at least according to the 'package_dir' option, if any)."""
     148          path = package.split('.')
     149  
     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)
     177  
     178                  if tail:
     179                      return os.path.join(*tail)
     180                  else:
     181                      return ''
     182  
     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)
     196  
     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)
     205  
     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
     209  
     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
     216  
     217      def find_package_modules(self, package, package_dir):
     218          self.check_package(package, package_dir)
     219          module_files = glob.glob(os.path.join(glob.escape(package_dir), "*.py"))
     220          modules = []
     221          setup_script = os.path.abspath(self.distribution.script_name)
     222  
     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
     231  
     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.
     240          """
     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 = {}
     248  
     249          # List of (package, module, filename) tuples to return
     250          modules = []
     251  
     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]
     260  
     261              try:
     262                  (package_dir, checked) = packages[package]
     263              except KeyError:
     264                  package_dir = self.get_package_dir(package)
     265                  checked = 0
     266  
     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))
     272  
     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
     279  
     280              modules.append((package, module_base, module_file))
     281  
     282          return modules
     283  
     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
     299  
     300      def get_source_files(self):
     301          return [module[-1] for module in self.find_all_modules()]
     302  
     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)
     306  
     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(importlib.util.cache_from_source(
     317                          filename, optimization=''))
     318                  if self.optimize > 0:
     319                      outputs.append(importlib.util.cache_from_source(
     320                          filename, optimization=self.optimize))
     321  
     322          outputs += [
     323              os.path.join(build_dir, filename)
     324              for package, src_dir, build_dir, filenames in self.data_files
     325              for filename in filenames
     326              ]
     327  
     328          return outputs
     329  
     330      def build_module(self, module, module_file, package):
     331          if isinstance(package, str):
     332              package = package.split('.')
     333          elif not isinstance(package, (list, tuple)):
     334              raise TypeError(
     335                    "'package' must be a string (dot-separated), list, or tuple")
     336  
     337          # Now put the module source file into the "build" area -- this is
     338          # easy, we just copy it somewhere under self.build_lib (the build
     339          # directory for Python source).
     340          outfile = self.get_module_outfile(self.build_lib, package, module)
     341          dir = os.path.dirname(outfile)
     342          self.mkpath(dir)
     343          return self.copy_file(module_file, outfile, preserve_mode=0)
     344  
     345      def build_modules(self):
     346          modules = self.find_modules()
     347          for (package, module, module_file) in modules:
     348              # Now "build" the module -- ie. copy the source file to
     349              # self.build_lib (the build directory for Python source).
     350              # (Actually, it gets copied to the directory for this package
     351              # under self.build_lib.)
     352              self.build_module(module, module_file, package)
     353  
     354      def build_packages(self):
     355          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)
     367  
     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)
     373  
     374      def byte_compile(self, files):
     375          if sys.dont_write_bytecode:
     376              self.warn('byte-compiling is disabled, skipping.')
     377              return
     378  
     379          from distutils.util import byte_compile
     380          prefix = self.build_lib
     381          if prefix[-1] != os.sep:
     382              prefix = prefix + os.sep
     383  
     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.
     387          if self.compile:
     388              byte_compile(files, optimize=0,
     389                           force=self.force, prefix=prefix, dry_run=self.dry_run)
     390          if self.optimize > 0:
     391              byte_compile(files, optimize=self.optimize,
     392                           force=self.force, prefix=prefix, dry_run=self.dry_run)
     393  
     394  class ESC[4;38;5;81mbuild_py_2to3(ESC[4;38;5;149mbuild_py, ESC[4;38;5;149mMixin2to3):
     395      def run(self):
     396          self.updated_files = []
     397  
     398          # Base class code
     399          if self.py_modules:
     400              self.build_modules()
     401          if self.packages:
     402              self.build_packages()
     403              self.build_package_data()
     404  
     405          # 2to3
     406          self.run_2to3(self.updated_files)
     407  
     408          # Remaining base class code
     409          self.byte_compile(self.get_outputs(include_bytecode=0))
     410  
     411      def build_module(self, module, module_file, package):
     412          res = build_py.build_module(self, module, module_file, package)
     413          if res[1]:
     414              # file was copied
     415              self.updated_files.append(res[0])
     416          return res