python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
setuptools/
command/
bdist_egg.py
       1  """setuptools.command.bdist_egg
       2  
       3  Build .egg distributions"""
       4  
       5  from distutils.dir_util import remove_tree, mkpath
       6  from distutils import log
       7  from types import CodeType
       8  import sys
       9  import os
      10  import re
      11  import textwrap
      12  import marshal
      13  
      14  from pkg_resources import get_build_platform, Distribution
      15  from setuptools.extension import Library
      16  from setuptools import Command
      17  from .._path import ensure_directory
      18  
      19  from sysconfig import get_path, get_python_version
      20  
      21  
      22  def _get_purelib():
      23      return get_path("purelib")
      24  
      25  
      26  def strip_module(filename):
      27      if '.' in filename:
      28          filename = os.path.splitext(filename)[0]
      29      if filename.endswith('module'):
      30          filename = filename[:-6]
      31      return filename
      32  
      33  
      34  def sorted_walk(dir):
      35      """Do os.walk in a reproducible way,
      36      independent of indeterministic filesystem readdir order
      37      """
      38      for base, dirs, files in os.walk(dir):
      39          dirs.sort()
      40          files.sort()
      41          yield base, dirs, files
      42  
      43  
      44  def write_stub(resource, pyfile):
      45      _stub_template = textwrap.dedent("""
      46          def __bootstrap__():
      47              global __bootstrap__, __loader__, __file__
      48              import sys, pkg_resources, importlib.util
      49              __file__ = pkg_resources.resource_filename(__name__, %r)
      50              __loader__ = None; del __bootstrap__, __loader__
      51              spec = importlib.util.spec_from_file_location(__name__,__file__)
      52              mod = importlib.util.module_from_spec(spec)
      53              spec.loader.exec_module(mod)
      54          __bootstrap__()
      55          """).lstrip()
      56      with open(pyfile, 'w') as f:
      57          f.write(_stub_template % resource)
      58  
      59  
      60  class ESC[4;38;5;81mbdist_egg(ESC[4;38;5;149mCommand):
      61      description = "create an \"egg\" distribution"
      62  
      63      user_options = [
      64          ('bdist-dir=', 'b',
      65           "temporary directory for creating the distribution"),
      66          ('plat-name=', 'p', "platform name to embed in generated filenames "
      67                              "(default: %s)" % get_build_platform()),
      68          ('exclude-source-files', None,
      69           "remove all .py files from the generated egg"),
      70          ('keep-temp', 'k',
      71           "keep the pseudo-installation tree around after " +
      72           "creating the distribution archive"),
      73          ('dist-dir=', 'd',
      74           "directory to put final built distributions in"),
      75          ('skip-build', None,
      76           "skip rebuilding everything (for testing/debugging)"),
      77      ]
      78  
      79      boolean_options = [
      80          'keep-temp', 'skip-build', 'exclude-source-files'
      81      ]
      82  
      83      def initialize_options(self):
      84          self.bdist_dir = None
      85          self.plat_name = None
      86          self.keep_temp = 0
      87          self.dist_dir = None
      88          self.skip_build = 0
      89          self.egg_output = None
      90          self.exclude_source_files = None
      91  
      92      def finalize_options(self):
      93          ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")
      94          self.egg_info = ei_cmd.egg_info
      95  
      96          if self.bdist_dir is None:
      97              bdist_base = self.get_finalized_command('bdist').bdist_base
      98              self.bdist_dir = os.path.join(bdist_base, 'egg')
      99  
     100          if self.plat_name is None:
     101              self.plat_name = get_build_platform()
     102  
     103          self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
     104  
     105          if self.egg_output is None:
     106  
     107              # Compute filename of the output egg
     108              basename = Distribution(
     109                  None, None, ei_cmd.egg_name, ei_cmd.egg_version,
     110                  get_python_version(),
     111                  self.distribution.has_ext_modules() and self.plat_name
     112              ).egg_name()
     113  
     114              self.egg_output = os.path.join(self.dist_dir, basename + '.egg')
     115  
     116      def do_install_data(self):
     117          # Hack for packages that install data to install's --install-lib
     118          self.get_finalized_command('install').install_lib = self.bdist_dir
     119  
     120          site_packages = os.path.normcase(os.path.realpath(_get_purelib()))
     121          old, self.distribution.data_files = self.distribution.data_files, []
     122  
     123          for item in old:
     124              if isinstance(item, tuple) and len(item) == 2:
     125                  if os.path.isabs(item[0]):
     126                      realpath = os.path.realpath(item[0])
     127                      normalized = os.path.normcase(realpath)
     128                      if normalized == site_packages or normalized.startswith(
     129                          site_packages + os.sep
     130                      ):
     131                          item = realpath[len(site_packages) + 1:], item[1]
     132                          # XXX else: raise ???
     133              self.distribution.data_files.append(item)
     134  
     135          try:
     136              log.info("installing package data to %s", self.bdist_dir)
     137              self.call_command('install_data', force=0, root=None)
     138          finally:
     139              self.distribution.data_files = old
     140  
     141      def get_outputs(self):
     142          return [self.egg_output]
     143  
     144      def call_command(self, cmdname, **kw):
     145          """Invoke reinitialized command `cmdname` with keyword args"""
     146          for dirname in INSTALL_DIRECTORY_ATTRS:
     147              kw.setdefault(dirname, self.bdist_dir)
     148          kw.setdefault('skip_build', self.skip_build)
     149          kw.setdefault('dry_run', self.dry_run)
     150          cmd = self.reinitialize_command(cmdname, **kw)
     151          self.run_command(cmdname)
     152          return cmd
     153  
     154      def run(self):  # noqa: C901  # is too complex (14)  # FIXME
     155          # Generate metadata first
     156          self.run_command("egg_info")
     157          # We run install_lib before install_data, because some data hacks
     158          # pull their data path from the install_lib command.
     159          log.info("installing library code to %s", self.bdist_dir)
     160          instcmd = self.get_finalized_command('install')
     161          old_root = instcmd.root
     162          instcmd.root = None
     163          if self.distribution.has_c_libraries() and not self.skip_build:
     164              self.run_command('build_clib')
     165          cmd = self.call_command('install_lib', warn_dir=0)
     166          instcmd.root = old_root
     167  
     168          all_outputs, ext_outputs = self.get_ext_outputs()
     169          self.stubs = []
     170          to_compile = []
     171          for (p, ext_name) in enumerate(ext_outputs):
     172              filename, ext = os.path.splitext(ext_name)
     173              pyfile = os.path.join(self.bdist_dir, strip_module(filename) +
     174                                    '.py')
     175              self.stubs.append(pyfile)
     176              log.info("creating stub loader for %s", ext_name)
     177              if not self.dry_run:
     178                  write_stub(os.path.basename(ext_name), pyfile)
     179              to_compile.append(pyfile)
     180              ext_outputs[p] = ext_name.replace(os.sep, '/')
     181  
     182          if to_compile:
     183              cmd.byte_compile(to_compile)
     184          if self.distribution.data_files:
     185              self.do_install_data()
     186  
     187          # Make the EGG-INFO directory
     188          archive_root = self.bdist_dir
     189          egg_info = os.path.join(archive_root, 'EGG-INFO')
     190          self.mkpath(egg_info)
     191          if self.distribution.scripts:
     192              script_dir = os.path.join(egg_info, 'scripts')
     193              log.info("installing scripts to %s", script_dir)
     194              self.call_command('install_scripts', install_dir=script_dir,
     195                                no_ep=1)
     196  
     197          self.copy_metadata_to(egg_info)
     198          native_libs = os.path.join(egg_info, "native_libs.txt")
     199          if all_outputs:
     200              log.info("writing %s", native_libs)
     201              if not self.dry_run:
     202                  ensure_directory(native_libs)
     203                  libs_file = open(native_libs, 'wt')
     204                  libs_file.write('\n'.join(all_outputs))
     205                  libs_file.write('\n')
     206                  libs_file.close()
     207          elif os.path.isfile(native_libs):
     208              log.info("removing %s", native_libs)
     209              if not self.dry_run:
     210                  os.unlink(native_libs)
     211  
     212          write_safety_flag(
     213              os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
     214          )
     215  
     216          if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):
     217              log.warn(
     218                  "WARNING: 'depends.txt' will not be used by setuptools 0.6!\n"
     219                  "Use the install_requires/extras_require setup() args instead."
     220              )
     221  
     222          if self.exclude_source_files:
     223              self.zap_pyfiles()
     224  
     225          # Make the archive
     226          make_zipfile(self.egg_output, archive_root, verbose=self.verbose,
     227                       dry_run=self.dry_run, mode=self.gen_header())
     228          if not self.keep_temp:
     229              remove_tree(self.bdist_dir, dry_run=self.dry_run)
     230  
     231          # Add to 'Distribution.dist_files' so that the "upload" command works
     232          getattr(self.distribution, 'dist_files', []).append(
     233              ('bdist_egg', get_python_version(), self.egg_output))
     234  
     235      def zap_pyfiles(self):
     236          log.info("Removing .py files from temporary directory")
     237          for base, dirs, files in walk_egg(self.bdist_dir):
     238              for name in files:
     239                  path = os.path.join(base, name)
     240  
     241                  if name.endswith('.py'):
     242                      log.debug("Deleting %s", path)
     243                      os.unlink(path)
     244  
     245                  if base.endswith('__pycache__'):
     246                      path_old = path
     247  
     248                      pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'
     249                      m = re.match(pattern, name)
     250                      path_new = os.path.join(
     251                          base, os.pardir, m.group('name') + '.pyc')
     252                      log.info(
     253                          "Renaming file from [%s] to [%s]"
     254                          % (path_old, path_new))
     255                      try:
     256                          os.remove(path_new)
     257                      except OSError:
     258                          pass
     259                      os.rename(path_old, path_new)
     260  
     261      def zip_safe(self):
     262          safe = getattr(self.distribution, 'zip_safe', None)
     263          if safe is not None:
     264              return safe
     265          log.warn("zip_safe flag not set; analyzing archive contents...")
     266          return analyze_egg(self.bdist_dir, self.stubs)
     267  
     268      def gen_header(self):
     269          return 'w'
     270  
     271      def copy_metadata_to(self, target_dir):
     272          "Copy metadata (egg info) to the target_dir"
     273          # normalize the path (so that a forward-slash in egg_info will
     274          # match using startswith below)
     275          norm_egg_info = os.path.normpath(self.egg_info)
     276          prefix = os.path.join(norm_egg_info, '')
     277          for path in self.ei_cmd.filelist.files:
     278              if path.startswith(prefix):
     279                  target = os.path.join(target_dir, path[len(prefix):])
     280                  ensure_directory(target)
     281                  self.copy_file(path, target)
     282  
     283      def get_ext_outputs(self):
     284          """Get a list of relative paths to C extensions in the output distro"""
     285  
     286          all_outputs = []
     287          ext_outputs = []
     288  
     289          paths = {self.bdist_dir: ''}
     290          for base, dirs, files in sorted_walk(self.bdist_dir):
     291              for filename in files:
     292                  if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
     293                      all_outputs.append(paths[base] + filename)
     294              for filename in dirs:
     295                  paths[os.path.join(base, filename)] = (paths[base] +
     296                                                         filename + '/')
     297  
     298          if self.distribution.has_ext_modules():
     299              build_cmd = self.get_finalized_command('build_ext')
     300              for ext in build_cmd.extensions:
     301                  if isinstance(ext, Library):
     302                      continue
     303                  fullname = build_cmd.get_ext_fullname(ext.name)
     304                  filename = build_cmd.get_ext_filename(fullname)
     305                  if not os.path.basename(filename).startswith('dl-'):
     306                      if os.path.exists(os.path.join(self.bdist_dir, filename)):
     307                          ext_outputs.append(filename)
     308  
     309          return all_outputs, ext_outputs
     310  
     311  
     312  NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
     313  
     314  
     315  def walk_egg(egg_dir):
     316      """Walk an unpacked egg's contents, skipping the metadata directory"""
     317      walker = sorted_walk(egg_dir)
     318      base, dirs, files = next(walker)
     319      if 'EGG-INFO' in dirs:
     320          dirs.remove('EGG-INFO')
     321      yield base, dirs, files
     322      for bdf in walker:
     323          yield bdf
     324  
     325  
     326  def analyze_egg(egg_dir, stubs):
     327      # check for existing flag in EGG-INFO
     328      for flag, fn in safety_flags.items():
     329          if os.path.exists(os.path.join(egg_dir, 'EGG-INFO', fn)):
     330              return flag
     331      if not can_scan():
     332          return False
     333      safe = True
     334      for base, dirs, files in walk_egg(egg_dir):
     335          for name in files:
     336              if name.endswith('.py') or name.endswith('.pyw'):
     337                  continue
     338              elif name.endswith('.pyc') or name.endswith('.pyo'):
     339                  # always scan, even if we already know we're not safe
     340                  safe = scan_module(egg_dir, base, name, stubs) and safe
     341      return safe
     342  
     343  
     344  def write_safety_flag(egg_dir, safe):
     345      # Write or remove zip safety flag file(s)
     346      for flag, fn in safety_flags.items():
     347          fn = os.path.join(egg_dir, fn)
     348          if os.path.exists(fn):
     349              if safe is None or bool(safe) != flag:
     350                  os.unlink(fn)
     351          elif safe is not None and bool(safe) == flag:
     352              f = open(fn, 'wt')
     353              f.write('\n')
     354              f.close()
     355  
     356  
     357  safety_flags = {
     358      True: 'zip-safe',
     359      False: 'not-zip-safe',
     360  }
     361  
     362  
     363  def scan_module(egg_dir, base, name, stubs):
     364      """Check whether module possibly uses unsafe-for-zipfile stuff"""
     365  
     366      filename = os.path.join(base, name)
     367      if filename[:-1] in stubs:
     368          return True  # Extension module
     369      pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')
     370      module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]
     371      if sys.version_info < (3, 7):
     372          skip = 12  # skip magic & date & file size
     373      else:
     374          skip = 16  # skip magic & reserved? & date & file size
     375      f = open(filename, 'rb')
     376      f.read(skip)
     377      code = marshal.load(f)
     378      f.close()
     379      safe = True
     380      symbols = dict.fromkeys(iter_symbols(code))
     381      for bad in ['__file__', '__path__']:
     382          if bad in symbols:
     383              log.warn("%s: module references %s", module, bad)
     384              safe = False
     385      if 'inspect' in symbols:
     386          for bad in [
     387              'getsource', 'getabsfile', 'getsourcefile', 'getfile'
     388              'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',
     389              'getinnerframes', 'getouterframes', 'stack', 'trace'
     390          ]:
     391              if bad in symbols:
     392                  log.warn("%s: module MAY be using inspect.%s", module, bad)
     393                  safe = False
     394      return safe
     395  
     396  
     397  def iter_symbols(code):
     398      """Yield names and strings used by `code` and its nested code objects"""
     399      for name in code.co_names:
     400          yield name
     401      for const in code.co_consts:
     402          if isinstance(const, str):
     403              yield const
     404          elif isinstance(const, CodeType):
     405              for name in iter_symbols(const):
     406                  yield name
     407  
     408  
     409  def can_scan():
     410      if not sys.platform.startswith('java') and sys.platform != 'cli':
     411          # CPython, PyPy, etc.
     412          return True
     413      log.warn("Unable to analyze compiled code on this platform.")
     414      log.warn("Please ask the author to include a 'zip_safe'"
     415               " setting (either True or False) in the package's setup.py")
     416  
     417  
     418  # Attribute names of options for commands that might need to be convinced to
     419  # install to the egg build directory
     420  
     421  INSTALL_DIRECTORY_ATTRS = [
     422      'install_lib', 'install_dir', 'install_data', 'install_base'
     423  ]
     424  
     425  
     426  def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
     427                   mode='w'):
     428      """Create a zip file from all the files under 'base_dir'.  The output
     429      zip file will be named 'base_dir' + ".zip".  Uses either the "zipfile"
     430      Python module (if available) or the InfoZIP "zip" utility (if installed
     431      and found on the default search path).  If neither tool is available,
     432      raises DistutilsExecError.  Returns the name of the output zip file.
     433      """
     434      import zipfile
     435  
     436      mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
     437      log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
     438  
     439      def visit(z, dirname, names):
     440          for name in names:
     441              path = os.path.normpath(os.path.join(dirname, name))
     442              if os.path.isfile(path):
     443                  p = path[len(base_dir) + 1:]
     444                  if not dry_run:
     445                      z.write(path, p)
     446                  log.debug("adding '%s'", p)
     447  
     448      compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
     449      if not dry_run:
     450          z = zipfile.ZipFile(zip_filename, mode, compression=compression)
     451          for dirname, dirs, files in sorted_walk(base_dir):
     452              visit(z, dirname, files)
     453          z.close()
     454      else:
     455          for dirname, dirs, files in sorted_walk(base_dir):
     456              visit(None, dirname, files)
     457      return zip_filename