• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import collections
2import os
3import os.path
4import subprocess
5import sys
6import sysconfig
7import tempfile
8from importlib import resources
9
10
11
12__all__ = ["version", "bootstrap"]
13_PACKAGE_NAMES = ('setuptools', 'pip')
14_SETUPTOOLS_VERSION = "58.1.0"
15_PIP_VERSION = "21.2.4"
16_PROJECTS = [
17    ("setuptools", _SETUPTOOLS_VERSION, "py3"),
18    ("pip", _PIP_VERSION, "py3"),
19]
20
21# Packages bundled in ensurepip._bundled have wheel_name set.
22# Packages from WHEEL_PKG_DIR have wheel_path set.
23_Package = collections.namedtuple('Package',
24                                  ('version', 'wheel_name', 'wheel_path'))
25
26# Directory of system wheel packages. Some Linux distribution packaging
27# policies recommend against bundling dependencies. For example, Fedora
28# installs wheel packages in the /usr/share/python-wheels/ directory and don't
29# install the ensurepip._bundled package.
30_WHEEL_PKG_DIR = sysconfig.get_config_var('WHEEL_PKG_DIR')
31
32
33def _find_packages(path):
34    packages = {}
35    try:
36        filenames = os.listdir(path)
37    except OSError:
38        # Ignore: path doesn't exist or permission error
39        filenames = ()
40    # Make the code deterministic if a directory contains multiple wheel files
41    # of the same package, but don't attempt to implement correct version
42    # comparison since this case should not happen.
43    filenames = sorted(filenames)
44    for filename in filenames:
45        # filename is like 'pip-21.2.4-py3-none-any.whl'
46        if not filename.endswith(".whl"):
47            continue
48        for name in _PACKAGE_NAMES:
49            prefix = name + '-'
50            if filename.startswith(prefix):
51                break
52        else:
53            continue
54
55        # Extract '21.2.4' from 'pip-21.2.4-py3-none-any.whl'
56        version = filename.removeprefix(prefix).partition('-')[0]
57        wheel_path = os.path.join(path, filename)
58        packages[name] = _Package(version, None, wheel_path)
59    return packages
60
61
62def _get_packages():
63    global _PACKAGES, _WHEEL_PKG_DIR
64    if _PACKAGES is not None:
65        return _PACKAGES
66
67    packages = {}
68    for name, version, py_tag in _PROJECTS:
69        wheel_name = f"{name}-{version}-{py_tag}-none-any.whl"
70        packages[name] = _Package(version, wheel_name, None)
71    if _WHEEL_PKG_DIR:
72        dir_packages = _find_packages(_WHEEL_PKG_DIR)
73        # only used the wheel package directory if all packages are found there
74        if all(name in dir_packages for name in _PACKAGE_NAMES):
75            packages = dir_packages
76    _PACKAGES = packages
77    return packages
78_PACKAGES = None
79
80
81def _run_pip(args, additional_paths=None):
82    # Run the bootstraping in a subprocess to avoid leaking any state that happens
83    # after pip has executed. Particulary, this avoids the case when pip holds onto
84    # the files in *additional_paths*, preventing us to remove them at the end of the
85    # invocation.
86    code = f"""
87import runpy
88import sys
89sys.path = {additional_paths or []} + sys.path
90sys.argv[1:] = {args}
91runpy.run_module("pip", run_name="__main__", alter_sys=True)
92"""
93    return subprocess.run([sys.executable, '-W', 'ignore::DeprecationWarning',
94                           "-c", code], check=True).returncode
95
96
97def version():
98    """
99    Returns a string specifying the bundled version of pip.
100    """
101    return _get_packages()['pip'].version
102
103
104def _disable_pip_configuration_settings():
105    # We deliberately ignore all pip environment variables
106    # when invoking pip
107    # See http://bugs.python.org/issue19734 for details
108    keys_to_remove = [k for k in os.environ if k.startswith("PIP_")]
109    for k in keys_to_remove:
110        del os.environ[k]
111    # We also ignore the settings in the default pip configuration file
112    # See http://bugs.python.org/issue20053 for details
113    os.environ['PIP_CONFIG_FILE'] = os.devnull
114
115
116def bootstrap(*, root=None, upgrade=False, user=False,
117              altinstall=False, default_pip=False,
118              verbosity=0):
119    """
120    Bootstrap pip into the current Python installation (or the given root
121    directory).
122
123    Note that calling this function will alter both sys.path and os.environ.
124    """
125    # Discard the return value
126    _bootstrap(root=root, upgrade=upgrade, user=user,
127               altinstall=altinstall, default_pip=default_pip,
128               verbosity=verbosity)
129
130
131def _bootstrap(*, root=None, upgrade=False, user=False,
132              altinstall=False, default_pip=False,
133              verbosity=0):
134    """
135    Bootstrap pip into the current Python installation (or the given root
136    directory). Returns pip command status code.
137
138    Note that calling this function will alter both sys.path and os.environ.
139    """
140    if altinstall and default_pip:
141        raise ValueError("Cannot use altinstall and default_pip together")
142
143    sys.audit("ensurepip.bootstrap", root)
144
145    _disable_pip_configuration_settings()
146
147    # By default, installing pip and setuptools installs all of the
148    # following scripts (X.Y == running Python version):
149    #
150    #   pip, pipX, pipX.Y, easy_install, easy_install-X.Y
151    #
152    # pip 1.5+ allows ensurepip to request that some of those be left out
153    if altinstall:
154        # omit pip, pipX and easy_install
155        os.environ["ENSUREPIP_OPTIONS"] = "altinstall"
156    elif not default_pip:
157        # omit pip and easy_install
158        os.environ["ENSUREPIP_OPTIONS"] = "install"
159
160    with tempfile.TemporaryDirectory() as tmpdir:
161        # Put our bundled wheels into a temporary directory and construct the
162        # additional paths that need added to sys.path
163        additional_paths = []
164        for name, package in _get_packages().items():
165            if package.wheel_name:
166                # Use bundled wheel package
167                from ensurepip import _bundled
168                wheel_name = package.wheel_name
169                whl = resources.read_binary(_bundled, wheel_name)
170            else:
171                # Use the wheel package directory
172                with open(package.wheel_path, "rb") as fp:
173                    whl = fp.read()
174                wheel_name = os.path.basename(package.wheel_path)
175
176            filename = os.path.join(tmpdir, wheel_name)
177            with open(filename, "wb") as fp:
178                fp.write(whl)
179
180            additional_paths.append(filename)
181
182        # Construct the arguments to be passed to the pip command
183        args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir]
184        if root:
185            args += ["--root", root]
186        if upgrade:
187            args += ["--upgrade"]
188        if user:
189            args += ["--user"]
190        if verbosity:
191            args += ["-" + "v" * verbosity]
192
193        return _run_pip([*args, *_PACKAGE_NAMES], additional_paths)
194
195def _uninstall_helper(*, verbosity=0):
196    """Helper to support a clean default uninstall process on Windows
197
198    Note that calling this function may alter os.environ.
199    """
200    # Nothing to do if pip was never installed, or has been removed
201    try:
202        import pip
203    except ImportError:
204        return
205
206    # If the installed pip version doesn't match the available one,
207    # leave it alone
208    available_version = version()
209    if pip.__version__ != available_version:
210        print(f"ensurepip will only uninstall a matching version "
211              f"({pip.__version__!r} installed, "
212              f"{available_version!r} available)",
213              file=sys.stderr)
214        return
215
216    _disable_pip_configuration_settings()
217
218    # Construct the arguments to be passed to the pip command
219    args = ["uninstall", "-y", "--disable-pip-version-check"]
220    if verbosity:
221        args += ["-" + "v" * verbosity]
222
223    return _run_pip([*args, *reversed(_PACKAGE_NAMES)])
224
225
226def _main(argv=None):
227    import argparse
228    parser = argparse.ArgumentParser(prog="python -m ensurepip")
229    parser.add_argument(
230        "--version",
231        action="version",
232        version="pip {}".format(version()),
233        help="Show the version of pip that is bundled with this Python.",
234    )
235    parser.add_argument(
236        "-v", "--verbose",
237        action="count",
238        default=0,
239        dest="verbosity",
240        help=("Give more output. Option is additive, and can be used up to 3 "
241              "times."),
242    )
243    parser.add_argument(
244        "-U", "--upgrade",
245        action="store_true",
246        default=False,
247        help="Upgrade pip and dependencies, even if already installed.",
248    )
249    parser.add_argument(
250        "--user",
251        action="store_true",
252        default=False,
253        help="Install using the user scheme.",
254    )
255    parser.add_argument(
256        "--root",
257        default=None,
258        help="Install everything relative to this alternate root directory.",
259    )
260    parser.add_argument(
261        "--altinstall",
262        action="store_true",
263        default=False,
264        help=("Make an alternate install, installing only the X.Y versioned "
265              "scripts (Default: pipX, pipX.Y, easy_install-X.Y)."),
266    )
267    parser.add_argument(
268        "--default-pip",
269        action="store_true",
270        default=False,
271        help=("Make a default pip install, installing the unqualified pip "
272              "and easy_install in addition to the versioned scripts."),
273    )
274
275    args = parser.parse_args(argv)
276
277    return _bootstrap(
278        root=args.root,
279        upgrade=args.upgrade,
280        user=args.user,
281        verbosity=args.verbosity,
282        altinstall=args.altinstall,
283        default_pip=args.default_pip,
284    )
285