From 26ed0f547ff7ea2810c76b1a2b2af81481dace15 Mon Sep 17 00:00:00 2001 From: Xavier de Gaye Date: Mon, 16 Dec 2019 18:24:08 -0700 Subject: [PATCH 1/7] bpo-31046: ensurepip does not honour the value of $(prefix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When cross-compiling, the local Python interpreter that is used to run `ensurepip` may not have the same value of `sys.prefix` as the value of the 'prefix' variable that is set in the Makefile. With the following values used to install Python locally for a later copy to the files hierarchy owned by the 'termux' application on an Android device: DESTDIR=/tmp/android prefix=/data/data/com.termux/files/usr/local 'make install' causes ensurepip to install pip in $(DESTDIR)/usr/local instead of the expected $(DESTDIR)/$(prefix) where is installed the standard library. The attached patch fixes the problem. The patch was implemented assuming that pip uses distutils for the installation (note that setup.py also uses the --prefix option in the Makefile), but I know nothing about pip so forgive me if the patch is wrong and please just assume it is just a way to demonstrate the problem. Fixes: https://github.com/python/cpython/issues/75229 Fixes: https://bugs.python.org/issue31046 Co-authored-by: Pradyun Gedam Co-authored-by: Erlend E. Aasland Co-authored-by: Zackery Spytz References: https://github.com/python/cpython/pull/17634 Signed-off-by: Matěj Cepl --- Doc/library/ensurepip.rst | 14 +++++++++++-- Lib/ensurepip/__init__.py | 20 ++++++++++++++----- Lib/test/test_ensurepip.py | 15 ++++++++++++++ Makefile.pre.in | 4 ++-- .../2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst | 1 + 5 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst diff --git a/Doc/library/ensurepip.rst b/Doc/library/ensurepip.rst index fa102c4a080103..26cf1715f5d74b 100644 --- a/Doc/library/ensurepip.rst +++ b/Doc/library/ensurepip.rst @@ -61,7 +61,11 @@ is at least as recent as the one available in ``ensurepip``, pass the By default, ``pip`` is installed into the current virtual environment (if one is active) or into the system site packages (if there is no active virtual environment). The installation location can be controlled -through two additional command line options: +through some additional command line options: + +.. option:: --prefix + + Installs ``pip`` using the given directory prefix. .. option:: --root @@ -104,7 +108,7 @@ Module API .. function:: bootstrap(root=None, upgrade=False, user=False, \ altinstall=False, default_pip=False, \ - verbosity=0) + verbosity=0, prefix=None) Bootstraps ``pip`` into the current or designated environment. @@ -132,6 +136,12 @@ Module API *verbosity* controls the level of output to :data:`sys.stdout` from the bootstrapping operation. + *prefix* specifies the directory prefix to use when installing. + + .. versionadded:: 3.14 + + The *prefix* parameter. + .. audit-event:: ensurepip.bootstrap root ensurepip.bootstrap .. note:: diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 4bd85990e8614a..05b388a89d3f3b 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -108,28 +108,30 @@ def _disable_pip_configuration_settings(): def bootstrap(*, root=None, upgrade=False, user=False, altinstall=False, default_pip=False, - verbosity=0): + verbosity=0, prefix=None): """ Bootstrap pip into the current Python installation (or the given root - directory). + and directory prefix). Note that calling this function will alter both sys.path and os.environ. """ # Discard the return value _bootstrap(root=root, upgrade=upgrade, user=user, altinstall=altinstall, default_pip=default_pip, - verbosity=verbosity) + verbosity=verbosity, prefix=prefix) def _bootstrap(*, root=None, upgrade=False, user=False, altinstall=False, default_pip=False, - verbosity=0): + verbosity=0, prefix=None): """ Bootstrap pip into the current Python installation (or the given root - directory). Returns pip command status code. + and directory prefix). Returns pip command status code. Note that calling this function will alter both sys.path and os.environ. """ + if root is not None and prefix is not None: + raise ValueError("Cannot use 'root' and 'prefix' together") if altinstall and default_pip: raise ValueError("Cannot use altinstall and default_pip together") @@ -162,6 +164,8 @@ def _bootstrap(*, root=None, upgrade=False, user=False, args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir] if root: args += ["--root", root] + if prefix: + args += ["--prefix", prefix] if upgrade: args += ["--upgrade"] if user: @@ -237,6 +241,11 @@ def _main(argv=None): default=None, help="Install everything relative to this alternate root directory.", ) + parser.add_argument( + "--prefix", + default=None, + help="Install everything using this prefix.", + ) parser.add_argument( "--altinstall", action="store_true", @@ -256,6 +265,7 @@ def _main(argv=None): return _bootstrap( root=args.root, + prefix=args.prefix, upgrade=args.upgrade, user=args.user, verbosity=args.verbosity, diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index 6d3c91b0b6d9f9..cd858ff09f3e4d 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -100,6 +100,21 @@ def test_bootstrapping_with_root(self): unittest.mock.ANY, ) + def test_bootstrapping_with_prefix(self): + ensurepip.bootstrap(prefix="/foo/bar/") + self.run_pip.assert_called_once_with( + [ + "install", "--no-cache-dir", "--no-index", "--find-links", + unittest.mock.ANY, "--prefix", "/foo/bar/", "pip", + ], + unittest.mock.ANY, + ) + + def test_root_and_prefix_mutual_exclusive(self): + with self.assertRaises(ValueError): + ensurepip.bootstrap(root="", prefix="") + self.assertFalse(self.run_pip.called) + def test_bootstrapping_with_user(self): ensurepip.bootstrap(user=True) diff --git a/Makefile.pre.in b/Makefile.pre.in index eedccc3ffe6a49..9b2951c6f685ba 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2371,7 +2371,7 @@ install: @FRAMEWORKINSTALLFIRST@ @INSTALLTARGETS@ @FRAMEWORKINSTALLLAST@ install|*) ensurepip="" ;; \ esac; \ $(RUNSHARED) $(PYTHON_FOR_BUILD) -m ensurepip \ - $$ensurepip --root=$(DESTDIR)/ ; \ + $$ensurepip --prefix=$(prefix) ; \ fi .PHONY: altinstall @@ -2382,7 +2382,7 @@ altinstall: commoninstall install|*) ensurepip="--altinstall" ;; \ esac; \ $(RUNSHARED) $(PYTHON_FOR_BUILD) -m ensurepip \ - $$ensurepip --root=$(DESTDIR)/ ; \ + $$ensurepip --prefix=$(prefix) ; \ fi .PHONY: commoninstall diff --git a/Misc/NEWS.d/next/Library/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst b/Misc/NEWS.d/next/Library/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst new file mode 100644 index 00000000000000..07eb89d4d23e50 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-12-16-17-50-42.bpo-31046.XA-Qfr.rst @@ -0,0 +1 @@ +A directory prefix can now be specified when using :mod:`ensurepip`. From b5283e7d53716d9ef0f6acdabadea5b38160cdc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Fri, 13 Jun 2025 22:58:14 +0200 Subject: [PATCH 2/7] bpo-31046: Fix ensurepip script shebangs with --root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using `python -m ensurepip` with the `--root` option for staged installations, the generated pip script contained an incorrect shebang that pointed into the staging directory. This made the installation unusable once the staging directory was removed. This commit fixes the issue by using the internal pip `--executable` option to force the shebang to point to the correct, final interpreter path. It also corrects related pathing issues: - Removes the check that incorrectly disallowed using --root and --prefix together. - Defaults the installation prefix to `/` when --root is used alone, ensuring installation occurs at the base of the staging directory. References: https://github.com/python/cpython/pull/17634#discussion_r1622453325 Signed-off-by: Matěj Cepl --- Lib/ensurepip/__init__.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 05b388a89d3f3b..8b4b8616a8283a 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -130,8 +130,6 @@ def _bootstrap(*, root=None, upgrade=False, user=False, Note that calling this function will alter both sys.path and os.environ. """ - if root is not None and prefix is not None: - raise ValueError("Cannot use 'root' and 'prefix' together") if altinstall and default_pip: raise ValueError("Cannot use altinstall and default_pip together") @@ -162,17 +160,38 @@ def _bootstrap(*, root=None, upgrade=False, user=False, # Construct the arguments to be passed to the pip command args = ["install", "--no-cache-dir", "--no-index", "--find-links", tmpdir] - if root: - args += ["--root", root] - if prefix: - args += ["--prefix", prefix] if upgrade: args += ["--upgrade"] - if user: - args += ["--user"] if verbosity: args += ["-" + "v" * verbosity] +        if user: +            # --user is mutually exclusive with --root/--prefix, +            # pip will enforce this. +            args += ["--user"] +        else: +            # Handle installation paths. +            # If --root is given but not --prefix, we default to a prefix of "/" +            # so that the install happens at the root of the --root directory. +            # Otherwise, pip would use the configured sys.prefix, e.g. +            # /usr/local, and install into ${root}/usr/local/. +            effective_prefix = prefix +            if root and not prefix: +                effective_prefix = "/" + +            if root: +                args += ["--root", root] + +            if effective_prefix: +                args += ["--prefix", effective_prefix] + +                # Force the script shebang to point to the correct, final +                # executable path. This is necessary when --root is used. +                executable_path = ( +                    Path(effective_prefix) / "bin" / Path(sys.executable).name +                ) +                args += ["--executable", os.fsdecode(executable_path)] + return _run_pip([*args, "pip"], [os.fsdecode(tmp_wheel_path)]) From 066fb862537b59fa03596ee42389e54a89ba944c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 7 Jul 2025 18:40:15 +0200 Subject: [PATCH 3/7] don't throw away `--root` parameter of ensurepip Don't break the default installation on the current system with good `sys.prefix`. --- Makefile.pre.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.pre.in b/Makefile.pre.in index 9b2951c6f685ba..ce2b1233bacf7e 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -2371,7 +2371,7 @@ install: @FRAMEWORKINSTALLFIRST@ @INSTALLTARGETS@ @FRAMEWORKINSTALLLAST@ install|*) ensurepip="" ;; \ esac; \ $(RUNSHARED) $(PYTHON_FOR_BUILD) -m ensurepip \ - $$ensurepip --prefix=$(prefix) ; \ + $$ensurepip --root=$(DESTDIR)/ --prefix=$(prefix) ; \ fi .PHONY: altinstall @@ -2382,7 +2382,7 @@ altinstall: commoninstall install|*) ensurepip="--altinstall" ;; \ esac; \ $(RUNSHARED) $(PYTHON_FOR_BUILD) -m ensurepip \ - $$ensurepip --prefix=$(prefix) ; \ + $$ensurepip --root=$(DESTDIR)/ --prefix=$(prefix) ; \ fi .PHONY: commoninstall From bdf8b7b2d92e1d50dc2f4bab43bdc27acad41ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Tue, 8 Jul 2025 13:56:44 +0200 Subject: [PATCH 4/7] fix: remove test_root_and_prefix_mutual_exclusive It is made incorrect by the previous commit. --- Lib/test/test_ensurepip.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index cd858ff09f3e4d..bf41601698cb6c 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -110,11 +110,6 @@ def test_bootstrapping_with_prefix(self): unittest.mock.ANY, ) - def test_root_and_prefix_mutual_exclusive(self): - with self.assertRaises(ValueError): - ensurepip.bootstrap(root="", prefix="") - self.assertFalse(self.run_pip.called) - def test_bootstrapping_with_user(self): ensurepip.bootstrap(user=True) From e1d4b79a0dc86854d72ed96bff8123a78a085dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Fri, 25 Jul 2025 13:02:40 +0200 Subject: [PATCH 5/7] fix test.test_ensurepip.TestBootstrap.test_bootstrapping_with_root --- Lib/test/test_ensurepip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index bf41601698cb6c..5b08cb1ccca4a1 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -94,7 +94,7 @@ def test_bootstrapping_with_root(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--root", "/foo/bar/", + unittest.mock.ANY, "--root", "/foo/bar/", "--prefix", unittest.mock.ANY, "pip", ], unittest.mock.ANY, From 55e6606a5d57b26f5ffd20cf888e54f62cb5ac8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Fri, 25 Jul 2025 13:02:40 +0200 Subject: [PATCH 6/7] Remove   characters. --- Lib/ensurepip/__init__.py | 52 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py index 8b4b8616a8283a..053955b16aea88 100644 --- a/Lib/ensurepip/__init__.py +++ b/Lib/ensurepip/__init__.py @@ -165,32 +165,32 @@ def _bootstrap(*, root=None, upgrade=False, user=False, if verbosity: args += ["-" + "v" * verbosity] -        if user: -            # --user is mutually exclusive with --root/--prefix, -            # pip will enforce this. -            args += ["--user"] -        else: -            # Handle installation paths. -            # If --root is given but not --prefix, we default to a prefix of "/" -            # so that the install happens at the root of the --root directory. -            # Otherwise, pip would use the configured sys.prefix, e.g. -            # /usr/local, and install into ${root}/usr/local/. -            effective_prefix = prefix -            if root and not prefix: -                effective_prefix = "/" - -            if root: -                args += ["--root", root] - -            if effective_prefix: -                args += ["--prefix", effective_prefix] - -                # Force the script shebang to point to the correct, final -                # executable path. This is necessary when --root is used. -                executable_path = ( -                    Path(effective_prefix) / "bin" / Path(sys.executable).name -                ) -                args += ["--executable", os.fsdecode(executable_path)] + if user: + # --user is mutually exclusive with --root/--prefix, + # pip will enforce this. + args += ["--user"] + else: + # Handle installation paths. + # If --root is given but not --prefix, we default to a prefix of "/" + # so that the install happens at the root of the --root directory. + # Otherwise, pip would use the configured sys.prefix, e.g. + # /usr/local, and install into ${root}/usr/local/. + effective_prefix = prefix + if root and not prefix: + effective_prefix = "/" + + if root: + args += ["--root", root] + + if effective_prefix: + args += ["--prefix", effective_prefix] + + # Force the script shebang to point to the correct, final + # executable path. This is necessary when --root is used. + executable_path = ( + Path(effective_prefix) / "bin" / Path(sys.executable).name + ) + args += ["--executable", os.fsdecode(executable_path)] return _run_pip([*args, "pip"], [os.fsdecode(tmp_wheel_path)]) From 8e928a68b908606778108c83014618dd89982b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Thu, 7 Aug 2025 20:07:50 +0200 Subject: [PATCH 7/7] Fix ensurepip tests to match new --executable argument behavior Update test expectations in test_ensurepip.py to account for the new --executable argument that gets added when using --prefix or --root options. The implementation now correctly sets the executable path when installing with a custom prefix, but the tests weren't updated to expect this additional argument. - test_bootstrapping_with_root: expect --prefix "/" and --executable - test_bootstrapping_with_prefix: expect --executable argument --- Lib/test/test_ensurepip.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ensurepip.py b/Lib/test/test_ensurepip.py index 5b08cb1ccca4a1..4de0bf512e465f 100644 --- a/Lib/test/test_ensurepip.py +++ b/Lib/test/test_ensurepip.py @@ -94,7 +94,8 @@ def test_bootstrapping_with_root(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--root", "/foo/bar/", "--prefix", unittest.mock.ANY, + unittest.mock.ANY, "--root", "/foo/bar/", "--prefix", "/", + "--executable", unittest.mock.ANY, "pip", ], unittest.mock.ANY, @@ -105,7 +106,8 @@ def test_bootstrapping_with_prefix(self): self.run_pip.assert_called_once_with( [ "install", "--no-cache-dir", "--no-index", "--find-links", - unittest.mock.ANY, "--prefix", "/foo/bar/", "pip", + unittest.mock.ANY, "--prefix", "/foo/bar/", + "--executable", unittest.mock.ANY, "pip", ], unittest.mock.ANY, )