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