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