Path: blob/main/test/lib/python3.9/site-packages/setuptools/command/bdist_egg.py
4799 views
"""setuptools.command.bdist_egg12Build .egg distributions"""34from distutils.dir_util import remove_tree, mkpath5from distutils import log6from types import CodeType7import sys8import os9import re10import textwrap11import marshal1213from pkg_resources import get_build_platform, Distribution14from setuptools.extension import Library15from setuptools import Command16from .._path import ensure_directory1718from sysconfig import get_path, get_python_version192021def _get_purelib():22return get_path("purelib")232425def strip_module(filename):26if '.' in filename:27filename = os.path.splitext(filename)[0]28if filename.endswith('module'):29filename = filename[:-6]30return filename313233def sorted_walk(dir):34"""Do os.walk in a reproducible way,35independent of indeterministic filesystem readdir order36"""37for base, dirs, files in os.walk(dir):38dirs.sort()39files.sort()40yield base, dirs, files414243def write_stub(resource, pyfile):44_stub_template = textwrap.dedent("""45def __bootstrap__():46global __bootstrap__, __loader__, __file__47import sys, pkg_resources, importlib.util48__file__ = pkg_resources.resource_filename(__name__, %r)49__loader__ = None; del __bootstrap__, __loader__50spec = importlib.util.spec_from_file_location(__name__,__file__)51mod = importlib.util.module_from_spec(spec)52spec.loader.exec_module(mod)53__bootstrap__()54""").lstrip()55with open(pyfile, 'w') as f:56f.write(_stub_template % resource)575859class bdist_egg(Command):60description = "create an \"egg\" distribution"6162user_options = [63('bdist-dir=', 'b',64"temporary directory for creating the distribution"),65('plat-name=', 'p', "platform name to embed in generated filenames "66"(default: %s)" % get_build_platform()),67('exclude-source-files', None,68"remove all .py files from the generated egg"),69('keep-temp', 'k',70"keep the pseudo-installation tree around after " +71"creating the distribution archive"),72('dist-dir=', 'd',73"directory to put final built distributions in"),74('skip-build', None,75"skip rebuilding everything (for testing/debugging)"),76]7778boolean_options = [79'keep-temp', 'skip-build', 'exclude-source-files'80]8182def initialize_options(self):83self.bdist_dir = None84self.plat_name = None85self.keep_temp = 086self.dist_dir = None87self.skip_build = 088self.egg_output = None89self.exclude_source_files = None9091def finalize_options(self):92ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")93self.egg_info = ei_cmd.egg_info9495if self.bdist_dir is None:96bdist_base = self.get_finalized_command('bdist').bdist_base97self.bdist_dir = os.path.join(bdist_base, 'egg')9899if self.plat_name is None:100self.plat_name = get_build_platform()101102self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))103104if self.egg_output is None:105106# Compute filename of the output egg107basename = Distribution(108None, None, ei_cmd.egg_name, ei_cmd.egg_version,109get_python_version(),110self.distribution.has_ext_modules() and self.plat_name111).egg_name()112113self.egg_output = os.path.join(self.dist_dir, basename + '.egg')114115def do_install_data(self):116# Hack for packages that install data to install's --install-lib117self.get_finalized_command('install').install_lib = self.bdist_dir118119site_packages = os.path.normcase(os.path.realpath(_get_purelib()))120old, self.distribution.data_files = self.distribution.data_files, []121122for item in old:123if isinstance(item, tuple) and len(item) == 2:124if os.path.isabs(item[0]):125realpath = os.path.realpath(item[0])126normalized = os.path.normcase(realpath)127if normalized == site_packages or normalized.startswith(128site_packages + os.sep129):130item = realpath[len(site_packages) + 1:], item[1]131# XXX else: raise ???132self.distribution.data_files.append(item)133134try:135log.info("installing package data to %s", self.bdist_dir)136self.call_command('install_data', force=0, root=None)137finally:138self.distribution.data_files = old139140def get_outputs(self):141return [self.egg_output]142143def call_command(self, cmdname, **kw):144"""Invoke reinitialized command `cmdname` with keyword args"""145for dirname in INSTALL_DIRECTORY_ATTRS:146kw.setdefault(dirname, self.bdist_dir)147kw.setdefault('skip_build', self.skip_build)148kw.setdefault('dry_run', self.dry_run)149cmd = self.reinitialize_command(cmdname, **kw)150self.run_command(cmdname)151return cmd152153def run(self): # noqa: C901 # is too complex (14) # FIXME154# Generate metadata first155self.run_command("egg_info")156# We run install_lib before install_data, because some data hacks157# pull their data path from the install_lib command.158log.info("installing library code to %s", self.bdist_dir)159instcmd = self.get_finalized_command('install')160old_root = instcmd.root161instcmd.root = None162if self.distribution.has_c_libraries() and not self.skip_build:163self.run_command('build_clib')164cmd = self.call_command('install_lib', warn_dir=0)165instcmd.root = old_root166167all_outputs, ext_outputs = self.get_ext_outputs()168self.stubs = []169to_compile = []170for (p, ext_name) in enumerate(ext_outputs):171filename, ext = os.path.splitext(ext_name)172pyfile = os.path.join(self.bdist_dir, strip_module(filename) +173'.py')174self.stubs.append(pyfile)175log.info("creating stub loader for %s", ext_name)176if not self.dry_run:177write_stub(os.path.basename(ext_name), pyfile)178to_compile.append(pyfile)179ext_outputs[p] = ext_name.replace(os.sep, '/')180181if to_compile:182cmd.byte_compile(to_compile)183if self.distribution.data_files:184self.do_install_data()185186# Make the EGG-INFO directory187archive_root = self.bdist_dir188egg_info = os.path.join(archive_root, 'EGG-INFO')189self.mkpath(egg_info)190if self.distribution.scripts:191script_dir = os.path.join(egg_info, 'scripts')192log.info("installing scripts to %s", script_dir)193self.call_command('install_scripts', install_dir=script_dir,194no_ep=1)195196self.copy_metadata_to(egg_info)197native_libs = os.path.join(egg_info, "native_libs.txt")198if all_outputs:199log.info("writing %s", native_libs)200if not self.dry_run:201ensure_directory(native_libs)202libs_file = open(native_libs, 'wt')203libs_file.write('\n'.join(all_outputs))204libs_file.write('\n')205libs_file.close()206elif os.path.isfile(native_libs):207log.info("removing %s", native_libs)208if not self.dry_run:209os.unlink(native_libs)210211write_safety_flag(212os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()213)214215if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):216log.warn(217"WARNING: 'depends.txt' will not be used by setuptools 0.6!\n"218"Use the install_requires/extras_require setup() args instead."219)220221if self.exclude_source_files:222self.zap_pyfiles()223224# Make the archive225make_zipfile(self.egg_output, archive_root, verbose=self.verbose,226dry_run=self.dry_run, mode=self.gen_header())227if not self.keep_temp:228remove_tree(self.bdist_dir, dry_run=self.dry_run)229230# Add to 'Distribution.dist_files' so that the "upload" command works231getattr(self.distribution, 'dist_files', []).append(232('bdist_egg', get_python_version(), self.egg_output))233234def zap_pyfiles(self):235log.info("Removing .py files from temporary directory")236for base, dirs, files in walk_egg(self.bdist_dir):237for name in files:238path = os.path.join(base, name)239240if name.endswith('.py'):241log.debug("Deleting %s", path)242os.unlink(path)243244if base.endswith('__pycache__'):245path_old = path246247pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'248m = re.match(pattern, name)249path_new = os.path.join(250base, os.pardir, m.group('name') + '.pyc')251log.info(252"Renaming file from [%s] to [%s]"253% (path_old, path_new))254try:255os.remove(path_new)256except OSError:257pass258os.rename(path_old, path_new)259260def zip_safe(self):261safe = getattr(self.distribution, 'zip_safe', None)262if safe is not None:263return safe264log.warn("zip_safe flag not set; analyzing archive contents...")265return analyze_egg(self.bdist_dir, self.stubs)266267def gen_header(self):268return 'w'269270def copy_metadata_to(self, target_dir):271"Copy metadata (egg info) to the target_dir"272# normalize the path (so that a forward-slash in egg_info will273# match using startswith below)274norm_egg_info = os.path.normpath(self.egg_info)275prefix = os.path.join(norm_egg_info, '')276for path in self.ei_cmd.filelist.files:277if path.startswith(prefix):278target = os.path.join(target_dir, path[len(prefix):])279ensure_directory(target)280self.copy_file(path, target)281282def get_ext_outputs(self):283"""Get a list of relative paths to C extensions in the output distro"""284285all_outputs = []286ext_outputs = []287288paths = {self.bdist_dir: ''}289for base, dirs, files in sorted_walk(self.bdist_dir):290for filename in files:291if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:292all_outputs.append(paths[base] + filename)293for filename in dirs:294paths[os.path.join(base, filename)] = (paths[base] +295filename + '/')296297if self.distribution.has_ext_modules():298build_cmd = self.get_finalized_command('build_ext')299for ext in build_cmd.extensions:300if isinstance(ext, Library):301continue302fullname = build_cmd.get_ext_fullname(ext.name)303filename = build_cmd.get_ext_filename(fullname)304if not os.path.basename(filename).startswith('dl-'):305if os.path.exists(os.path.join(self.bdist_dir, filename)):306ext_outputs.append(filename)307308return all_outputs, ext_outputs309310311NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())312313314def walk_egg(egg_dir):315"""Walk an unpacked egg's contents, skipping the metadata directory"""316walker = sorted_walk(egg_dir)317base, dirs, files = next(walker)318if 'EGG-INFO' in dirs:319dirs.remove('EGG-INFO')320yield base, dirs, files321for bdf in walker:322yield bdf323324325def analyze_egg(egg_dir, stubs):326# check for existing flag in EGG-INFO327for flag, fn in safety_flags.items():328if os.path.exists(os.path.join(egg_dir, 'EGG-INFO', fn)):329return flag330if not can_scan():331return False332safe = True333for base, dirs, files in walk_egg(egg_dir):334for name in files:335if name.endswith('.py') or name.endswith('.pyw'):336continue337elif name.endswith('.pyc') or name.endswith('.pyo'):338# always scan, even if we already know we're not safe339safe = scan_module(egg_dir, base, name, stubs) and safe340return safe341342343def write_safety_flag(egg_dir, safe):344# Write or remove zip safety flag file(s)345for flag, fn in safety_flags.items():346fn = os.path.join(egg_dir, fn)347if os.path.exists(fn):348if safe is None or bool(safe) != flag:349os.unlink(fn)350elif safe is not None and bool(safe) == flag:351f = open(fn, 'wt')352f.write('\n')353f.close()354355356safety_flags = {357True: 'zip-safe',358False: 'not-zip-safe',359}360361362def scan_module(egg_dir, base, name, stubs):363"""Check whether module possibly uses unsafe-for-zipfile stuff"""364365filename = os.path.join(base, name)366if filename[:-1] in stubs:367return True # Extension module368pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')369module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]370if sys.version_info < (3, 7):371skip = 12 # skip magic & date & file size372else:373skip = 16 # skip magic & reserved? & date & file size374f = open(filename, 'rb')375f.read(skip)376code = marshal.load(f)377f.close()378safe = True379symbols = dict.fromkeys(iter_symbols(code))380for bad in ['__file__', '__path__']:381if bad in symbols:382log.warn("%s: module references %s", module, bad)383safe = False384if 'inspect' in symbols:385for bad in [386'getsource', 'getabsfile', 'getsourcefile', 'getfile'387'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',388'getinnerframes', 'getouterframes', 'stack', 'trace'389]:390if bad in symbols:391log.warn("%s: module MAY be using inspect.%s", module, bad)392safe = False393return safe394395396def iter_symbols(code):397"""Yield names and strings used by `code` and its nested code objects"""398for name in code.co_names:399yield name400for const in code.co_consts:401if isinstance(const, str):402yield const403elif isinstance(const, CodeType):404for name in iter_symbols(const):405yield name406407408def can_scan():409if not sys.platform.startswith('java') and sys.platform != 'cli':410# CPython, PyPy, etc.411return True412log.warn("Unable to analyze compiled code on this platform.")413log.warn("Please ask the author to include a 'zip_safe'"414" setting (either True or False) in the package's setup.py")415416417# Attribute names of options for commands that might need to be convinced to418# install to the egg build directory419420INSTALL_DIRECTORY_ATTRS = [421'install_lib', 'install_dir', 'install_data', 'install_base'422]423424425def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,426mode='w'):427"""Create a zip file from all the files under 'base_dir'. The output428zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"429Python module (if available) or the InfoZIP "zip" utility (if installed430and found on the default search path). If neither tool is available,431raises DistutilsExecError. Returns the name of the output zip file.432"""433import zipfile434435mkpath(os.path.dirname(zip_filename), dry_run=dry_run)436log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)437438def visit(z, dirname, names):439for name in names:440path = os.path.normpath(os.path.join(dirname, name))441if os.path.isfile(path):442p = path[len(base_dir) + 1:]443if not dry_run:444z.write(path, p)445log.debug("adding '%s'", p)446447compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED448if not dry_run:449z = zipfile.ZipFile(zip_filename, mode, compression=compression)450for dirname, dirs, files in sorted_walk(base_dir):451visit(z, dirname, files)452z.close()453else:454for dirname, dirs, files in sorted_walk(base_dir):455visit(None, dirname, files)456return zip_filename457458459