aboutsummaryrefslogtreecommitdiffstats
path: root/setuptools/command/easy_install.py
diff options
context:
space:
mode:
authorJason R. Coombs <jaraco@jaraco.com>2015-12-31 10:51:55 -0500
committerJason R. Coombs <jaraco@jaraco.com>2015-12-31 10:51:55 -0500
commit29fa01621c3de0a5c78c4f49b5d051386d0d566f (patch)
treefce7387c8ba82178be987b004cbda1c22005a04f /setuptools/command/easy_install.py
parent928324bd76f35e9c8c526df828577b5640a95ed0 (diff)
parent6bdbe8957d8c8d293e3fea3fa4baf45eb7c3a3a4 (diff)
downloadexternal_python_setuptools-29fa01621c3de0a5c78c4f49b5d051386d0d566f.tar.gz
external_python_setuptools-29fa01621c3de0a5c78c4f49b5d051386d0d566f.tar.bz2
external_python_setuptools-29fa01621c3de0a5c78c4f49b5d051386d0d566f.zip
Merge with master. Ref #229.
--HG-- branch : feature/issue-229
Diffstat (limited to 'setuptools/command/easy_install.py')
-rwxr-xr-xsetuptools/command/easy_install.py631
1 files changed, 388 insertions, 243 deletions
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index a24e3b59..6aab38c8 100755
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -20,6 +20,7 @@ from distutils.errors import DistutilsArgError, DistutilsOptionError, \
from distutils.command.install import INSTALL_SCHEMES, SCHEME_KEYS
from distutils import log, dir_util
from distutils.command.build_scripts import first_line_re
+from distutils.spawn import find_executable
import sys
import os
import zipimport
@@ -35,6 +36,9 @@ import warnings
import site
import struct
import contextlib
+import subprocess
+import shlex
+import io
import six
from six.moves import configparser
@@ -55,15 +59,10 @@ from pkg_resources import (
)
import pkg_resources
-
# Turn on PEP440Warnings
warnings.filterwarnings("default", category=pkg_resources.PEP440Warning)
-sys_executable = os.environ.get('__PYVENV_LAUNCHER__',
- os.path.normpath(sys.executable))
-
-
__all__ = [
'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg',
'main', 'get_exe_prefixes',
@@ -155,12 +154,9 @@ class easy_install(Command):
create_index = PackageIndex
def initialize_options(self):
- if site.ENABLE_USER_SITE:
- whereami = os.path.abspath(__file__)
- self.user = whereami.startswith(site.USER_SITE)
- else:
- self.user = 0
-
+ # 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
@@ -206,20 +202,34 @@ class easy_install(Command):
)
def delete_blockers(self, blockers):
- for filename in blockers:
- if os.path.exists(filename) or os.path.islink(filename):
- log.info("Deleting %s", filename)
- if not self.dry_run:
- if (os.path.isdir(filename) and
- not os.path.islink(filename)):
- rmtree(filename)
- else:
- os.unlink(filename)
+ 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)
+
+ @staticmethod
+ def _render_version():
+ """
+ Render the Setuptools version and installation details, then exit.
+ """
+ ver = sys.version[:3]
+ dist = get_distribution('setuptools')
+ tmpl = 'setuptools {dist.version} from {dist.location} (Python {ver})'
+ print(tmpl.format(**locals()))
+ raise SystemExit()
def finalize_options(self):
- if self.version:
- print('setuptools %s' % get_distribution('setuptools').version)
- sys.exit()
+ self.version and self._render_version()
py_version = sys.version.split()[0]
prefix, exec_prefix = get_config_vars('prefix', 'exec_prefix')
@@ -243,18 +253,7 @@ class easy_install(Command):
self.config_vars['userbase'] = self.install_userbase
self.config_vars['usersite'] = self.install_usersite
- # fix the install_dir if "--user" was used
- # XXX: duplicate of the code in the setup command
- if self.user and site.ENABLE_USER_SITE:
- self.create_home_path()
- if self.install_userbase is None:
- raise DistutilsPlatformError(
- "User base directory is not specified")
- self.install_base = self.install_platbase = self.install_userbase
- if os.name == 'posix':
- self.select_scheme("unix_user")
- else:
- self.select_scheme(os.name + "_user")
+ self._fix_install_dir_for_user_site()
self.expand_basedirs()
self.expand_dirs()
@@ -349,6 +348,21 @@ class easy_install(Command):
self.outputs = []
+ def _fix_install_dir_for_user_site(self):
+ """
+ Fix the install_dir if "--user" was used.
+ """
+ if not self.user or not site.ENABLE_USER_SITE:
+ 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 = os.name.replace('posix', 'unix') + '_user'
+ self.select_scheme(scheme_name)
+
def _expand_attrs(self, attrs):
for attr in attrs:
val = getattr(self, attr)
@@ -441,7 +455,7 @@ class easy_install(Command):
self.pth_file = None
PYTHONPATH = os.environ.get('PYTHONPATH', '').split(os.pathsep)
- if instdir not in map(normalize_path, [_f for _f in PYTHONPATH if _f]):
+ if instdir not in map(normalize_path, filter(None, PYTHONPATH)):
# only PYTHONPATH dirs need a site.py, so pretend it's there
self.sitepy_installed = True
elif self.multi_version and not os.path.exists(pth_file):
@@ -449,43 +463,49 @@ class easy_install(Command):
self.pth_file = None # and don't create a .pth file
self.install_dir = instdir
- def cant_write_to_target(self):
- template = """can't create or remove files in install directory
+ __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:
+ The following error occurred while trying to add or remove files in the
+ installation directory:
- %s
+ %s
-The installation directory you specified (via --install-dir, --prefix, or
-the distutils default setting) was:
+ The installation directory you specified (via --install-dir, --prefix, or
+ the distutils default setting) was:
- %s
-"""
- msg = template % (sys.exc_info()[1], self.install_dir,)
+ %s
+ """).lstrip()
- if not os.path.exists(self.install_dir):
- msg += """
-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).
-"""
- else:
- msg += """
-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.
+ __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()
-For information on other options, you may wish to consult the
-documentation at:
+ __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.
- https://pythonhosted.org/setuptools/easy_install.html
+ For information on other options, you may wish to consult the
+ documentation at:
-Please make the appropriate changes for your system and try again.
-"""
+ https://pythonhosted.org/setuptools/easy_install.html
+
+ Please make the appropriate changes for your system and try again.
+ """).lstrip()
+
+ 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):
@@ -699,17 +719,10 @@ Please make the appropriate changes for your system and try again.
distros = WorkingSet([]).resolve(
[requirement], self.local_index, self.easy_install
)
- except DistributionNotFound:
- e = sys.exc_info()[1]
- raise DistutilsError(
- "Could not find required distribution %s" % e.args
- )
- except VersionConflict:
- e = sys.exc_info()[1]
- raise DistutilsError(
- "Installed distribution %s conflicts with requirement %s"
- % e.args
- )
+ except DistributionNotFound as e:
+ raise DistutilsError(str(e))
+ except VersionConflict as e:
+ raise DistutilsError(e.report())
if self.always_copy or self.always_copy_from:
# Force all the relevant distros to be copied or activated
for dist in distros:
@@ -749,9 +762,10 @@ Please make the appropriate changes for your system and try again.
return dst
def install_wrapper_scripts(self, dist):
- if not self.exclude_scripts:
- for args in get_script_args(dist):
- self.write_script(*args)
+ 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"""
@@ -759,8 +773,8 @@ Please make the appropriate changes for your system and try again.
is_script = is_python_script(script_text, script_name)
if is_script:
- script_text = (get_script_header(script_text) +
- self._load_template(dev_path) % locals())
+ body = self._load_template(dev_path) % locals()
+ script_text = ScriptWriter.get_header(script_text) + body
self.write_script(script_name, _to_ascii(script_text), 'b')
@staticmethod
@@ -792,9 +806,8 @@ Please make the appropriate changes for your system and try again.
ensure_directory(target)
if os.path.exists(target):
os.unlink(target)
- f = open(target, "w" + mode)
- f.write(contents)
- f.close()
+ with open(target, "w" + mode) as f:
+ f.write(contents)
chmod(target, 0o777 - mask)
def install_eggs(self, spec, dist_filename, tmpdir):
@@ -923,9 +936,10 @@ Please make the appropriate changes for your system and try again.
f.write('%s: %s\n' % (k.replace('_', '-').title(), v))
f.close()
script_dir = os.path.join(_egg_info, 'scripts')
- self.delete_blockers( # delete entry-point scripts to avoid duping
+ # delete entry-point scripts to avoid duping
+ self.delete_blockers(
[os.path.join(script_dir, args[0]) for args in
- get_script_args(dist)]
+ ScriptWriter.get_args(dist)]
)
# Build .egg file from tmpdir
bdist_egg.make_zipfile(
@@ -987,46 +1001,52 @@ Please make the appropriate changes for your system and try again.
f.write('\n'.join(locals()[name]) + '\n')
f.close()
+ __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()
+
+ __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.)
+ """)
+
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 += """
-
-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
-"""
+ msg += '\n' + self.__mv_warning
if self.install_dir not in map(normalize_path, sys.path):
- msg += """
+ msg += '\n' + self.__id_warning
-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.)
-"""
eggloc = dist.location
name = dist.project_name
version = dist.version
extras = '' # TODO: self.report_extras(req, dist)
return msg % locals()
- def report_editable(self, spec, setup_script):
- dirname = os.path.dirname(setup_script)
- python = sys.executable
- return """\nExtracted editable version of %(spec)s to %(dirname)s
+ __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::
+ 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
+ %(python)s setup.py develop
-See the setuptools documentation for the "develop" command for more info.
-""" % locals()
+ See the setuptools documentation for the "develop" command for more info.
+ """).lstrip()
+
+ 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)
@@ -1045,8 +1065,7 @@ See the setuptools documentation for the "develop" command for more info.
)
try:
run_setup(setup_script, args)
- except SystemExit:
- v = sys.exc_info()[1]
+ except SystemExit as v:
raise DistutilsError("Setup script exited with %s" % (v.args[0],))
def build_and_install(self, setup_script, setup_base):
@@ -1178,35 +1197,38 @@ See the setuptools documentation for the "develop" command for more info.
finally:
log.set_verbosity(self.verbose) # restore original verbosity
- def no_default_version_msg(self):
- template = """bad install directory or PYTHONPATH
+ __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:
-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
- %s
+ and your PYTHONPATH environment variable currently contains:
-and your PYTHONPATH environment variable currently contains:
+ %r
- %r
+ Here are some of your options for correcting the problem:
-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 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 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:
-* You can set up the installation directory to support ".pth" files by
- using one of the approaches described here:
+ https://pythonhosted.org/setuptools/easy_install.html#custom-installation-locations
- https://pythonhosted.org/setuptools/easy_install.html#custom-installation-locations
+ Please make the appropriate changes for your system and try again.""").lstrip()
-Please make the appropriate changes for your system and try again."""
+ def no_default_version_msg(self):
+ template = self.__no_default_msg
return template % (self.install_dir, os.environ.get('PYTHONPATH', ''))
def install_site_py(self):
@@ -1403,13 +1425,8 @@ def extract_wininst_cfg(dist_filename):
{'version': '', 'target_version': ''})
try:
part = f.read(cfglen)
- # part is in bytes, but we need to read up to the first null
- # byte.
- if sys.version_info >= (2, 6):
- null_byte = bytes([0])
- else:
- null_byte = chr(0)
- config = part.split(null_byte, 1)[0]
+ # 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())
@@ -1521,23 +1538,16 @@ class PthDistributions(Environment):
if not self.dirty:
return
- data = '\n'.join(map(self.make_relative, self.paths))
- if data:
+ rel_paths = list(map(self.make_relative, self.paths))
+ if rel_paths:
log.debug("Saving %s", self.filename)
- data = (
- "import sys; sys.__plen = len(sys.path)\n"
- "%s\n"
- "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)\n"
- ) % data
+ lines = self._wrap_lines(rel_paths)
+ data = '\n'.join(lines) + '\n'
if os.path.islink(self.filename):
os.unlink(self.filename)
- f = open(self.filename, 'wt')
- f.write(data)
- f.close()
+ with open(self.filename, 'wt') as f:
+ f.write(data)
elif os.path.exists(self.filename):
log.debug("Deleting empty %s", self.filename)
@@ -1545,6 +1555,10 @@ class PthDistributions(Environment):
self.dirty = False
+ @staticmethod
+ def _wrap_lines(lines):
+ return lines
+
def add(self, dist):
"""Add `dist` to the distribution map"""
new_path = (
@@ -1582,6 +1596,34 @@ class PthDistributions(Environment):
return path
+class RewritePthDistributions(PthDistributions):
+
+ @classmethod
+ def _wrap_lines(cls, lines):
+ yield cls.prelude
+ for line in lines:
+ yield line
+ yield cls.postlude
+
+ _inline = lambda text: textwrap.dedent(text).strip().replace('\n', '; ')
+ prelude = _inline("""
+ import sys
+ sys.__plen = len(sys.path)
+ """)
+ postlude = _inline("""
+ 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', 'rewrite') == 'rewrite':
+ PthDistributions = RewritePthDistributions
+
+
def _first_line_re():
"""
Return a regular expression based on first_line_re suitable for matching
@@ -1594,33 +1636,6 @@ def _first_line_re():
return re.compile(first_line_re.pattern.decode())
-def get_script_header(script_text, executable=sys_executable, wininst=False):
- """Create a #! line, getting options (if any) from script_text"""
- first = (script_text + '\n').splitlines()[0]
- match = _first_line_re().match(first)
- options = ''
- if match:
- options = match.group(1) or ''
- if options:
- options = ' ' + options
- if wininst:
- executable = "python.exe"
- else:
- executable = nt_quote_arg(executable)
- hdr = "#!%(executable)s%(options)s\n" % locals()
- if not isascii(hdr):
- # Non-ascii path to sys.executable, use -x to prevent warnings
- if options:
- if options.strip().startswith('-'):
- options = ' -x' + options.strip()[1:]
- # else: punt, we can't do it, let the warning happen anyway
- else:
- options = ' -x'
- executable = fix_jython_executable(executable, options)
- hdr = "#!%(executable)s%(options)s\n" % locals()
- return hdr
-
-
def auto_chmod(func, arg, exc):
if func is os.remove and os.name == 'nt':
chmod(arg, stat.S_IWRITE)
@@ -1819,9 +1834,8 @@ def is_python(text, filename='<string>'):
def is_sh(executable):
"""Determine if the specified executable is a .sh (contains a #! line)"""
try:
- fp = open(executable)
- magic = fp.read(2)
- fp.close()
+ with io.open(executable, encoding='latin-1') as fp:
+ magic = fp.read(2)
except (OSError, IOError):
return executable
return magic == '#!'
@@ -1829,36 +1843,7 @@ def is_sh(executable):
def nt_quote_arg(arg):
"""Quote a command line argument according to Windows parsing rules"""
-
- result = []
- needquote = False
- nb = 0
-
- needquote = (" " in arg) or ("\t" in arg)
- if needquote:
- result.append('"')
-
- for c in arg:
- if c == '\\':
- nb += 1
- elif c == '"':
- # double preceding backslashes, then add a \"
- result.append('\\' * (nb * 2) + '\\"')
- nb = 0
- else:
- if nb:
- result.append('\\' * nb)
- nb = 0
- result.append(c)
-
- if nb:
- result.append('\\' * nb)
-
- if needquote:
- result.append('\\' * nb) # double the trailing backslashes
- result.append('"')
-
- return ''.join(result)
+ return subprocess.list2cmdline([arg])
def is_python_script(script_text, filename):
@@ -1887,31 +1872,130 @@ def chmod(path, mode):
log.debug("changing mode of %s to %o", path, mode)
try:
_chmod(path, mode)
- except os.error:
- e = sys.exc_info()[1]
+ except os.error as e:
log.debug("chmod failed: %s", e)
def fix_jython_executable(executable, options):
- if sys.platform.startswith('java') and is_sh(executable):
- # Workaround for Jython is not needed on Linux systems.
- import java
+ warnings.warn("Use JythonCommandSpec", DeprecationWarning, stacklevel=2)
+
+ if not JythonCommandSpec.relevant():
+ return executable
+
+ cmd = CommandSpec.best().from_param(executable)
+ cmd.install_options(options)
+ return cmd.as_header().lstrip('#!').rstrip('\n')
+
+
+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()
+
+ @classmethod
+ def best(cls):
+ """
+ Choose the best CommandSpec class based on environmental conditions.
+ """
+ return cls if not JythonCommandSpec.relevant() else JythonCommandSpec
- if java.lang.System.getProperty("os.name") == "Linux":
- return executable
+ @classmethod
+ def _sys_executable(cls):
+ _default = os.path.normpath(sys.executable)
+ return os.environ.get('__PYVENV_LAUNCHER__', _default)
+
+ @classmethod
+ 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)
+
+ @classmethod
+ def from_environment(cls):
+ return cls([cls._sys_executable()])
+
+ @classmethod
+ 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']
+
+ @staticmethod
+ 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))
+
+ @staticmethod
+ def _render(items):
+ cmdline = subprocess.list2cmdline(items)
+ return '#!' + cmdline + '\n'
- # Workaround Jython's sys.executable being a .sh (an invalid
- # shebang line interpreter)
- if options:
+# For pbr compat; will be removed in a future version.
+sys_executable = CommandSpec._sys_executable()
+
+
+class WindowsCommandSpec(CommandSpec):
+ split_args = dict(posix=False)
+
+
+class JythonCommandSpec(CommandSpec):
+ @classmethod
+ def relevant(cls):
+ return (
+ sys.platform.startswith('java')
+ and
+ __import__('java').lang.System.getProperty('os.name') != 'Linux'
+ )
+
+ def as_header(self):
+ """
+ Workaround Jython's sys.executable being a .sh (an invalid
+ shebang line interpreter)
+ """
+ if not is_sh(self[0]):
+ return super(JythonCommandSpec, self).as_header()
+
+ if self.options:
# Can't apply the workaround, leave it broken
log.warn(
"WARNING: Unable to adapt shebang line for Jython,"
" the following script is NOT executable\n"
" see http://bugs.jython.org/issue1112 for"
" more information.")
- else:
- return '/usr/bin/env %s' % executable
- return executable
+ return super(JythonCommandSpec, self).as_header()
+
+ items = ['/usr/bin/env'] + self + list(self.options)
+ return self._render(items)
class ScriptWriter(object):
@@ -1932,39 +2016,92 @@ class ScriptWriter(object):
)
""").lstrip()
+ command_spec_class = CommandSpec
+
+ @classmethod
+ def get_script_args(cls, dist, executable=None, wininst=False):
+ # for backward compatibility
+ warnings.warn("Use get_args", DeprecationWarning)
+ writer = (WindowsScriptWriter if wininst else ScriptWriter).best()
+ header = cls.get_script_header("", executable, wininst)
+ return writer.get_args(dist, header)
+
@classmethod
- def get_script_args(cls, dist, executable=sys_executable, wininst=False):
+ def get_script_header(cls, script_text, executable=None, wininst=False):
+ # for backward compatibility
+ warnings.warn("Use get_header", DeprecationWarning)
+ if wininst:
+ executable = "python.exe"
+ cmd = cls.command_spec_class.best().from_param(executable)
+ cmd.install_options(script_text)
+ return cmd.as_header()
+
+ @classmethod
+ def get_args(cls, dist, header=None):
"""
- Yield write_script() argument tuples for a distribution's entrypoints
+ Yield write_script() argument tuples for a distribution's
+ console_scripts and gui_scripts entry points.
"""
- gen_class = cls.get_writer(wininst)
+ if header is None:
+ header = cls.get_header()
spec = str(dist.as_requirement())
- header = get_script_header("", executable, wininst)
for type_ in 'console', 'gui':
group = type_ + '_scripts'
for name, ep in dist.get_entry_map(group).items():
- script_text = gen_class.template % locals()
- for res in gen_class._get_script_args(type_, name, header,
- script_text):
+ 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
+ @staticmethod
+ 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")
+
@classmethod
def get_writer(cls, force_windows):
- if force_windows or sys.platform == 'win32':
- return WindowsScriptWriter.get_writer()
- return cls
+ # for backward compatibility
+ warnings.warn("Use best", DeprecationWarning)
+ return WindowsScriptWriter.best() if force_windows else cls.best()
+
+ @classmethod
+ def best(cls):
+ """
+ Select the best ScriptWriter for this environment.
+ """
+ return WindowsScriptWriter.best() if sys.platform == 'win32' else cls
@classmethod
def _get_script_args(cls, type_, name, header, script_text):
# Simply write the stub with no extension.
yield (name, header + script_text)
+ @classmethod
+ 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
+
@classmethod
def get_writer(cls):
+ # for backward compatibility
+ warnings.warn("Use best", DeprecationWarning)
+ return cls.best()
+
+ @classmethod
+ def best(cls):
"""
- Get a script writer suitable for Windows
+ Select the best ScriptWriter suitable for Windows
"""
writer_lookup = dict(
executable=WindowsExecutableLauncherWriter,
@@ -1987,8 +2124,8 @@ class WindowsScriptWriter(ScriptWriter):
blockers = [name + x for x in old]
yield name + ext, header + script_text, 't', blockers
- @staticmethod
- def _adjust_header(type_, orig_header):
+ @classmethod
+ def _adjust_header(cls, type_, orig_header):
"""
Make sure 'pythonw' is used for gui and and 'python' is used for
console (regardless of what sys.executable is).
@@ -1999,11 +2136,19 @@ class WindowsScriptWriter(ScriptWriter):
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
+
+ @staticmethod
+ 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('"')
- if sys.platform == 'win32' and not os.path.exists(clean_header):
- # the adjusted version doesn't exist, so return the original
- return orig_header
- return new_header
+ return sys.platform != 'win32' or find_executable(clean_header)
class WindowsExecutableLauncherWriter(WindowsScriptWriter):
@@ -2039,6 +2184,7 @@ class WindowsExecutableLauncherWriter(WindowsScriptWriter):
# for backward-compatibility
get_script_args = ScriptWriter.get_script_args
+get_script_header = ScriptWriter.get_script_header
def get_win_launcher(type):
@@ -2160,4 +2306,3 @@ def _patch_usage():
yield
finally:
distutils.core.gen_usage = saved
-