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