Spaces:
Running
Running
""" | |
Easy Install | |
------------ | |
A tool for doing automatic download/extract/build of distutils-based Python | |
packages. For detailed documentation, see the accompanying EasyInstall.txt | |
file, or visit the `EasyInstall home page`__. | |
__ https://setuptools.pypa.io/en/latest/deprecated/easy_install.html | |
""" | |
from glob import glob | |
from distutils.util import get_platform | |
from distutils.util import convert_path, subst_vars | |
from distutils.errors import ( | |
DistutilsArgError, DistutilsOptionError, | |
DistutilsError, DistutilsPlatformError, | |
) | |
from distutils import log, dir_util | |
from distutils.command.build_scripts import first_line_re | |
from distutils.spawn import find_executable | |
from distutils.command import install | |
import sys | |
import os | |
import zipimport | |
import shutil | |
import tempfile | |
import zipfile | |
import re | |
import stat | |
import random | |
import textwrap | |
import warnings | |
import site | |
import struct | |
import contextlib | |
import subprocess | |
import shlex | |
import io | |
import configparser | |
import sysconfig | |
from sysconfig import get_path | |
from setuptools import Command | |
from setuptools.sandbox import run_setup | |
from setuptools.command import setopt | |
from setuptools.archive_util import unpack_archive | |
from setuptools.package_index import ( | |
PackageIndex, parse_requirement_arg, URL_SCHEME, | |
) | |
from setuptools.command import bdist_egg, egg_info | |
from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning | |
from setuptools.wheel import Wheel | |
from pkg_resources import ( | |
normalize_path, resource_string, | |
get_distribution, find_distributions, Environment, Requirement, | |
Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound, | |
VersionConflict, DEVELOP_DIST, | |
) | |
import pkg_resources | |
from .. import py312compat | |
from .._path import ensure_directory | |
from ..extern.jaraco.text import yield_lines | |
# Turn on PEP440Warnings | |
warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) | |
__all__ = [ | |
'easy_install', 'PthDistributions', 'extract_wininst_cfg', | |
'get_exe_prefixes', | |
] | |
def is_64bit(): | |
return struct.calcsize("P") == 8 | |
def _to_bytes(s): | |
return s.encode('utf8') | |
def isascii(s): | |
try: | |
s.encode('ascii') | |
return True | |
except UnicodeError: | |
return False | |
def _one_liner(text): | |
return textwrap.dedent(text).strip().replace('\n', '; ') | |
class easy_install(Command): | |
"""Manage a download/build/install process""" | |
description = "Find/get/install Python packages" | |
command_consumes_arguments = True | |
user_options = [ | |
('prefix=', None, "installation prefix"), | |
("zip-ok", "z", "install package as a zipfile"), | |
("multi-version", "m", "make apps have to require() a version"), | |
("upgrade", "U", "force upgrade (searches PyPI for latest versions)"), | |
("install-dir=", "d", "install package to DIR"), | |
("script-dir=", "s", "install scripts to DIR"), | |
("exclude-scripts", "x", "Don't install scripts"), | |
("always-copy", "a", "Copy all needed packages to install dir"), | |
("index-url=", "i", "base URL of Python Package Index"), | |
("find-links=", "f", "additional URL(s) to search for packages"), | |
("build-directory=", "b", | |
"download/extract/build in DIR; keep the results"), | |
('optimize=', 'O', | |
"also compile with optimization: -O1 for \"python -O\", " | |
"-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), | |
('record=', None, | |
"filename in which to record list of installed files"), | |
('always-unzip', 'Z', "don't install as a zipfile, no matter what"), | |
('site-dirs=', 'S', "list of directories where .pth files work"), | |
('editable', 'e', "Install specified packages in editable form"), | |
('no-deps', 'N', "don't install dependencies"), | |
('allow-hosts=', 'H', "pattern(s) that hostnames must match"), | |
('local-snapshots-ok', 'l', | |
"allow building eggs from local checkouts"), | |
('version', None, "print version information and exit"), | |
('no-find-links', None, | |
"Don't load find-links defined in packages being installed"), | |
('user', None, "install in user site-package '%s'" % site.USER_SITE) | |
] | |
boolean_options = [ | |
'zip-ok', 'multi-version', 'exclude-scripts', 'upgrade', 'always-copy', | |
'editable', | |
'no-deps', 'local-snapshots-ok', 'version', | |
'user' | |
] | |
negative_opt = {'always-unzip': 'zip-ok'} | |
create_index = PackageIndex | |
def initialize_options(self): | |
EasyInstallDeprecationWarning.emit() | |
# the --user option seems to be an opt-in one, | |
# so the default should be False. | |
self.user = 0 | |
self.zip_ok = self.local_snapshots_ok = None | |
self.install_dir = self.script_dir = self.exclude_scripts = None | |
self.index_url = None | |
self.find_links = None | |
self.build_directory = None | |
self.args = None | |
self.optimize = self.record = None | |
self.upgrade = self.always_copy = self.multi_version = None | |
self.editable = self.no_deps = self.allow_hosts = None | |
self.root = self.prefix = self.no_report = None | |
self.version = None | |
self.install_purelib = None # for pure module distributions | |
self.install_platlib = None # non-pure (dists w/ extensions) | |
self.install_headers = None # for C/C++ headers | |
self.install_lib = None # set to either purelib or platlib | |
self.install_scripts = None | |
self.install_data = None | |
self.install_base = None | |
self.install_platbase = None | |
self.install_userbase = site.USER_BASE | |
self.install_usersite = site.USER_SITE | |
self.no_find_links = None | |
# Options not specifiable via command line | |
self.package_index = None | |
self.pth_file = self.always_copy_from = None | |
self.site_dirs = None | |
self.installed_projects = {} | |
# Always read easy_install options, even if we are subclassed, or have | |
# an independent instance created. This ensures that defaults will | |
# always come from the standard configuration file(s)' "easy_install" | |
# section, even if this is a "develop" or "install" command, or some | |
# other embedding. | |
self._dry_run = None | |
self.verbose = self.distribution.verbose | |
self.distribution._set_command_options( | |
self, self.distribution.get_option_dict('easy_install') | |
) | |
def delete_blockers(self, blockers): | |
extant_blockers = ( | |
filename for filename in blockers | |
if os.path.exists(filename) or os.path.islink(filename) | |
) | |
list(map(self._delete_path, extant_blockers)) | |
def _delete_path(self, path): | |
log.info("Deleting %s", path) | |
if self.dry_run: | |
return | |
is_tree = os.path.isdir(path) and not os.path.islink(path) | |
remover = _rmtree if is_tree else os.unlink | |
remover(path) | |
def _render_version(): | |
""" | |
Render the Setuptools version and installation details, then exit. | |
""" | |
ver = '{}.{}'.format(*sys.version_info) | |
dist = get_distribution('setuptools') | |
tmpl = 'setuptools {dist.version} from {dist.location} (Python {ver})' | |
print(tmpl.format(**locals())) | |
raise SystemExit() | |
def finalize_options(self): # noqa: C901 # is too complex (25) # FIXME | |
self.version and self._render_version() | |
py_version = sys.version.split()[0] | |
self.config_vars = dict(sysconfig.get_config_vars()) | |
self.config_vars.update({ | |
'dist_name': self.distribution.get_name(), | |
'dist_version': self.distribution.get_version(), | |
'dist_fullname': self.distribution.get_fullname(), | |
'py_version': py_version, | |
'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}', | |
'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}', | |
'sys_prefix': self.config_vars['prefix'], | |
'sys_exec_prefix': self.config_vars['exec_prefix'], | |
# Only python 3.2+ has abiflags | |
'abiflags': getattr(sys, 'abiflags', ''), | |
'platlibdir': getattr(sys, 'platlibdir', 'lib'), | |
}) | |
with contextlib.suppress(AttributeError): | |
# only for distutils outside stdlib | |
self.config_vars.update({ | |
'implementation_lower': install._get_implementation().lower(), | |
'implementation': install._get_implementation(), | |
}) | |
# pypa/distutils#113 Python 3.9 compat | |
self.config_vars.setdefault( | |
'py_version_nodot_plat', | |
getattr(sys, 'windir', '').replace('.', ''), | |
) | |
self.config_vars['userbase'] = self.install_userbase | |
self.config_vars['usersite'] = self.install_usersite | |
if self.user and not site.ENABLE_USER_SITE: | |
log.warn("WARNING: The user site-packages directory is disabled.") | |
self._fix_install_dir_for_user_site() | |
self.expand_basedirs() | |
self.expand_dirs() | |
self._expand( | |
'install_dir', 'script_dir', 'build_directory', | |
'site_dirs', | |
) | |
# If a non-default installation directory was specified, default the | |
# script directory to match it. | |
if self.script_dir is None: | |
self.script_dir = self.install_dir | |
if self.no_find_links is None: | |
self.no_find_links = False | |
# Let install_dir get set by install_lib command, which in turn | |
# gets its info from the install command, and takes into account | |
# --prefix and --home and all that other crud. | |
self.set_undefined_options( | |
'install_lib', ('install_dir', 'install_dir') | |
) | |
# Likewise, set default script_dir from 'install_scripts.install_dir' | |
self.set_undefined_options( | |
'install_scripts', ('install_dir', 'script_dir') | |
) | |
if self.user and self.install_purelib: | |
self.install_dir = self.install_purelib | |
self.script_dir = self.install_scripts | |
# default --record from the install command | |
self.set_undefined_options('install', ('record', 'record')) | |
self.all_site_dirs = get_site_dirs() | |
self.all_site_dirs.extend(self._process_site_dirs(self.site_dirs)) | |
if not self.editable: | |
self.check_site_dir() | |
default_index = os.getenv("__EASYINSTALL_INDEX", "https://pypi.org/simple/") | |
# ^ Private API for testing purposes only | |
self.index_url = self.index_url or default_index | |
self.shadow_path = self.all_site_dirs[:] | |
for path_item in self.install_dir, normalize_path(self.script_dir): | |
if path_item not in self.shadow_path: | |
self.shadow_path.insert(0, path_item) | |
if self.allow_hosts is not None: | |
hosts = [s.strip() for s in self.allow_hosts.split(',')] | |
else: | |
hosts = ['*'] | |
if self.package_index is None: | |
self.package_index = self.create_index( | |
self.index_url, search_path=self.shadow_path, hosts=hosts, | |
) | |
self.local_index = Environment(self.shadow_path + sys.path) | |
if self.find_links is not None: | |
if isinstance(self.find_links, str): | |
self.find_links = self.find_links.split() | |
else: | |
self.find_links = [] | |
if self.local_snapshots_ok: | |
self.package_index.scan_egg_links(self.shadow_path + sys.path) | |
if not self.no_find_links: | |
self.package_index.add_find_links(self.find_links) | |
self.set_undefined_options('install_lib', ('optimize', 'optimize')) | |
self.optimize = self._validate_optimize(self.optimize) | |
if self.editable and not self.build_directory: | |
raise DistutilsArgError( | |
"Must specify a build directory (-b) when using --editable" | |
) | |
if not self.args: | |
raise DistutilsArgError( | |
"No urls, filenames, or requirements specified (see --help)") | |
self.outputs = [] | |
def _process_site_dirs(site_dirs): | |
if site_dirs is None: | |
return | |
normpath = map(normalize_path, sys.path) | |
site_dirs = [ | |
os.path.expanduser(s.strip()) for s in | |
site_dirs.split(',') | |
] | |
for d in site_dirs: | |
if not os.path.isdir(d): | |
log.warn("%s (in --site-dirs) does not exist", d) | |
elif normalize_path(d) not in normpath: | |
raise DistutilsOptionError( | |
d + " (in --site-dirs) is not on sys.path" | |
) | |
else: | |
yield normalize_path(d) | |
def _validate_optimize(value): | |
try: | |
value = int(value) | |
if value not in range(3): | |
raise ValueError | |
except ValueError as e: | |
raise DistutilsOptionError( | |
"--optimize must be 0, 1, or 2" | |
) from e | |
return value | |
def _fix_install_dir_for_user_site(self): | |
""" | |
Fix the install_dir if "--user" was used. | |
""" | |
if not self.user: | |
return | |
self.create_home_path() | |
if self.install_userbase is None: | |
msg = "User base directory is not specified" | |
raise DistutilsPlatformError(msg) | |
self.install_base = self.install_platbase = self.install_userbase | |
scheme_name = f'{os.name}_user' | |
self.select_scheme(scheme_name) | |
def _expand_attrs(self, attrs): | |
for attr in attrs: | |
val = getattr(self, attr) | |
if val is not None: | |
if os.name == 'posix' or os.name == 'nt': | |
val = os.path.expanduser(val) | |
val = subst_vars(val, self.config_vars) | |
setattr(self, attr, val) | |
def expand_basedirs(self): | |
"""Calls `os.path.expanduser` on install_base, install_platbase and | |
root.""" | |
self._expand_attrs(['install_base', 'install_platbase', 'root']) | |
def expand_dirs(self): | |
"""Calls `os.path.expanduser` on install dirs.""" | |
dirs = [ | |
'install_purelib', | |
'install_platlib', | |
'install_lib', | |
'install_headers', | |
'install_scripts', | |
'install_data', | |
] | |
self._expand_attrs(dirs) | |
def run(self, show_deprecation=True): | |
if show_deprecation: | |
self.announce( | |
"WARNING: The easy_install command is deprecated " | |
"and will be removed in a future version.", | |
log.WARN, | |
) | |
if self.verbose != self.distribution.verbose: | |
log.set_verbosity(self.verbose) | |
try: | |
for spec in self.args: | |
self.easy_install(spec, not self.no_deps) | |
if self.record: | |
outputs = self.outputs | |
if self.root: # strip any package prefix | |
root_len = len(self.root) | |
for counter in range(len(outputs)): | |
outputs[counter] = outputs[counter][root_len:] | |
from distutils import file_util | |
self.execute( | |
file_util.write_file, (self.record, outputs), | |
"writing list of installed files to '%s'" % | |
self.record | |
) | |
self.warn_deprecated_options() | |
finally: | |
log.set_verbosity(self.distribution.verbose) | |
def pseudo_tempname(self): | |
"""Return a pseudo-tempname base in the install directory. | |
This code is intentionally naive; if a malicious party can write to | |
the target directory you're already in deep doodoo. | |
""" | |
try: | |
pid = os.getpid() | |
except Exception: | |
pid = random.randint(0, sys.maxsize) | |
return os.path.join(self.install_dir, "test-easy-install-%s" % pid) | |
def warn_deprecated_options(self): | |
pass | |
def check_site_dir(self): # noqa: C901 # is too complex (12) # FIXME | |
"""Verify that self.install_dir is .pth-capable dir, if needed""" | |
instdir = normalize_path(self.install_dir) | |
pth_file = os.path.join(instdir, 'easy-install.pth') | |
if not os.path.exists(instdir): | |
try: | |
os.makedirs(instdir) | |
except (OSError, IOError): | |
self.cant_write_to_target() | |
# Is it a configured, PYTHONPATH, implicit, or explicit site dir? | |
is_site_dir = instdir in self.all_site_dirs | |
if not is_site_dir and not self.multi_version: | |
# No? Then directly test whether it does .pth file processing | |
is_site_dir = self.check_pth_processing() | |
else: | |
# make sure we can write to target dir | |
testfile = self.pseudo_tempname() + '.write-test' | |
test_exists = os.path.exists(testfile) | |
try: | |
if test_exists: | |
os.unlink(testfile) | |
open(testfile, 'w').close() | |
os.unlink(testfile) | |
except (OSError, IOError): | |
self.cant_write_to_target() | |
if not is_site_dir and not self.multi_version: | |
# Can't install non-multi to non-site dir with easy_install | |
pythonpath = os.environ.get('PYTHONPATH', '') | |
log.warn(self.__no_default_msg, self.install_dir, pythonpath) | |
if is_site_dir: | |
if self.pth_file is None: | |
self.pth_file = PthDistributions(pth_file, self.all_site_dirs) | |
else: | |
self.pth_file = None | |
if self.multi_version and not os.path.exists(pth_file): | |
self.pth_file = None # don't create a .pth file | |
self.install_dir = instdir | |
__cant_write_msg = textwrap.dedent(""" | |
can't create or remove files in install directory | |
The following error occurred while trying to add or remove files in the | |
installation directory: | |
%s | |
The installation directory you specified (via --install-dir, --prefix, or | |
the distutils default setting) was: | |
%s | |
""").lstrip() # noqa | |
__not_exists_id = textwrap.dedent(""" | |
This directory does not currently exist. Please create it and try again, or | |
choose a different installation directory (using the -d or --install-dir | |
option). | |
""").lstrip() # noqa | |
__access_msg = textwrap.dedent(""" | |
Perhaps your account does not have write access to this directory? If the | |
installation directory is a system-owned directory, you may need to sign in | |
as the administrator or "root" account. If you do not have administrative | |
access to this machine, you may wish to choose a different installation | |
directory, preferably one that is listed in your PYTHONPATH environment | |
variable. | |
For information on other options, you may wish to consult the | |
documentation at: | |
https://setuptools.pypa.io/en/latest/deprecated/easy_install.html | |
Please make the appropriate changes for your system and try again. | |
""").lstrip() # noqa | |
def cant_write_to_target(self): | |
msg = self.__cant_write_msg % (sys.exc_info()[1], self.install_dir,) | |
if not os.path.exists(self.install_dir): | |
msg += '\n' + self.__not_exists_id | |
else: | |
msg += '\n' + self.__access_msg | |
raise DistutilsError(msg) | |
def check_pth_processing(self): | |
"""Empirically verify whether .pth files are supported in inst. dir""" | |
instdir = self.install_dir | |
log.info("Checking .pth file support in %s", instdir) | |
pth_file = self.pseudo_tempname() + ".pth" | |
ok_file = pth_file + '.ok' | |
ok_exists = os.path.exists(ok_file) | |
tmpl = _one_liner(""" | |
import os | |
f = open({ok_file!r}, 'w') | |
f.write('OK') | |
f.close() | |
""") + '\n' | |
try: | |
if ok_exists: | |
os.unlink(ok_file) | |
dirname = os.path.dirname(ok_file) | |
os.makedirs(dirname, exist_ok=True) | |
f = open(pth_file, 'w') | |
except (OSError, IOError): | |
self.cant_write_to_target() | |
else: | |
try: | |
f.write(tmpl.format(**locals())) | |
f.close() | |
f = None | |
executable = sys.executable | |
if os.name == 'nt': | |
dirname, basename = os.path.split(executable) | |
alt = os.path.join(dirname, 'pythonw.exe') | |
use_alt = ( | |
basename.lower() == 'python.exe' and | |
os.path.exists(alt) | |
) | |
if use_alt: | |
# use pythonw.exe to avoid opening a console window | |
executable = alt | |
from distutils.spawn import spawn | |
spawn([executable, '-E', '-c', 'pass'], 0) | |
if os.path.exists(ok_file): | |
log.info( | |
"TEST PASSED: %s appears to support .pth files", | |
instdir | |
) | |
return True | |
finally: | |
if f: | |
f.close() | |
if os.path.exists(ok_file): | |
os.unlink(ok_file) | |
if os.path.exists(pth_file): | |
os.unlink(pth_file) | |
if not self.multi_version: | |
log.warn("TEST FAILED: %s does NOT support .pth files", instdir) | |
return False | |
def install_egg_scripts(self, dist): | |
"""Write all the scripts for `dist`, unless scripts are excluded""" | |
if not self.exclude_scripts and dist.metadata_isdir('scripts'): | |
for script_name in dist.metadata_listdir('scripts'): | |
if dist.metadata_isdir('scripts/' + script_name): | |
# The "script" is a directory, likely a Python 3 | |
# __pycache__ directory, so skip it. | |
continue | |
self.install_script( | |
dist, script_name, | |
dist.get_metadata('scripts/' + script_name) | |
) | |
self.install_wrapper_scripts(dist) | |
def add_output(self, path): | |
if os.path.isdir(path): | |
for base, dirs, files in os.walk(path): | |
for filename in files: | |
self.outputs.append(os.path.join(base, filename)) | |
else: | |
self.outputs.append(path) | |
def not_editable(self, spec): | |
if self.editable: | |
raise DistutilsArgError( | |
"Invalid argument %r: you can't use filenames or URLs " | |
"with --editable (except via the --find-links option)." | |
% (spec,) | |
) | |
def check_editable(self, spec): | |
if not self.editable: | |
return | |
if os.path.exists(os.path.join(self.build_directory, spec.key)): | |
raise DistutilsArgError( | |
"%r already exists in %s; can't do a checkout there" % | |
(spec.key, self.build_directory) | |
) | |
def _tmpdir(self): | |
tmpdir = tempfile.mkdtemp(prefix=u"easy_install-") | |
try: | |
# cast to str as workaround for #709 and #710 and #712 | |
yield str(tmpdir) | |
finally: | |
os.path.exists(tmpdir) and _rmtree(tmpdir) | |
def easy_install(self, spec, deps=False): | |
with self._tmpdir() as tmpdir: | |
if not isinstance(spec, Requirement): | |
if URL_SCHEME(spec): | |
# It's a url, download it to tmpdir and process | |
self.not_editable(spec) | |
dl = self.package_index.download(spec, tmpdir) | |
return self.install_item(None, dl, tmpdir, deps, True) | |
elif os.path.exists(spec): | |
# Existing file or directory, just process it directly | |
self.not_editable(spec) | |
return self.install_item(None, spec, tmpdir, deps, True) | |
else: | |
spec = parse_requirement_arg(spec) | |
self.check_editable(spec) | |
dist = self.package_index.fetch_distribution( | |
spec, tmpdir, self.upgrade, self.editable, | |
not self.always_copy, self.local_index | |
) | |
if dist is None: | |
msg = "Could not find suitable distribution for %r" % spec | |
if self.always_copy: | |
msg += " (--always-copy skips system and development eggs)" | |
raise DistutilsError(msg) | |
elif dist.precedence == DEVELOP_DIST: | |
# .egg-info dists don't need installing, just process deps | |
self.process_distribution(spec, dist, deps, "Using") | |
return dist | |
else: | |
return self.install_item(spec, dist.location, tmpdir, deps) | |
def install_item(self, spec, download, tmpdir, deps, install_needed=False): | |
# Installation is also needed if file in tmpdir or is not an egg | |
install_needed = install_needed or self.always_copy | |
install_needed = install_needed or os.path.dirname(download) == tmpdir | |
install_needed = install_needed or not download.endswith('.egg') | |
install_needed = install_needed or ( | |
self.always_copy_from is not None and | |
os.path.dirname(normalize_path(download)) == | |
normalize_path(self.always_copy_from) | |
) | |
if spec and not install_needed: | |
# at this point, we know it's a local .egg, we just don't know if | |
# it's already installed. | |
for dist in self.local_index[spec.project_name]: | |
if dist.location == download: | |
break | |
else: | |
install_needed = True # it's not in the local index | |
log.info("Processing %s", os.path.basename(download)) | |
if install_needed: | |
dists = self.install_eggs(spec, download, tmpdir) | |
for dist in dists: | |
self.process_distribution(spec, dist, deps) | |
else: | |
dists = [self.egg_distribution(download)] | |
self.process_distribution(spec, dists[0], deps, "Using") | |
if spec is not None: | |
for dist in dists: | |
if dist in spec: | |
return dist | |
def select_scheme(self, name): | |
try: | |
install._select_scheme(self, name) | |
except AttributeError: | |
# stdlib distutils | |
install.install.select_scheme(self, name.replace('posix', 'unix')) | |
# FIXME: 'easy_install.process_distribution' is too complex (12) | |
def process_distribution( # noqa: C901 | |
self, requirement, dist, deps=True, *info, | |
): | |
self.update_pth(dist) | |
self.package_index.add(dist) | |
if dist in self.local_index[dist.key]: | |
self.local_index.remove(dist) | |
self.local_index.add(dist) | |
self.install_egg_scripts(dist) | |
self.installed_projects[dist.key] = dist | |
log.info(self.installation_report(requirement, dist, *info)) | |
if (dist.has_metadata('dependency_links.txt') and | |
not self.no_find_links): | |
self.package_index.add_find_links( | |
dist.get_metadata_lines('dependency_links.txt') | |
) | |
if not deps and not self.always_copy: | |
return | |
elif requirement is not None and dist.key != requirement.key: | |
log.warn("Skipping dependencies for %s", dist) | |
return # XXX this is not the distribution we were looking for | |
elif requirement is None or dist not in requirement: | |
# if we wound up with a different version, resolve what we've got | |
distreq = dist.as_requirement() | |
requirement = Requirement(str(distreq)) | |
log.info("Processing dependencies for %s", requirement) | |
try: | |
distros = WorkingSet([]).resolve( | |
[requirement], self.local_index, self.easy_install | |
) | |
except DistributionNotFound as e: | |
raise DistutilsError(str(e)) from e | |
except VersionConflict as e: | |
raise DistutilsError(e.report()) from e | |
if self.always_copy or self.always_copy_from: | |
# Force all the relevant distros to be copied or activated | |
for dist in distros: | |
if dist.key not in self.installed_projects: | |
self.easy_install(dist.as_requirement()) | |
log.info("Finished processing dependencies for %s", requirement) | |
def should_unzip(self, dist): | |
if self.zip_ok is not None: | |
return not self.zip_ok | |
if dist.has_metadata('not-zip-safe'): | |
return True | |
if not dist.has_metadata('zip-safe'): | |
return True | |
return False | |
def maybe_move(self, spec, dist_filename, setup_base): | |
dst = os.path.join(self.build_directory, spec.key) | |
if os.path.exists(dst): | |
msg = ( | |
"%r already exists in %s; build directory %s will not be kept" | |
) | |
log.warn(msg, spec.key, self.build_directory, setup_base) | |
return setup_base | |
if os.path.isdir(dist_filename): | |
setup_base = dist_filename | |
else: | |
if os.path.dirname(dist_filename) == setup_base: | |
os.unlink(dist_filename) # get it out of the tmp dir | |
contents = os.listdir(setup_base) | |
if len(contents) == 1: | |
dist_filename = os.path.join(setup_base, contents[0]) | |
if os.path.isdir(dist_filename): | |
# if the only thing there is a directory, move it instead | |
setup_base = dist_filename | |
ensure_directory(dst) | |
shutil.move(setup_base, dst) | |
return dst | |
def install_wrapper_scripts(self, dist): | |
if self.exclude_scripts: | |
return | |
for args in ScriptWriter.best().get_args(dist): | |
self.write_script(*args) | |
def install_script(self, dist, script_name, script_text, dev_path=None): | |
"""Generate a legacy script wrapper and install it""" | |
spec = str(dist.as_requirement()) | |
is_script = is_python_script(script_text, script_name) | |
if is_script: | |
body = self._load_template(dev_path) % locals() | |
script_text = ScriptWriter.get_header(script_text) + body | |
self.write_script(script_name, _to_bytes(script_text), 'b') | |
def _load_template(dev_path): | |
""" | |
There are a couple of template scripts in the package. This | |
function loads one of them and prepares it for use. | |
""" | |
# See https://github.com/pypa/setuptools/issues/134 for info | |
# on script file naming and downstream issues with SVR4 | |
name = 'script.tmpl' | |
if dev_path: | |
name = name.replace('.tmpl', ' (dev).tmpl') | |
raw_bytes = resource_string('setuptools', name) | |
return raw_bytes.decode('utf-8') | |
def write_script(self, script_name, contents, mode="t", blockers=()): | |
"""Write an executable file to the scripts directory""" | |
self.delete_blockers( # clean up old .py/.pyw w/o a script | |
[os.path.join(self.script_dir, x) for x in blockers] | |
) | |
log.info("Installing %s script to %s", script_name, self.script_dir) | |
target = os.path.join(self.script_dir, script_name) | |
self.add_output(target) | |
if self.dry_run: | |
return | |
mask = current_umask() | |
ensure_directory(target) | |
if os.path.exists(target): | |
os.unlink(target) | |
with open(target, "w" + mode) as f: | |
f.write(contents) | |
chmod(target, 0o777 - mask) | |
def install_eggs(self, spec, dist_filename, tmpdir): | |
# .egg dirs or files are already built, so just return them | |
installer_map = { | |
'.egg': self.install_egg, | |
'.exe': self.install_exe, | |
'.whl': self.install_wheel, | |
} | |
try: | |
install_dist = installer_map[ | |
dist_filename.lower()[-4:] | |
] | |
except KeyError: | |
pass | |
else: | |
return [install_dist(dist_filename, tmpdir)] | |
# Anything else, try to extract and build | |
setup_base = tmpdir | |
if os.path.isfile(dist_filename) and not dist_filename.endswith('.py'): | |
unpack_archive(dist_filename, tmpdir, self.unpack_progress) | |
elif os.path.isdir(dist_filename): | |
setup_base = os.path.abspath(dist_filename) | |
if (setup_base.startswith(tmpdir) # something we downloaded | |
and self.build_directory and spec is not None): | |
setup_base = self.maybe_move(spec, dist_filename, setup_base) | |
# Find the setup.py file | |
setup_script = os.path.join(setup_base, 'setup.py') | |
if not os.path.exists(setup_script): | |
setups = glob(os.path.join(setup_base, '*', 'setup.py')) | |
if not setups: | |
raise DistutilsError( | |
"Couldn't find a setup script in %s" % | |
os.path.abspath(dist_filename) | |
) | |
if len(setups) > 1: | |
raise DistutilsError( | |
"Multiple setup scripts in %s" % | |
os.path.abspath(dist_filename) | |
) | |
setup_script = setups[0] | |
# Now run it, and return the result | |
if self.editable: | |
log.info(self.report_editable(spec, setup_script)) | |
return [] | |
else: | |
return self.build_and_install(setup_script, setup_base) | |
def egg_distribution(self, egg_path): | |
if os.path.isdir(egg_path): | |
metadata = PathMetadata(egg_path, os.path.join(egg_path, | |
'EGG-INFO')) | |
else: | |
metadata = EggMetadata(zipimport.zipimporter(egg_path)) | |
return Distribution.from_filename(egg_path, metadata=metadata) | |
# FIXME: 'easy_install.install_egg' is too complex (11) | |
def install_egg(self, egg_path, tmpdir): # noqa: C901 | |
destination = os.path.join( | |
self.install_dir, | |
os.path.basename(egg_path), | |
) | |
destination = os.path.abspath(destination) | |
if not self.dry_run: | |
ensure_directory(destination) | |
dist = self.egg_distribution(egg_path) | |
if not ( | |
os.path.exists(destination) and os.path.samefile(egg_path, destination) | |
): | |
if os.path.isdir(destination) and not os.path.islink(destination): | |
dir_util.remove_tree(destination, dry_run=self.dry_run) | |
elif os.path.exists(destination): | |
self.execute( | |
os.unlink, | |
(destination,), | |
"Removing " + destination, | |
) | |
try: | |
new_dist_is_zipped = False | |
if os.path.isdir(egg_path): | |
if egg_path.startswith(tmpdir): | |
f, m = shutil.move, "Moving" | |
else: | |
f, m = shutil.copytree, "Copying" | |
elif self.should_unzip(dist): | |
self.mkpath(destination) | |
f, m = self.unpack_and_compile, "Extracting" | |
else: | |
new_dist_is_zipped = True | |
if egg_path.startswith(tmpdir): | |
f, m = shutil.move, "Moving" | |
else: | |
f, m = shutil.copy2, "Copying" | |
self.execute( | |
f, | |
(egg_path, destination), | |
(m + " %s to %s") % ( | |
os.path.basename(egg_path), | |
os.path.dirname(destination) | |
), | |
) | |
update_dist_caches( | |
destination, | |
fix_zipimporter_caches=new_dist_is_zipped, | |
) | |
except Exception: | |
update_dist_caches(destination, fix_zipimporter_caches=False) | |
raise | |
self.add_output(destination) | |
return self.egg_distribution(destination) | |
def install_exe(self, dist_filename, tmpdir): | |
# See if it's valid, get data | |
cfg = extract_wininst_cfg(dist_filename) | |
if cfg is None: | |
raise DistutilsError( | |
"%s is not a valid distutils Windows .exe" % dist_filename | |
) | |
# Create a dummy distribution object until we build the real distro | |
dist = Distribution( | |
None, | |
project_name=cfg.get('metadata', 'name'), | |
version=cfg.get('metadata', 'version'), platform=get_platform(), | |
) | |
# Convert the .exe to an unpacked egg | |
egg_path = os.path.join(tmpdir, dist.egg_name() + '.egg') | |
dist.location = egg_path | |
egg_tmp = egg_path + '.tmp' | |
_egg_info = os.path.join(egg_tmp, 'EGG-INFO') | |
pkg_inf = os.path.join(_egg_info, 'PKG-INFO') | |
ensure_directory(pkg_inf) # make sure EGG-INFO dir exists | |
dist._provider = PathMetadata(egg_tmp, _egg_info) # XXX | |
self.exe_to_egg(dist_filename, egg_tmp) | |
# Write EGG-INFO/PKG-INFO | |
if not os.path.exists(pkg_inf): | |
f = open(pkg_inf, 'w') | |
f.write('Metadata-Version: 1.0\n') | |
for k, v in cfg.items('metadata'): | |
if k != 'target_version': | |
f.write('%s: %s\n' % (k.replace('_', '-').title(), v)) | |
f.close() | |
script_dir = os.path.join(_egg_info, 'scripts') | |
# delete entry-point scripts to avoid duping | |
self.delete_blockers([ | |
os.path.join(script_dir, args[0]) | |
for args in ScriptWriter.get_args(dist) | |
]) | |
# Build .egg file from tmpdir | |
bdist_egg.make_zipfile( | |
egg_path, egg_tmp, verbose=self.verbose, dry_run=self.dry_run, | |
) | |
# install the .egg | |
return self.install_egg(egg_path, tmpdir) | |
# FIXME: 'easy_install.exe_to_egg' is too complex (12) | |
def exe_to_egg(self, dist_filename, egg_tmp): # noqa: C901 | |
"""Extract a bdist_wininst to the directories an egg would use""" | |
# Check for .pth file and set up prefix translations | |
prefixes = get_exe_prefixes(dist_filename) | |
to_compile = [] | |
native_libs = [] | |
top_level = {} | |
def process(src, dst): | |
s = src.lower() | |
for old, new in prefixes: | |
if s.startswith(old): | |
src = new + src[len(old):] | |
parts = src.split('/') | |
dst = os.path.join(egg_tmp, *parts) | |
dl = dst.lower() | |
if dl.endswith('.pyd') or dl.endswith('.dll'): | |
parts[-1] = bdist_egg.strip_module(parts[-1]) | |
top_level[os.path.splitext(parts[0])[0]] = 1 | |
native_libs.append(src) | |
elif dl.endswith('.py') and old != 'SCRIPTS/': | |
top_level[os.path.splitext(parts[0])[0]] = 1 | |
to_compile.append(dst) | |
return dst | |
if not src.endswith('.pth'): | |
log.warn("WARNING: can't process %s", src) | |
return None | |
# extract, tracking .pyd/.dll->native_libs and .py -> to_compile | |
unpack_archive(dist_filename, egg_tmp, process) | |
stubs = [] | |
for res in native_libs: | |
if res.lower().endswith('.pyd'): # create stubs for .pyd's | |
parts = res.split('/') | |
resource = parts[-1] | |
parts[-1] = bdist_egg.strip_module(parts[-1]) + '.py' | |
pyfile = os.path.join(egg_tmp, *parts) | |
to_compile.append(pyfile) | |
stubs.append(pyfile) | |
bdist_egg.write_stub(resource, pyfile) | |
self.byte_compile(to_compile) # compile .py's | |
bdist_egg.write_safety_flag( | |
os.path.join(egg_tmp, 'EGG-INFO'), | |
bdist_egg.analyze_egg(egg_tmp, stubs)) # write zip-safety flag | |
for name in 'top_level', 'native_libs': | |
if locals()[name]: | |
txt = os.path.join(egg_tmp, 'EGG-INFO', name + '.txt') | |
if not os.path.exists(txt): | |
f = open(txt, 'w') | |
f.write('\n'.join(locals()[name]) + '\n') | |
f.close() | |
def install_wheel(self, wheel_path, tmpdir): | |
wheel = Wheel(wheel_path) | |
assert wheel.is_compatible() | |
destination = os.path.join(self.install_dir, wheel.egg_name()) | |
destination = os.path.abspath(destination) | |
if not self.dry_run: | |
ensure_directory(destination) | |
if os.path.isdir(destination) and not os.path.islink(destination): | |
dir_util.remove_tree(destination, dry_run=self.dry_run) | |
elif os.path.exists(destination): | |
self.execute( | |
os.unlink, | |
(destination,), | |
"Removing " + destination, | |
) | |
try: | |
self.execute( | |
wheel.install_as_egg, | |
(destination,), | |
("Installing %s to %s") % ( | |
os.path.basename(wheel_path), | |
os.path.dirname(destination) | |
), | |
) | |
finally: | |
update_dist_caches(destination, fix_zipimporter_caches=False) | |
self.add_output(destination) | |
return self.egg_distribution(destination) | |
__mv_warning = textwrap.dedent(""" | |
Because this distribution was installed --multi-version, before you can | |
import modules from this package in an application, you will need to | |
'import pkg_resources' and then use a 'require()' call similar to one of | |
these examples, in order to select the desired version: | |
pkg_resources.require("%(name)s") # latest installed version | |
pkg_resources.require("%(name)s==%(version)s") # this exact version | |
pkg_resources.require("%(name)s>=%(version)s") # this version or higher | |
""").lstrip() # noqa | |
__id_warning = textwrap.dedent(""" | |
Note also that the installation directory must be on sys.path at runtime for | |
this to work. (e.g. by being the application's script directory, by being on | |
PYTHONPATH, or by being added to sys.path by your code.) | |
""") # noqa | |
def installation_report(self, req, dist, what="Installed"): | |
"""Helpful installation message for display to package users""" | |
msg = "\n%(what)s %(eggloc)s%(extras)s" | |
if self.multi_version and not self.no_report: | |
msg += '\n' + self.__mv_warning | |
if self.install_dir not in map(normalize_path, sys.path): | |
msg += '\n' + self.__id_warning | |
eggloc = dist.location | |
name = dist.project_name | |
version = dist.version | |
extras = '' # TODO: self.report_extras(req, dist) | |
return msg % locals() | |
__editable_msg = textwrap.dedent(""" | |
Extracted editable version of %(spec)s to %(dirname)s | |
If it uses setuptools in its setup script, you can activate it in | |
"development" mode by going to that directory and running:: | |
%(python)s setup.py develop | |
See the setuptools documentation for the "develop" command for more info. | |
""").lstrip() # noqa | |
def report_editable(self, spec, setup_script): | |
dirname = os.path.dirname(setup_script) | |
python = sys.executable | |
return '\n' + self.__editable_msg % locals() | |
def run_setup(self, setup_script, setup_base, args): | |
sys.modules.setdefault('distutils.command.bdist_egg', bdist_egg) | |
sys.modules.setdefault('distutils.command.egg_info', egg_info) | |
args = list(args) | |
if self.verbose > 2: | |
v = 'v' * (self.verbose - 1) | |
args.insert(0, '-' + v) | |
elif self.verbose < 2: | |
args.insert(0, '-q') | |
if self.dry_run: | |
args.insert(0, '-n') | |
log.info( | |
"Running %s %s", setup_script[len(setup_base) + 1:], ' '.join(args) | |
) | |
try: | |
run_setup(setup_script, args) | |
except SystemExit as v: | |
raise DistutilsError( | |
"Setup script exited with %s" % (v.args[0],) | |
) from v | |
def build_and_install(self, setup_script, setup_base): | |
args = ['bdist_egg', '--dist-dir'] | |
dist_dir = tempfile.mkdtemp( | |
prefix='egg-dist-tmp-', dir=os.path.dirname(setup_script) | |
) | |
try: | |
self._set_fetcher_options(os.path.dirname(setup_script)) | |
args.append(dist_dir) | |
self.run_setup(setup_script, setup_base, args) | |
all_eggs = Environment([dist_dir]) | |
eggs = [] | |
for key in all_eggs: | |
for dist in all_eggs[key]: | |
eggs.append(self.install_egg(dist.location, setup_base)) | |
if not eggs and not self.dry_run: | |
log.warn("No eggs found in %s (setup script problem?)", | |
dist_dir) | |
return eggs | |
finally: | |
_rmtree(dist_dir) | |
log.set_verbosity(self.verbose) # restore our log verbosity | |
def _set_fetcher_options(self, base): | |
""" | |
When easy_install is about to run bdist_egg on a source dist, that | |
source dist might have 'setup_requires' directives, requiring | |
additional fetching. Ensure the fetcher options given to easy_install | |
are available to that command as well. | |
""" | |
# find the fetch options from easy_install and write them out | |
# to the setup.cfg file. | |
ei_opts = self.distribution.get_option_dict('easy_install').copy() | |
fetch_directives = ( | |
'find_links', 'site_dirs', 'index_url', 'optimize', 'allow_hosts', | |
) | |
fetch_options = {} | |
for key, val in ei_opts.items(): | |
if key not in fetch_directives: | |
continue | |
fetch_options[key] = val[1] | |
# create a settings dictionary suitable for `edit_config` | |
settings = dict(easy_install=fetch_options) | |
cfg_filename = os.path.join(base, 'setup.cfg') | |
setopt.edit_config(cfg_filename, settings) | |
def update_pth(self, dist): # noqa: C901 # is too complex (11) # FIXME | |
if self.pth_file is None: | |
return | |
for d in self.pth_file[dist.key]: # drop old entries | |
if not self.multi_version and d.location == dist.location: | |
continue | |
log.info("Removing %s from easy-install.pth file", d) | |
self.pth_file.remove(d) | |
if d.location in self.shadow_path: | |
self.shadow_path.remove(d.location) | |
if not self.multi_version: | |
if dist.location in self.pth_file.paths: | |
log.info( | |
"%s is already the active version in easy-install.pth", | |
dist, | |
) | |
else: | |
log.info("Adding %s to easy-install.pth file", dist) | |
self.pth_file.add(dist) # add new entry | |
if dist.location not in self.shadow_path: | |
self.shadow_path.append(dist.location) | |
if self.dry_run: | |
return | |
self.pth_file.save() | |
if dist.key != 'setuptools': | |
return | |
# Ensure that setuptools itself never becomes unavailable! | |
# XXX should this check for latest version? | |
filename = os.path.join(self.install_dir, 'setuptools.pth') | |
if os.path.islink(filename): | |
os.unlink(filename) | |
with open(filename, 'wt') as f: | |
f.write(self.pth_file.make_relative(dist.location) + '\n') | |
def unpack_progress(self, src, dst): | |
# Progress filter for unpacking | |
log.debug("Unpacking %s to %s", src, dst) | |
return dst # only unpack-and-compile skips files for dry run | |
def unpack_and_compile(self, egg_path, destination): | |
to_compile = [] | |
to_chmod = [] | |
def pf(src, dst): | |
if dst.endswith('.py') and not src.startswith('EGG-INFO/'): | |
to_compile.append(dst) | |
elif dst.endswith('.dll') or dst.endswith('.so'): | |
to_chmod.append(dst) | |
self.unpack_progress(src, dst) | |
return not self.dry_run and dst or None | |
unpack_archive(egg_path, destination, pf) | |
self.byte_compile(to_compile) | |
if not self.dry_run: | |
for f in to_chmod: | |
mode = ((os.stat(f)[stat.ST_MODE]) | 0o555) & 0o7755 | |
chmod(f, mode) | |
def byte_compile(self, to_compile): | |
if sys.dont_write_bytecode: | |
return | |
from distutils.util import byte_compile | |
try: | |
# try to make the byte compile messages quieter | |
log.set_verbosity(self.verbose - 1) | |
byte_compile(to_compile, optimize=0, force=1, dry_run=self.dry_run) | |
if self.optimize: | |
byte_compile( | |
to_compile, optimize=self.optimize, force=1, | |
dry_run=self.dry_run, | |
) | |
finally: | |
log.set_verbosity(self.verbose) # restore original verbosity | |
__no_default_msg = textwrap.dedent(""" | |
bad install directory or PYTHONPATH | |
You are attempting to install a package to a directory that is not | |
on PYTHONPATH and which Python does not read ".pth" files from. The | |
installation directory you specified (via --install-dir, --prefix, or | |
the distutils default setting) was: | |
%s | |
and your PYTHONPATH environment variable currently contains: | |
%r | |
Here are some of your options for correcting the problem: | |
* You can choose a different installation directory, i.e., one that is | |
on PYTHONPATH or supports .pth files | |
* You can add the installation directory to the PYTHONPATH environment | |
variable. (It must then also be on PYTHONPATH whenever you run | |
Python and want to use the package(s) you are installing.) | |
* You can set up the installation directory to support ".pth" files by | |
using one of the approaches described here: | |
https://setuptools.pypa.io/en/latest/deprecated/easy_install.html#custom-installation-locations | |
Please make the appropriate changes for your system and try again. | |
""").strip() | |
def create_home_path(self): | |
"""Create directories under ~.""" | |
if not self.user: | |
return | |
home = convert_path(os.path.expanduser("~")) | |
for path in only_strs(self.config_vars.values()): | |
if path.startswith(home) and not os.path.isdir(path): | |
self.debug_print("os.makedirs('%s', 0o700)" % path) | |
os.makedirs(path, 0o700) | |
INSTALL_SCHEMES = dict( | |
posix=dict( | |
install_dir='$base/lib/python$py_version_short/site-packages', | |
script_dir='$base/bin', | |
), | |
) | |
DEFAULT_SCHEME = dict( | |
install_dir='$base/Lib/site-packages', | |
script_dir='$base/Scripts', | |
) | |
def _expand(self, *attrs): | |
config_vars = self.get_finalized_command('install').config_vars | |
if self.prefix: | |
# Set default install_dir/scripts from --prefix | |
config_vars = dict(config_vars) | |
config_vars['base'] = self.prefix | |
scheme = self.INSTALL_SCHEMES.get(os.name, self.DEFAULT_SCHEME) | |
for attr, val in scheme.items(): | |
if getattr(self, attr, None) is None: | |
setattr(self, attr, val) | |
from distutils.util import subst_vars | |
for attr in attrs: | |
val = getattr(self, attr) | |
if val is not None: | |
val = subst_vars(val, config_vars) | |
if os.name == 'posix': | |
val = os.path.expanduser(val) | |
setattr(self, attr, val) | |
def _pythonpath(): | |
items = os.environ.get('PYTHONPATH', '').split(os.pathsep) | |
return filter(None, items) | |
def get_site_dirs(): | |
""" | |
Return a list of 'site' dirs | |
""" | |
sitedirs = [] | |
# start with PYTHONPATH | |
sitedirs.extend(_pythonpath()) | |
prefixes = [sys.prefix] | |
if sys.exec_prefix != sys.prefix: | |
prefixes.append(sys.exec_prefix) | |
for prefix in prefixes: | |
if not prefix: | |
continue | |
if sys.platform in ('os2emx', 'riscos'): | |
sitedirs.append(os.path.join(prefix, "Lib", "site-packages")) | |
elif os.sep == '/': | |
sitedirs.extend([ | |
os.path.join( | |
prefix, | |
"lib", | |
"python{}.{}".format(*sys.version_info), | |
"site-packages", | |
), | |
os.path.join(prefix, "lib", "site-python"), | |
]) | |
else: | |
sitedirs.extend([ | |
prefix, | |
os.path.join(prefix, "lib", "site-packages"), | |
]) | |
if sys.platform != 'darwin': | |
continue | |
# for framework builds *only* we add the standard Apple | |
# locations. Currently only per-user, but /Library and | |
# /Network/Library could be added too | |
if 'Python.framework' not in prefix: | |
continue | |
home = os.environ.get('HOME') | |
if not home: | |
continue | |
home_sp = os.path.join( | |
home, | |
'Library', | |
'Python', | |
'{}.{}'.format(*sys.version_info), | |
'site-packages', | |
) | |
sitedirs.append(home_sp) | |
lib_paths = get_path('purelib'), get_path('platlib') | |
sitedirs.extend(s for s in lib_paths if s not in sitedirs) | |
if site.ENABLE_USER_SITE: | |
sitedirs.append(site.USER_SITE) | |
with contextlib.suppress(AttributeError): | |
sitedirs.extend(site.getsitepackages()) | |
sitedirs = list(map(normalize_path, sitedirs)) | |
return sitedirs | |
def expand_paths(inputs): # noqa: C901 # is too complex (11) # FIXME | |
"""Yield sys.path directories that might contain "old-style" packages""" | |
seen = {} | |
for dirname in inputs: | |
dirname = normalize_path(dirname) | |
if dirname in seen: | |
continue | |
seen[dirname] = 1 | |
if not os.path.isdir(dirname): | |
continue | |
files = os.listdir(dirname) | |
yield dirname, files | |
for name in files: | |
if not name.endswith('.pth'): | |
# We only care about the .pth files | |
continue | |
if name in ('easy-install.pth', 'setuptools.pth'): | |
# Ignore .pth files that we control | |
continue | |
# Read the .pth file | |
f = open(os.path.join(dirname, name)) | |
lines = list(yield_lines(f)) | |
f.close() | |
# Yield existing non-dupe, non-import directory lines from it | |
for line in lines: | |
if line.startswith("import"): | |
continue | |
line = normalize_path(line.rstrip()) | |
if line in seen: | |
continue | |
seen[line] = 1 | |
if not os.path.isdir(line): | |
continue | |
yield line, os.listdir(line) | |
def extract_wininst_cfg(dist_filename): | |
"""Extract configuration data from a bdist_wininst .exe | |
Returns a configparser.RawConfigParser, or None | |
""" | |
f = open(dist_filename, 'rb') | |
try: | |
endrec = zipfile._EndRecData(f) | |
if endrec is None: | |
return None | |
prepended = (endrec[9] - endrec[5]) - endrec[6] | |
if prepended < 12: # no wininst data here | |
return None | |
f.seek(prepended - 12) | |
tag, cfglen, bmlen = struct.unpack("<iii", f.read(12)) | |
if tag not in (0x1234567A, 0x1234567B): | |
return None # not a valid tag | |
f.seek(prepended - (12 + cfglen)) | |
init = {'version': '', 'target_version': ''} | |
cfg = configparser.RawConfigParser(init) | |
try: | |
part = f.read(cfglen) | |
# Read up to the first null byte. | |
config = part.split(b'\0', 1)[0] | |
# Now the config is in bytes, but for RawConfigParser, it should | |
# be text, so decode it. | |
config = config.decode(sys.getfilesystemencoding()) | |
cfg.read_file(io.StringIO(config)) | |
except configparser.Error: | |
return None | |
if not cfg.has_section('metadata') or not cfg.has_section('Setup'): | |
return None | |
return cfg | |
finally: | |
f.close() | |
def get_exe_prefixes(exe_filename): | |
"""Get exe->egg path translations for a given .exe file""" | |
prefixes = [ | |
('PURELIB/', ''), | |
('PLATLIB/pywin32_system32', ''), | |
('PLATLIB/', ''), | |
('SCRIPTS/', 'EGG-INFO/scripts/'), | |
('DATA/lib/site-packages', ''), | |
] | |
z = zipfile.ZipFile(exe_filename) | |
try: | |
for info in z.infolist(): | |
name = info.filename | |
parts = name.split('/') | |
if len(parts) == 3 and parts[2] == 'PKG-INFO': | |
if parts[1].endswith('.egg-info'): | |
prefixes.insert(0, ('/'.join(parts[:2]), 'EGG-INFO/')) | |
break | |
if len(parts) != 2 or not name.endswith('.pth'): | |
continue | |
if name.endswith('-nspkg.pth'): | |
continue | |
if parts[0].upper() in ('PURELIB', 'PLATLIB'): | |
contents = z.read(name).decode() | |
for pth in yield_lines(contents): | |
pth = pth.strip().replace('\\', '/') | |
if not pth.startswith('import'): | |
prefixes.append((('%s/%s/' % (parts[0], pth)), '')) | |
finally: | |
z.close() | |
prefixes = [(x.lower(), y) for x, y in prefixes] | |
prefixes.sort() | |
prefixes.reverse() | |
return prefixes | |
class PthDistributions(Environment): | |
"""A .pth file with Distribution paths in it""" | |
def __init__(self, filename, sitedirs=()): | |
self.filename = filename | |
self.sitedirs = list(map(normalize_path, sitedirs)) | |
self.basedir = normalize_path(os.path.dirname(self.filename)) | |
self.paths, self.dirty = self._load() | |
# keep a copy if someone manually updates the paths attribute on the instance | |
self._init_paths = self.paths[:] | |
super().__init__([], None, None) | |
for path in yield_lines(self.paths): | |
list(map(self.add, find_distributions(path, True))) | |
def _load_raw(self): | |
paths = [] | |
dirty = saw_import = False | |
seen = dict.fromkeys(self.sitedirs) | |
f = open(self.filename, 'rt') | |
for line in f: | |
path = line.rstrip() | |
# still keep imports and empty/commented lines for formatting | |
paths.append(path) | |
if line.startswith(('import ', 'from ')): | |
saw_import = True | |
continue | |
stripped_path = path.strip() | |
if not stripped_path or stripped_path.startswith('#'): | |
continue | |
# skip non-existent paths, in case somebody deleted a package | |
# manually, and duplicate paths as well | |
normalized_path = normalize_path(os.path.join(self.basedir, path)) | |
if normalized_path in seen or not os.path.exists(normalized_path): | |
log.debug("cleaned up dirty or duplicated %r", path) | |
dirty = True | |
paths.pop() | |
continue | |
seen[normalized_path] = 1 | |
f.close() | |
# remove any trailing empty/blank line | |
while paths and not paths[-1].strip(): | |
paths.pop() | |
dirty = True | |
return paths, dirty or (paths and saw_import) | |
def _load(self): | |
if os.path.isfile(self.filename): | |
return self._load_raw() | |
return [], False | |
def save(self): | |
"""Write changed .pth file back to disk""" | |
# first reload the file | |
last_paths, last_dirty = self._load() | |
# and check that there are no difference with what we have. | |
# there can be difference if someone else has written to the file | |
# since we first loaded it. | |
# we don't want to lose the eventual new paths added since then. | |
for path in last_paths[:]: | |
if path not in self.paths: | |
self.paths.append(path) | |
log.info("detected new path %r", path) | |
last_dirty = True | |
else: | |
last_paths.remove(path) | |
# also, re-check that all paths are still valid before saving them | |
for path in self.paths[:]: | |
if path not in last_paths \ | |
and not path.startswith(('import ', 'from ', '#')): | |
absolute_path = os.path.join(self.basedir, path) | |
if not os.path.exists(absolute_path): | |
self.paths.remove(path) | |
log.info("removing now non-existent path %r", path) | |
last_dirty = True | |
self.dirty |= last_dirty or self.paths != self._init_paths | |
if not self.dirty: | |
return | |
rel_paths = list(map(self.make_relative, self.paths)) | |
if rel_paths: | |
log.debug("Saving %s", self.filename) | |
lines = self._wrap_lines(rel_paths) | |
data = '\n'.join(lines) + '\n' | |
if os.path.islink(self.filename): | |
os.unlink(self.filename) | |
with open(self.filename, 'wt') as f: | |
f.write(data) | |
elif os.path.exists(self.filename): | |
log.debug("Deleting empty %s", self.filename) | |
os.unlink(self.filename) | |
self.dirty = False | |
self._init_paths[:] = self.paths[:] | |
def _wrap_lines(lines): | |
return lines | |
def add(self, dist): | |
"""Add `dist` to the distribution map""" | |
new_path = ( | |
dist.location not in self.paths and ( | |
dist.location not in self.sitedirs or | |
# account for '.' being in PYTHONPATH | |
dist.location == os.getcwd() | |
) | |
) | |
if new_path: | |
self.paths.append(dist.location) | |
self.dirty = True | |
super().add(dist) | |
def remove(self, dist): | |
"""Remove `dist` from the distribution map""" | |
while dist.location in self.paths: | |
self.paths.remove(dist.location) | |
self.dirty = True | |
super().remove(dist) | |
def make_relative(self, path): | |
npath, last = os.path.split(normalize_path(path)) | |
baselen = len(self.basedir) | |
parts = [last] | |
sep = os.altsep == '/' and '/' or os.sep | |
while len(npath) >= baselen: | |
if npath == self.basedir: | |
parts.append(os.curdir) | |
parts.reverse() | |
return sep.join(parts) | |
npath, last = os.path.split(npath) | |
parts.append(last) | |
else: | |
return path | |
class RewritePthDistributions(PthDistributions): | |
def _wrap_lines(cls, lines): | |
yield cls.prelude | |
for line in lines: | |
yield line | |
yield cls.postlude | |
prelude = _one_liner(""" | |
import sys | |
sys.__plen = len(sys.path) | |
""") | |
postlude = _one_liner(""" | |
import sys | |
new = sys.path[sys.__plen:] | |
del sys.path[sys.__plen:] | |
p = getattr(sys, '__egginsert', 0) | |
sys.path[p:p] = new | |
sys.__egginsert = p + len(new) | |
""") | |
if os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite': | |
PthDistributions = RewritePthDistributions | |
def _first_line_re(): | |
""" | |
Return a regular expression based on first_line_re suitable for matching | |
strings. | |
""" | |
if isinstance(first_line_re.pattern, str): | |
return first_line_re | |
# first_line_re in Python >=3.1.4 and >=3.2.1 is a bytes pattern. | |
return re.compile(first_line_re.pattern.decode()) | |
def auto_chmod(func, arg, exc): | |
if func in [os.unlink, os.remove] and os.name == 'nt': | |
chmod(arg, stat.S_IWRITE) | |
return func(arg) | |
et, ev, _ = sys.exc_info() | |
# TODO: This code doesn't make sense. What is it trying to do? | |
raise (ev[0], ev[1] + (" %s %s" % (func, arg))) | |
def update_dist_caches(dist_path, fix_zipimporter_caches): | |
""" | |
Fix any globally cached `dist_path` related data | |
`dist_path` should be a path of a newly installed egg distribution (zipped | |
or unzipped). | |
sys.path_importer_cache contains finder objects that have been cached when | |
importing data from the original distribution. Any such finders need to be | |
cleared since the replacement distribution might be packaged differently, | |
e.g. a zipped egg distribution might get replaced with an unzipped egg | |
folder or vice versa. Having the old finders cached may then cause Python | |
to attempt loading modules from the replacement distribution using an | |
incorrect loader. | |
zipimport.zipimporter objects are Python loaders charged with importing | |
data packaged inside zip archives. If stale loaders referencing the | |
original distribution, are left behind, they can fail to load modules from | |
the replacement distribution. E.g. if an old zipimport.zipimporter instance | |
is used to load data from a new zipped egg archive, it may cause the | |
operation to attempt to locate the requested data in the wrong location - | |
one indicated by the original distribution's zip archive directory | |
information. Such an operation may then fail outright, e.g. report having | |
read a 'bad local file header', or even worse, it may fail silently & | |
return invalid data. | |
zipimport._zip_directory_cache contains cached zip archive directory | |
information for all existing zipimport.zipimporter instances and all such | |
instances connected to the same archive share the same cached directory | |
information. | |
If asked, and the underlying Python implementation allows it, we can fix | |
all existing zipimport.zipimporter instances instead of having to track | |
them down and remove them one by one, by updating their shared cached zip | |
archive directory information. This, of course, assumes that the | |
replacement distribution is packaged as a zipped egg. | |
If not asked to fix existing zipimport.zipimporter instances, we still do | |
our best to clear any remaining zipimport.zipimporter related cached data | |
that might somehow later get used when attempting to load data from the new | |
distribution and thus cause such load operations to fail. Note that when | |
tracking down such remaining stale data, we can not catch every conceivable | |
usage from here, and we clear only those that we know of and have found to | |
cause problems if left alive. Any remaining caches should be updated by | |
whomever is in charge of maintaining them, i.e. they should be ready to | |
handle us replacing their zip archives with new distributions at runtime. | |
""" | |
# There are several other known sources of stale zipimport.zipimporter | |
# instances that we do not clear here, but might if ever given a reason to | |
# do so: | |
# * Global setuptools pkg_resources.working_set (a.k.a. 'master working | |
# set') may contain distributions which may in turn contain their | |
# zipimport.zipimporter loaders. | |
# * Several zipimport.zipimporter loaders held by local variables further | |
# up the function call stack when running the setuptools installation. | |
# * Already loaded modules may have their __loader__ attribute set to the | |
# exact loader instance used when importing them. Python 3.4 docs state | |
# that this information is intended mostly for introspection and so is | |
# not expected to cause us problems. | |
normalized_path = normalize_path(dist_path) | |
_uncache(normalized_path, sys.path_importer_cache) | |
if fix_zipimporter_caches: | |
_replace_zip_directory_cache_data(normalized_path) | |
else: | |
# Here, even though we do not want to fix existing and now stale | |
# zipimporter cache information, we still want to remove it. Related to | |
# Python's zip archive directory information cache, we clear each of | |
# its stale entries in two phases: | |
# 1. Clear the entry so attempting to access zip archive information | |
# via any existing stale zipimport.zipimporter instances fails. | |
# 2. Remove the entry from the cache so any newly constructed | |
# zipimport.zipimporter instances do not end up using old stale | |
# zip archive directory information. | |
# This whole stale data removal step does not seem strictly necessary, | |
# but has been left in because it was done before we started replacing | |
# the zip archive directory information cache content if possible, and | |
# there are no relevant unit tests that we can depend on to tell us if | |
# this is really needed. | |
_remove_and_clear_zip_directory_cache_data(normalized_path) | |
def _collect_zipimporter_cache_entries(normalized_path, cache): | |
""" | |
Return zipimporter cache entry keys related to a given normalized path. | |
Alternative path spellings (e.g. those using different character case or | |
those using alternative path separators) related to the same path are | |
included. Any sub-path entries are included as well, i.e. those | |
corresponding to zip archives embedded in other zip archives. | |
""" | |
result = [] | |
prefix_len = len(normalized_path) | |
for p in cache: | |
np = normalize_path(p) | |
if (np.startswith(normalized_path) and | |
np[prefix_len:prefix_len + 1] in (os.sep, '')): | |
result.append(p) | |
return result | |
def _update_zipimporter_cache(normalized_path, cache, updater=None): | |
""" | |
Update zipimporter cache data for a given normalized path. | |
Any sub-path entries are processed as well, i.e. those corresponding to zip | |
archives embedded in other zip archives. | |
Given updater is a callable taking a cache entry key and the original entry | |
(after already removing the entry from the cache), and expected to update | |
the entry and possibly return a new one to be inserted in its place. | |
Returning None indicates that the entry should not be replaced with a new | |
one. If no updater is given, the cache entries are simply removed without | |
any additional processing, the same as if the updater simply returned None. | |
""" | |
for p in _collect_zipimporter_cache_entries(normalized_path, cache): | |
# N.B. pypy's custom zipimport._zip_directory_cache implementation does | |
# not support the complete dict interface: | |
# * Does not support item assignment, thus not allowing this function | |
# to be used only for removing existing cache entries. | |
# * Does not support the dict.pop() method, forcing us to use the | |
# get/del patterns instead. For more detailed information see the | |
# following links: | |
# https://github.com/pypa/setuptools/issues/202#issuecomment-202913420 | |
# https://foss.heptapod.net/pypy/pypy/-/blob/144c4e65cb6accb8e592f3a7584ea38265d1873c/pypy/module/zipimport/interp_zipimport.py | |
old_entry = cache[p] | |
del cache[p] | |
new_entry = updater and updater(p, old_entry) | |
if new_entry is not None: | |
cache[p] = new_entry | |
def _uncache(normalized_path, cache): | |
_update_zipimporter_cache(normalized_path, cache) | |
def _remove_and_clear_zip_directory_cache_data(normalized_path): | |
def clear_and_remove_cached_zip_archive_directory_data(path, old_entry): | |
old_entry.clear() | |
_update_zipimporter_cache( | |
normalized_path, zipimport._zip_directory_cache, | |
updater=clear_and_remove_cached_zip_archive_directory_data) | |
# PyPy Python implementation does not allow directly writing to the | |
# zipimport._zip_directory_cache and so prevents us from attempting to correct | |
# its content. The best we can do there is clear the problematic cache content | |
# and have PyPy repopulate it as needed. The downside is that if there are any | |
# stale zipimport.zipimporter instances laying around, attempting to use them | |
# will fail due to not having its zip archive directory information available | |
# instead of being automatically corrected to use the new correct zip archive | |
# directory information. | |
if '__pypy__' in sys.builtin_module_names: | |
_replace_zip_directory_cache_data = \ | |
_remove_and_clear_zip_directory_cache_data | |
else: | |
def _replace_zip_directory_cache_data(normalized_path): | |
def replace_cached_zip_archive_directory_data(path, old_entry): | |
# N.B. In theory, we could load the zip directory information just | |
# once for all updated path spellings, and then copy it locally and | |
# update its contained path strings to contain the correct | |
# spelling, but that seems like a way too invasive move (this cache | |
# structure is not officially documented anywhere and could in | |
# theory change with new Python releases) for no significant | |
# benefit. | |
old_entry.clear() | |
zipimport.zipimporter(path) | |
old_entry.update(zipimport._zip_directory_cache[path]) | |
return old_entry | |
_update_zipimporter_cache( | |
normalized_path, zipimport._zip_directory_cache, | |
updater=replace_cached_zip_archive_directory_data) | |
def is_python(text, filename='<string>'): | |
"Is this string a valid Python script?" | |
try: | |
compile(text, filename, 'exec') | |
except (SyntaxError, TypeError): | |
return False | |
else: | |
return True | |
def is_sh(executable): | |
"""Determine if the specified executable is a .sh (contains a #! line)""" | |
try: | |
with io.open(executable, encoding='latin-1') as fp: | |
magic = fp.read(2) | |
except (OSError, IOError): | |
return executable | |
return magic == '#!' | |
def nt_quote_arg(arg): | |
"""Quote a command line argument according to Windows parsing rules""" | |
return subprocess.list2cmdline([arg]) | |
def is_python_script(script_text, filename): | |
"""Is this text, as a whole, a Python script? (as opposed to shell/bat/etc. | |
""" | |
if filename.endswith('.py') or filename.endswith('.pyw'): | |
return True # extension says it's Python | |
if is_python(script_text, filename): | |
return True # it's syntactically valid Python | |
if script_text.startswith('#!'): | |
# It begins with a '#!' line, so check if 'python' is in it somewhere | |
return 'python' in script_text.splitlines()[0].lower() | |
return False # Not any Python I can recognize | |
try: | |
from os import chmod as _chmod | |
except ImportError: | |
# Jython compatibility | |
def _chmod(*args): | |
pass | |
def chmod(path, mode): | |
log.debug("changing mode of %s to %o", path, mode) | |
try: | |
_chmod(path, mode) | |
except os.error as e: | |
log.debug("chmod failed: %s", e) | |
class CommandSpec(list): | |
""" | |
A command spec for a #! header, specified as a list of arguments akin to | |
those passed to Popen. | |
""" | |
options = [] | |
split_args = dict() | |
def best(cls): | |
""" | |
Choose the best CommandSpec class based on environmental conditions. | |
""" | |
return cls | |
def _sys_executable(cls): | |
_default = os.path.normpath(sys.executable) | |
return os.environ.get('__PYVENV_LAUNCHER__', _default) | |
def from_param(cls, param): | |
""" | |
Construct a CommandSpec from a parameter to build_scripts, which may | |
be None. | |
""" | |
if isinstance(param, cls): | |
return param | |
if isinstance(param, list): | |
return cls(param) | |
if param is None: | |
return cls.from_environment() | |
# otherwise, assume it's a string. | |
return cls.from_string(param) | |
def from_environment(cls): | |
return cls([cls._sys_executable()]) | |
def from_string(cls, string): | |
""" | |
Construct a command spec from a simple string representing a command | |
line parseable by shlex.split. | |
""" | |
items = shlex.split(string, **cls.split_args) | |
return cls(items) | |
def install_options(self, script_text): | |
self.options = shlex.split(self._extract_options(script_text)) | |
cmdline = subprocess.list2cmdline(self) | |
if not isascii(cmdline): | |
self.options[:0] = ['-x'] | |
def _extract_options(orig_script): | |
""" | |
Extract any options from the first line of the script. | |
""" | |
first = (orig_script + '\n').splitlines()[0] | |
match = _first_line_re().match(first) | |
options = match.group(1) or '' if match else '' | |
return options.strip() | |
def as_header(self): | |
return self._render(self + list(self.options)) | |
def _strip_quotes(item): | |
_QUOTES = '"\'' | |
for q in _QUOTES: | |
if item.startswith(q) and item.endswith(q): | |
return item[1:-1] | |
return item | |
def _render(items): | |
cmdline = subprocess.list2cmdline( | |
CommandSpec._strip_quotes(item.strip()) for item in items) | |
return '#!' + cmdline + '\n' | |
# For pbr compat; will be removed in a future version. | |
sys_executable = CommandSpec._sys_executable() | |
class WindowsCommandSpec(CommandSpec): | |
split_args = dict(posix=False) | |
class ScriptWriter: | |
""" | |
Encapsulates behavior around writing entry point scripts for console and | |
gui apps. | |
""" | |
template = textwrap.dedent(r""" | |
# EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r | |
import re | |
import sys | |
# for compatibility with easy_install; see #2198 | |
__requires__ = %(spec)r | |
try: | |
from importlib.metadata import distribution | |
except ImportError: | |
try: | |
from importlib_metadata import distribution | |
except ImportError: | |
from pkg_resources import load_entry_point | |
def importlib_load_entry_point(spec, group, name): | |
dist_name, _, _ = spec.partition('==') | |
matches = ( | |
entry_point | |
for entry_point in distribution(dist_name).entry_points | |
if entry_point.group == group and entry_point.name == name | |
) | |
return next(matches).load() | |
globals().setdefault('load_entry_point', importlib_load_entry_point) | |
if __name__ == '__main__': | |
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) | |
sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)()) | |
""").lstrip() | |
command_spec_class = CommandSpec | |
def get_args(cls, dist, header=None): | |
""" | |
Yield write_script() argument tuples for a distribution's | |
console_scripts and gui_scripts entry points. | |
""" | |
if header is None: | |
header = cls.get_header() | |
spec = str(dist.as_requirement()) | |
for type_ in 'console', 'gui': | |
group = type_ + '_scripts' | |
for name, ep in dist.get_entry_map(group).items(): | |
cls._ensure_safe_name(name) | |
script_text = cls.template % locals() | |
args = cls._get_script_args(type_, name, header, script_text) | |
for res in args: | |
yield res | |
def _ensure_safe_name(name): | |
""" | |
Prevent paths in *_scripts entry point names. | |
""" | |
has_path_sep = re.search(r'[\\/]', name) | |
if has_path_sep: | |
raise ValueError("Path separators not allowed in script names") | |
def best(cls): | |
""" | |
Select the best ScriptWriter for this environment. | |
""" | |
if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'): | |
return WindowsScriptWriter.best() | |
else: | |
return cls | |
def _get_script_args(cls, type_, name, header, script_text): | |
# Simply write the stub with no extension. | |
yield (name, header + script_text) | |
def get_header(cls, script_text="", executable=None): | |
"""Create a #! line, getting options (if any) from script_text""" | |
cmd = cls.command_spec_class.best().from_param(executable) | |
cmd.install_options(script_text) | |
return cmd.as_header() | |
class WindowsScriptWriter(ScriptWriter): | |
command_spec_class = WindowsCommandSpec | |
def best(cls): | |
""" | |
Select the best ScriptWriter suitable for Windows | |
""" | |
writer_lookup = dict( | |
executable=WindowsExecutableLauncherWriter, | |
natural=cls, | |
) | |
# for compatibility, use the executable launcher by default | |
launcher = os.environ.get('SETUPTOOLS_LAUNCHER', 'executable') | |
return writer_lookup[launcher] | |
def _get_script_args(cls, type_, name, header, script_text): | |
"For Windows, add a .py extension" | |
ext = dict(console='.pya', gui='.pyw')[type_] | |
if ext not in os.environ['PATHEXT'].lower().split(';'): | |
msg = ( | |
"{ext} not listed in PATHEXT; scripts will not be " | |
"recognized as executables." | |
).format(**locals()) | |
SetuptoolsWarning.emit(msg) | |
old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe'] | |
old.remove(ext) | |
header = cls._adjust_header(type_, header) | |
blockers = [name + x for x in old] | |
yield name + ext, header + script_text, 't', blockers | |
def _adjust_header(cls, type_, orig_header): | |
""" | |
Make sure 'pythonw' is used for gui and 'python' is used for | |
console (regardless of what sys.executable is). | |
""" | |
pattern = 'pythonw.exe' | |
repl = 'python.exe' | |
if type_ == 'gui': | |
pattern, repl = repl, pattern | |
pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE) | |
new_header = pattern_ob.sub(string=orig_header, repl=repl) | |
return new_header if cls._use_header(new_header) else orig_header | |
def _use_header(new_header): | |
""" | |
Should _adjust_header use the replaced header? | |
On non-windows systems, always use. On | |
Windows systems, only use the replaced header if it resolves | |
to an executable on the system. | |
""" | |
clean_header = new_header[2:-1].strip('"') | |
return sys.platform != 'win32' or find_executable(clean_header) | |
class WindowsExecutableLauncherWriter(WindowsScriptWriter): | |
def _get_script_args(cls, type_, name, header, script_text): | |
""" | |
For Windows, add a .py extension and an .exe launcher | |
""" | |
if type_ == 'gui': | |
launcher_type = 'gui' | |
ext = '-script.pyw' | |
old = ['.pyw'] | |
else: | |
launcher_type = 'cli' | |
ext = '-script.py' | |
old = ['.py', '.pyc', '.pyo'] | |
hdr = cls._adjust_header(type_, header) | |
blockers = [name + x for x in old] | |
yield (name + ext, hdr + script_text, 't', blockers) | |
yield ( | |
name + '.exe', get_win_launcher(launcher_type), | |
'b' # write in binary mode | |
) | |
if not is_64bit(): | |
# install a manifest for the launcher to prevent Windows | |
# from detecting it as an installer (which it will for | |
# launchers like easy_install.exe). Consider only | |
# adding a manifest for launchers detected as installers. | |
# See Distribute #143 for details. | |
m_name = name + '.exe.manifest' | |
yield (m_name, load_launcher_manifest(name), 't') | |
def get_win_launcher(type): | |
""" | |
Load the Windows launcher (executable) suitable for launching a script. | |
`type` should be either 'cli' or 'gui' | |
Returns the executable as a byte string. | |
""" | |
launcher_fn = '%s.exe' % type | |
if is_64bit(): | |
if get_platform() == "win-arm64": | |
launcher_fn = launcher_fn.replace(".", "-arm64.") | |
else: | |
launcher_fn = launcher_fn.replace(".", "-64.") | |
else: | |
launcher_fn = launcher_fn.replace(".", "-32.") | |
return resource_string('setuptools', launcher_fn) | |
def load_launcher_manifest(name): | |
manifest = pkg_resources.resource_string(__name__, 'launcher manifest.xml') | |
return manifest.decode('utf-8') % vars() | |
def _rmtree(path, ignore_errors=False, onexc=auto_chmod): | |
return py312compat.shutil_rmtree(path, ignore_errors, onexc) | |
def current_umask(): | |
tmp = os.umask(0o022) | |
os.umask(tmp) | |
return tmp | |
def only_strs(values): | |
""" | |
Exclude non-str values. Ref #3063. | |
""" | |
return filter(lambda val: isinstance(val, str), values) | |
class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): | |
_SUMMARY = "easy_install command is deprecated." | |
_DETAILS = """ | |
Please avoid running ``setup.py`` and ``easy_install``. | |
Instead, use pypa/build, pypa/installer or other | |
standards-based tools. | |
""" | |
_SEE_URL = "https://github.com/pypa/setuptools/issues/917" | |
# _DUE_DATE not defined yet | |