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