python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
setuptools/
_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 DistutilsOptionError, DistutilsFileError
      12  from distutils.util import convert_path
      13  from distutils import log
      14  
      15  
      16  class ESC[4;38;5;81mbuild_py(ESC[4;38;5;149mCommand):
      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.warn("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 distutils.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              )