123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- """setuptools.command.bdist_egg
- Build .egg distributions"""
- from distutils.dir_util import remove_tree, mkpath
- from distutils import log
- from types import CodeType
- import sys
- import os
- import re
- import textwrap
- import marshal
- from pkg_resources import get_build_platform, Distribution, ensure_directory
- from setuptools.extension import Library
- from setuptools import Command
- from sysconfig import get_path, get_python_version
- def _get_purelib():
- return get_path("purelib")
- def strip_module(filename):
- if '.' in filename:
- filename = os.path.splitext(filename)[0]
- if filename.endswith('module'):
- filename = filename[:-6]
- return filename
- def sorted_walk(dir):
- """Do os.walk in a reproducible way,
- independent of indeterministic filesystem readdir order
- """
- for base, dirs, files in os.walk(dir):
- dirs.sort()
- files.sort()
- yield base, dirs, files
- def write_stub(resource, pyfile):
- _stub_template = textwrap.dedent("""
- def __bootstrap__():
- global __bootstrap__, __loader__, __file__
- import sys, pkg_resources, importlib.util
- __file__ = pkg_resources.resource_filename(__name__, %r)
- __loader__ = None; del __bootstrap__, __loader__
- spec = importlib.util.spec_from_file_location(__name__,__file__)
- mod = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(mod)
- __bootstrap__()
- """).lstrip()
- with open(pyfile, 'w') as f:
- f.write(_stub_template % resource)
- class bdist_egg(Command):
- description = "create an \"egg\" distribution"
- user_options = [
- ('bdist-dir=', 'b',
- "temporary directory for creating the distribution"),
- ('plat-name=', 'p', "platform name to embed in generated filenames "
- "(default: %s)" % get_build_platform()),
- ('exclude-source-files', None,
- "remove all .py files from the generated egg"),
- ('keep-temp', 'k',
- "keep the pseudo-installation tree around after " +
- "creating the distribution archive"),
- ('dist-dir=', 'd',
- "directory to put final built distributions in"),
- ('skip-build', None,
- "skip rebuilding everything (for testing/debugging)"),
- ]
- boolean_options = [
- 'keep-temp', 'skip-build', 'exclude-source-files'
- ]
- def initialize_options(self):
- self.bdist_dir = None
- self.plat_name = None
- self.keep_temp = 0
- self.dist_dir = None
- self.skip_build = 0
- self.egg_output = None
- self.exclude_source_files = None
- def finalize_options(self):
- ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")
- self.egg_info = ei_cmd.egg_info
- if self.bdist_dir is None:
- bdist_base = self.get_finalized_command('bdist').bdist_base
- self.bdist_dir = os.path.join(bdist_base, 'egg')
- if self.plat_name is None:
- self.plat_name = get_build_platform()
- self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
- if self.egg_output is None:
- # Compute filename of the output egg
- basename = Distribution(
- None, None, ei_cmd.egg_name, ei_cmd.egg_version,
- get_python_version(),
- self.distribution.has_ext_modules() and self.plat_name
- ).egg_name()
- self.egg_output = os.path.join(self.dist_dir, basename + '.egg')
- def do_install_data(self):
- # Hack for packages that install data to install's --install-lib
- self.get_finalized_command('install').install_lib = self.bdist_dir
- site_packages = os.path.normcase(os.path.realpath(_get_purelib()))
- old, self.distribution.data_files = self.distribution.data_files, []
- for item in old:
- if isinstance(item, tuple) and len(item) == 2:
- if os.path.isabs(item[0]):
- realpath = os.path.realpath(item[0])
- normalized = os.path.normcase(realpath)
- if normalized == site_packages or normalized.startswith(
- site_packages + os.sep
- ):
- item = realpath[len(site_packages) + 1:], item[1]
- # XXX else: raise ???
- self.distribution.data_files.append(item)
- try:
- log.info("installing package data to %s", self.bdist_dir)
- self.call_command('install_data', force=0, root=None)
- finally:
- self.distribution.data_files = old
- def get_outputs(self):
- return [self.egg_output]
- def call_command(self, cmdname, **kw):
- """Invoke reinitialized command `cmdname` with keyword args"""
- for dirname in INSTALL_DIRECTORY_ATTRS:
- kw.setdefault(dirname, self.bdist_dir)
- kw.setdefault('skip_build', self.skip_build)
- kw.setdefault('dry_run', self.dry_run)
- cmd = self.reinitialize_command(cmdname, **kw)
- self.run_command(cmdname)
- return cmd
- def run(self): # noqa: C901 # is too complex (14) # FIXME
- # Generate metadata first
- self.run_command("egg_info")
- # We run install_lib before install_data, because some data hacks
- # pull their data path from the install_lib command.
- log.info("installing library code to %s", self.bdist_dir)
- instcmd = self.get_finalized_command('install')
- old_root = instcmd.root
- instcmd.root = None
- if self.distribution.has_c_libraries() and not self.skip_build:
- self.run_command('build_clib')
- cmd = self.call_command('install_lib', warn_dir=0)
- instcmd.root = old_root
- all_outputs, ext_outputs = self.get_ext_outputs()
- self.stubs = []
- to_compile = []
- for (p, ext_name) in enumerate(ext_outputs):
- filename, ext = os.path.splitext(ext_name)
- pyfile = os.path.join(self.bdist_dir, strip_module(filename) +
- '.py')
- self.stubs.append(pyfile)
- log.info("creating stub loader for %s", ext_name)
- if not self.dry_run:
- write_stub(os.path.basename(ext_name), pyfile)
- to_compile.append(pyfile)
- ext_outputs[p] = ext_name.replace(os.sep, '/')
- if to_compile:
- cmd.byte_compile(to_compile)
- if self.distribution.data_files:
- self.do_install_data()
- # Make the EGG-INFO directory
- archive_root = self.bdist_dir
- egg_info = os.path.join(archive_root, 'EGG-INFO')
- self.mkpath(egg_info)
- if self.distribution.scripts:
- script_dir = os.path.join(egg_info, 'scripts')
- log.info("installing scripts to %s", script_dir)
- self.call_command('install_scripts', install_dir=script_dir,
- no_ep=1)
- self.copy_metadata_to(egg_info)
- native_libs = os.path.join(egg_info, "native_libs.txt")
- if all_outputs:
- log.info("writing %s", native_libs)
- if not self.dry_run:
- ensure_directory(native_libs)
- libs_file = open(native_libs, 'wt')
- libs_file.write('\n'.join(all_outputs))
- libs_file.write('\n')
- libs_file.close()
- elif os.path.isfile(native_libs):
- log.info("removing %s", native_libs)
- if not self.dry_run:
- os.unlink(native_libs)
- write_safety_flag(
- os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
- )
- if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):
- log.warn(
- "WARNING: 'depends.txt' will not be used by setuptools 0.6!\n"
- "Use the install_requires/extras_require setup() args instead."
- )
- if self.exclude_source_files:
- self.zap_pyfiles()
- # Make the archive
- make_zipfile(self.egg_output, archive_root, verbose=self.verbose,
- dry_run=self.dry_run, mode=self.gen_header())
- if not self.keep_temp:
- remove_tree(self.bdist_dir, dry_run=self.dry_run)
- # Add to 'Distribution.dist_files' so that the "upload" command works
- getattr(self.distribution, 'dist_files', []).append(
- ('bdist_egg', get_python_version(), self.egg_output))
- def zap_pyfiles(self):
- log.info("Removing .py files from temporary directory")
- for base, dirs, files in walk_egg(self.bdist_dir):
- for name in files:
- path = os.path.join(base, name)
- if name.endswith('.py'):
- log.debug("Deleting %s", path)
- os.unlink(path)
- if base.endswith('__pycache__'):
- path_old = path
- pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'
- m = re.match(pattern, name)
- path_new = os.path.join(
- base, os.pardir, m.group('name') + '.pyc')
- log.info(
- "Renaming file from [%s] to [%s]"
- % (path_old, path_new))
- try:
- os.remove(path_new)
- except OSError:
- pass
- os.rename(path_old, path_new)
- def zip_safe(self):
- safe = getattr(self.distribution, 'zip_safe', None)
- if safe is not None:
- return safe
- log.warn("zip_safe flag not set; analyzing archive contents...")
- return analyze_egg(self.bdist_dir, self.stubs)
- def gen_header(self):
- return 'w'
- def copy_metadata_to(self, target_dir):
- "Copy metadata (egg info) to the target_dir"
- # normalize the path (so that a forward-slash in egg_info will
- # match using startswith below)
- norm_egg_info = os.path.normpath(self.egg_info)
- prefix = os.path.join(norm_egg_info, '')
- for path in self.ei_cmd.filelist.files:
- if path.startswith(prefix):
- target = os.path.join(target_dir, path[len(prefix):])
- ensure_directory(target)
- self.copy_file(path, target)
- def get_ext_outputs(self):
- """Get a list of relative paths to C extensions in the output distro"""
- all_outputs = []
- ext_outputs = []
- paths = {self.bdist_dir: ''}
- for base, dirs, files in sorted_walk(self.bdist_dir):
- for filename in files:
- if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
- all_outputs.append(paths[base] + filename)
- for filename in dirs:
- paths[os.path.join(base, filename)] = (paths[base] +
- filename + '/')
- if self.distribution.has_ext_modules():
- build_cmd = self.get_finalized_command('build_ext')
- for ext in build_cmd.extensions:
- if isinstance(ext, Library):
- continue
- fullname = build_cmd.get_ext_fullname(ext.name)
- filename = build_cmd.get_ext_filename(fullname)
- if not os.path.basename(filename).startswith('dl-'):
- if os.path.exists(os.path.join(self.bdist_dir, filename)):
- ext_outputs.append(filename)
- return all_outputs, ext_outputs
- NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
- def walk_egg(egg_dir):
- """Walk an unpacked egg's contents, skipping the metadata directory"""
- walker = sorted_walk(egg_dir)
- base, dirs, files = next(walker)
- if 'EGG-INFO' in dirs:
- dirs.remove('EGG-INFO')
- yield base, dirs, files
- for bdf in walker:
- yield bdf
- def analyze_egg(egg_dir, stubs):
- # check for existing flag in EGG-INFO
- for flag, fn in safety_flags.items():
- if os.path.exists(os.path.join(egg_dir, 'EGG-INFO', fn)):
- return flag
- if not can_scan():
- return False
- safe = True
- for base, dirs, files in walk_egg(egg_dir):
- for name in files:
- if name.endswith('.py') or name.endswith('.pyw'):
- continue
- elif name.endswith('.pyc') or name.endswith('.pyo'):
- # always scan, even if we already know we're not safe
- safe = scan_module(egg_dir, base, name, stubs) and safe
- return safe
- def write_safety_flag(egg_dir, safe):
- # Write or remove zip safety flag file(s)
- for flag, fn in safety_flags.items():
- fn = os.path.join(egg_dir, fn)
- if os.path.exists(fn):
- if safe is None or bool(safe) != flag:
- os.unlink(fn)
- elif safe is not None and bool(safe) == flag:
- f = open(fn, 'wt')
- f.write('\n')
- f.close()
- safety_flags = {
- True: 'zip-safe',
- False: 'not-zip-safe',
- }
- def scan_module(egg_dir, base, name, stubs):
- """Check whether module possibly uses unsafe-for-zipfile stuff"""
- filename = os.path.join(base, name)
- if filename[:-1] in stubs:
- return True # Extension module
- pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')
- module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]
- if sys.version_info < (3, 7):
- skip = 12 # skip magic & date & file size
- else:
- skip = 16 # skip magic & reserved? & date & file size
- f = open(filename, 'rb')
- f.read(skip)
- code = marshal.load(f)
- f.close()
- safe = True
- symbols = dict.fromkeys(iter_symbols(code))
- for bad in ['__file__', '__path__']:
- if bad in symbols:
- log.warn("%s: module references %s", module, bad)
- safe = False
- if 'inspect' in symbols:
- for bad in [
- 'getsource', 'getabsfile', 'getsourcefile', 'getfile'
- 'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',
- 'getinnerframes', 'getouterframes', 'stack', 'trace'
- ]:
- if bad in symbols:
- log.warn("%s: module MAY be using inspect.%s", module, bad)
- safe = False
- return safe
- def iter_symbols(code):
- """Yield names and strings used by `code` and its nested code objects"""
- for name in code.co_names:
- yield name
- for const in code.co_consts:
- if isinstance(const, str):
- yield const
- elif isinstance(const, CodeType):
- for name in iter_symbols(const):
- yield name
- def can_scan():
- if not sys.platform.startswith('java') and sys.platform != 'cli':
- # CPython, PyPy, etc.
- return True
- log.warn("Unable to analyze compiled code on this platform.")
- log.warn("Please ask the author to include a 'zip_safe'"
- " setting (either True or False) in the package's setup.py")
- # Attribute names of options for commands that might need to be convinced to
- # install to the egg build directory
- INSTALL_DIRECTORY_ATTRS = [
- 'install_lib', 'install_dir', 'install_data', 'install_base'
- ]
- def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
- mode='w'):
- """Create a zip file from all the files under 'base_dir'. The output
- zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"
- Python module (if available) or the InfoZIP "zip" utility (if installed
- and found on the default search path). If neither tool is available,
- raises DistutilsExecError. Returns the name of the output zip file.
- """
- import zipfile
- mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
- log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
- def visit(z, dirname, names):
- for name in names:
- path = os.path.normpath(os.path.join(dirname, name))
- if os.path.isfile(path):
- p = path[len(base_dir) + 1:]
- if not dry_run:
- z.write(path, p)
- log.debug("adding '%s'", p)
- compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
- if not dry_run:
- z = zipfile.ZipFile(zip_filename, mode, compression=compression)
- for dirname, dirs, files in sorted_walk(base_dir):
- visit(z, dirname, files)
- z.close()
- else:
- for dirname, dirs, files in sorted_walk(base_dir):
- visit(None, dirname, files)
- return zip_filename
|