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