Compare commits

...
Sign in to create a new pull request.

13 commits

Author SHA1 Message Date
Miro Hrončok
e0d87e4172 Do not install the pip.whl with executable permissions
The wheel file is never executed, it has no shebang
(yes, zips can have shebangs).

    bash: /usr/share/python-wheels/pip-....whl: cannot execute binary file: Exec format error

This executable permission was never intended,
it only happens to be the default for the install command.
2026-08-31 12:45:22 +02:00
Lumir Balhar
ac655bc5de Security fix for CVE-2026-13346
Co-Authored-by: Miro Hrončok <miro@hroncok.cz>
2026-08-26 08:58:17 +02:00
Lukáš Zachar
08dc5bf450 CI: Make some test not running for ELN (and RHEL)
See also https://forge.fedoraproject.org/ci/tickets/issues/550

[skip changelog]
2026-08-25 14:23:39 +00:00
Miro Hrončok
c05c4a66bb Allow flit-core 4 for building this package 2026-08-20 20:16:00 +02:00
Fedora Release Engineering
47955c1bf6 Rebuilt for https://fedoraproject.org/wiki/Fedora_45_Mass_Rebuild 2026-07-16 20:49:58 +00:00
Lumir Balhar
9d73fb50fc Update to 26.1.2 (rhbz#2483638)
Co-Authored-By: Miro Hrončok <miro@hroncok.cz>
2026-07-03 11:23:30 +02:00
Python Maint
415ed181e5 Rebuilt for Python 3.15 2026-06-04 21:52:04 +02:00
Python Maint
3f4a4319ae Bump release 2026-06-03 13:59:55 +02:00
Python Maint
9583373c97 Bootstrap for Python 3.15 2026-06-03 13:47:16 +02:00
Miro Hrončok
c8ed4920c4 Never provide pip if this is not the build for the main Python
[skip changelog]
2026-05-23 22:17:15 +00:00
Lumir Balhar
5fd8a26f31 Remove Python 3.6 from CI configuration 2026-05-06 11:53:10 +02:00
Lumir Balhar
a5bfb663f2 Remove Python 3.9 from tests; pip no longer supports it since 26.1. 2026-05-06 11:53:10 +02:00
Lumir Balhar
3f3ab3fbad Update to 26.1.1 (rhbz#2466579) 2026-05-06 11:53:00 +02:00
9 changed files with 534 additions and 92 deletions

38
09a03f6cfa.patch Normal file
View file

@ -0,0 +1,38 @@
From 09a03f6cfaeecae8bf5774ae371d37c4369e2da4 Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Sat, 15 Aug 2026 13:36:05 -0400
Subject: [PATCH] Allow flit-core 4 to build pip
---
pyproject.toml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 48f64c8018..fe6b39913d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,13 +45,13 @@ Source = "https://github.com/pypa/pip"
Changelog = "https://pip.pypa.io/en/stable/news/"
[build-system]
-requires = ["flit-core >=3.11,<4"]
+requires = ["flit-core >=3.11,<5"]
build-backend = "flit_core.buildapi"
[dependency-groups]
test = [
"cryptography",
- "flit-core >= 3.11, < 4",
+ "flit-core >= 3.11, < 5",
"freezegun",
"installer",
# pytest-subket requires 7.0+
@@ -69,7 +69,7 @@ test = [
]
test-common-wheels = [
- "flit-core >= 3.11, < 4",
+ "flit-core >= 3.11, < 5",
# We pin setuptools<80 because our test suite currently
# depends on setup.py develop to generate egg-link files.
"setuptools >= 70.1.0, <80",

347
10dfb6b900.patch Normal file
View file

@ -0,0 +1,347 @@
From 10dfb6b9005484578b386f64b9f36982e3dc6679 Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Tue, 30 Jun 2026 21:52:39 -0400
Subject: [PATCH] Fix Link.filename decoding URL path twice (#14110)
Link already percent-decodes the URL path into `self._path`, but
`Link.filename` decoded the basename again, so a doubly-encoded
separator was decoded twice: `%252F` became `%2F` in `__init__`, then
`/` in `filename`, turning the single component `a%2Fb.whl` into
`a/b.whl`.
Drop the second decode, and add a `join_within_directory` helper so the
download-path joins treat the name as a single path component.
---
news/14110.bugfix.rst | 1 +
src/pip/_internal/models/link.py | 63 ++++++++++---
src/pip/_internal/network/download.py | 20 +++--
src/pip/_internal/operations/prepare.py | 6 +-
tests/unit/test_link.py | 113 +++++++++++++++++++++++-
5 files changed, 182 insertions(+), 21 deletions(-)
create mode 100644 news/14110.bugfix.rst
diff --git a/news/14110.bugfix.rst b/news/14110.bugfix.rst
new file mode 100644
index 0000000000..f7d4f78882
--- /dev/null
+++ b/news/14110.bugfix.rst
@@ -0,0 +1 @@
+Fix ``Link.filename`` decoding the URL path twice.
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index 0a09c66222..cbbe945c17 100644
--- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py
@@ -13,6 +13,7 @@
from typing import (
Any,
NamedTuple,
+ NewType,
)
from pip._internal.exceptions import InvalidEggFragment
@@ -30,6 +31,49 @@
logger = logging.getLogger(__name__)
+# A single path component: percent-decoded once and reduced to a basename, so it
+# contains no path separator and is not a ``.`` or ``..`` reference. The empty
+# string means "no component".
+PathComponent = NewType("PathComponent", str)
+
+
+def _to_path_component(name: str) -> PathComponent:
+ """Reduce ``name`` to a single path component, or ``""`` if it has none.
+
+ ``os.path.basename`` drops any directory part, drive letter, or separator;
+ a ``.``, ``..``, or empty result is not a component and becomes ``""``.
+ """
+ name = os.path.basename(name)
+ if name in ("", os.curdir, os.pardir):
+ return PathComponent("")
+
+ return PathComponent(name)
+
+
+def as_path_component(name: str) -> PathComponent:
+ """Like ``_to_path_component`` but reject the empty result.
+
+ Use where a file is about to be written, so a missing name is an error
+ rather than a silent fallback to the directory itself.
+ """
+ component = _to_path_component(name)
+ if not component:
+ raise ValueError(f"Unexpected file name derived from URL: {name!r}")
+
+ return component
+
+
+def join_within_directory(directory: str, component: PathComponent) -> str:
+ """Join a single path ``component`` onto ``directory``.
+
+ ``component`` is a :data:`PathComponent`, so by type it has no separator and
+ is not a ``.`` or ``..`` reference; the result can never escape ``directory``.
+ Requiring ``PathComponent`` rather than ``str`` lets the type checker enforce
+ at the call site that the name was reduced to a safe component beforehand.
+ """
+ return os.path.join(directory, component)
+
+
# Order matters, earlier hashes have a precedence over later hashes for what
# we will pick to use.
_SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5")
@@ -424,18 +468,13 @@ def redacted_url(self) -> str:
return redact_auth_from_url(self.url)
@property
- def filename(self) -> str:
- path = self.path.rstrip("/")
- name = posixpath.basename(path)
- if not name:
- # Make sure we don't leak auth information if the netloc
- # includes a username and password.
- netloc, user_pass = split_auth_from_netloc(self.netloc)
- return netloc
-
- name = urllib.parse.unquote(name)
- assert name, f"URL {self._url!r} produced no filename"
- return name
+ def filename(self) -> PathComponent:
+ name = _to_path_component(posixpath.basename(self.path.rstrip("/")))
+ if name:
+ return name
+
+ # No component in the path; fall back to the netloc, dropping any auth.
+ return _to_path_component(split_auth_from_netloc(self.netloc)[0])
@property
def file_path(self) -> str:
diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py
index 039b268878..6faafb5cb0 100644
--- a/src/pip/_internal/network/download.py
+++ b/src/pip/_internal/network/download.py
@@ -19,7 +19,12 @@
from pip._internal.cli.progress_bars import BarType, get_download_progress_renderer
from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError
-from pip._internal.models.link import Link
+from pip._internal.models.link import (
+ Link,
+ PathComponent,
+ as_path_component,
+ join_within_directory,
+)
from pip._internal.network.cache import SafeFileCache, is_from_cache
from pip._internal.network.session import CacheControlAdapter, PipSession
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
@@ -121,11 +126,14 @@ def parse_content_disposition(content_disposition: str, default_filename: str) -
return filename or default_filename
-def _get_http_response_filename(resp: Response, link: Link) -> str:
+def _get_http_response_filename(resp: Response, link: Link) -> PathComponent:
"""Get an ideal filename from the given HTTP response, falling back to
the link filename if not provided.
+
+ The result is validated as a single path component, so it can be joined onto
+ a download directory without escaping it.
"""
- filename = link.filename # fallback
+ filename: str = link.filename # fallback
# Have a look at the Content-Disposition header for a better guess
content_disposition = resp.headers.get("content-disposition")
if content_disposition:
@@ -139,7 +147,7 @@ def _get_http_response_filename(resp: Response, link: Link) -> str:
ext = os.path.splitext(resp.url)[1]
if ext:
filename += ext
- return filename
+ return as_path_component(filename)
@dataclass
@@ -192,7 +200,9 @@ def __call__(self, link: Link, location: str) -> tuple[str, str]:
resp = self._http_get(link)
download_size = _get_http_response_size(resp)
- filepath = os.path.join(location, _get_http_response_filename(resp, link))
+ filepath = join_within_directory(
+ location, _get_http_response_filename(resp, link)
+ )
with open(filepath, "wb") as content_file:
download = _FileDownload(link, content_file, download_size)
self._process_response(download, resp)
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index afcc0376da..3b44403e0d 100644
--- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py
@@ -29,7 +29,7 @@
from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_metadata_distribution
from pip._internal.models.direct_url import ArchiveInfo, DirectUrl
-from pip._internal.models.link import Link
+from pip._internal.models.link import Link, join_within_directory
from pip._internal.models.wheel import Wheel
from pip._internal.network.download import Downloader
from pip._internal.network.lazy_wheel import (
@@ -201,7 +201,7 @@ def _check_download_dir(
"""Check download_dir for previously downloaded file with correct hash
If a correct file is found return its path else None
"""
- download_path = os.path.join(download_dir, link.filename)
+ download_path = join_within_directory(download_dir, link.filename)
if not os.path.exists(download_path):
return None
@@ -687,7 +687,7 @@ def save_linked_requirement(self, req: InstallRequirement) -> None:
# No distribution was downloaded for this requirement.
return
- download_location = os.path.join(self.download_dir, link.filename)
+ download_location = join_within_directory(self.download_dir, link.filename)
if not os.path.exists(download_location):
shutil.copy(req.local_file_path, download_location)
download_path = display_path(download_location)
diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py
index c49f8547ac..bc8cb8ab9b 100644
--- a/tests/unit/test_link.py
+++ b/tests/unit/test_link.py
@@ -1,9 +1,17 @@
from __future__ import annotations
+import os
+import posixpath
+
import pytest
from pip._internal.exceptions import InvalidEggFragment, PipError
-from pip._internal.models.link import Link, links_equivalent
+from pip._internal.models.link import (
+ Link,
+ as_path_component,
+ join_within_directory,
+ links_equivalent,
+)
from pip._internal.utils.hashes import Hashes
@@ -29,6 +37,13 @@ def test_repr(self, url: str, expected: str) -> None:
("https://example.com/path/page.html", "page.html"),
# Test a quoted character.
("https://example.com/path/page%231.html", "page#1.html"),
+ # A doubly-encoded separator must stay encoded: the path is decoded
+ # exactly once, so the file name keeps its literal "%2F" instead of
+ # collapsing into a "/".
+ (
+ "https://example.com/a%252Fb.whl",
+ "a%2Fb.whl",
+ ),
(
"http://yo/myproject-1.0%2Bfoobar.0-py2.py3-none-any.whl",
"myproject-1.0+foobar.0-py2.py3-none-any.whl",
@@ -49,6 +64,52 @@ def test_filename(self, url: str, expected: str) -> None:
link = Link(url)
assert link.filename == expected
+ @pytest.mark.parametrize(
+ "url",
+ [
+ "https://example.com/a%252Fb.whl",
+ "https://example.com/%252e%252e%252fb.whl",
+ ],
+ )
+ def test_filename_decoded_once_stays_single_component(self, url: str) -> None:
+ # The path is decoded exactly once, so an encoded separator stays
+ # encoded and the file name remains a single path component rather
+ # than collapsing into a "/"-separated path.
+ filename = Link(url).filename
+ assert not posixpath.isabs(filename)
+ assert posixpath.basename(filename) == filename
+
+ @pytest.mark.parametrize(
+ "url",
+ [
+ "https://example.com/..",
+ "https://example.com/.",
+ "https://example.com/foo/%2e%2e",
+ ],
+ )
+ def test_filename_parent_reference_falls_back_to_netloc(self, url: str) -> None:
+ # A path that is only a "." or ".." reference has no usable file name,
+ # so filename falls back to the netloc rather than handing back a
+ # traversal component that could escape a download directory.
+ assert Link(url).filename == "example.com"
+
+ @pytest.mark.parametrize(
+ "url",
+ [
+ # A path-less URL whose authority looks like a traversal: the netloc
+ # fallback must still reduce to a single path component.
+ "http://..\\..\\..\\evil.whl",
+ "http://../",
+ "http://..",
+ ],
+ )
+ def test_filename_is_always_a_path_component(self, url: str) -> None:
+ # filename must never carry a separator or parent reference, so joining
+ # it onto a directory can never escape that directory.
+ name = Link(url).filename
+ assert os.path.basename(name) == name
+ assert name not in (os.curdir, os.pardir)
+
def test_splitext(self) -> None:
assert ("wheel", ".whl") == Link("http://yo/wheel.whl").splitext()
@@ -244,3 +305,53 @@ def test_links_equivalent(url1: str, url2: str) -> None:
)
def test_links_equivalent_false(url1: str, url2: str) -> None:
assert not links_equivalent(Link(url1), Link(url2))
+
+
+@pytest.mark.parametrize(
+ "name",
+ [
+ "wheel.whl",
+ "myproject-1.0+foobar.0-py2.py3-none-any.whl",
+ # A literal "%2F" is a normal file name, not a separator.
+ "a%2Fb.whl",
+ ],
+)
+def test_as_path_component_keeps_plain_name(name: str) -> None:
+ assert as_path_component(name) == name
+
+
+@pytest.mark.parametrize(
+ "name",
+ [
+ os.path.join(os.sep, "abs", "pkg.whl"),
+ os.path.join("..", "pkg.whl"),
+ os.path.join("nested", "pkg.whl"),
+ ],
+)
+def test_as_path_component_reduces_to_basename(name: str) -> None:
+ # A name carrying directory components is reduced to its basename, so the
+ # result always stays inside the directory it is later joined onto.
+ assert as_path_component(name) == os.path.basename(name)
+
+
+@pytest.mark.parametrize("name", ["", ".", "..", "/", os.path.join("sub", "..")])
+def test_as_path_component_rejects_empty_or_parent_reference(name: str) -> None:
+ with pytest.raises(ValueError):
+ as_path_component(name)
+
+
+@pytest.mark.parametrize(
+ "name",
+ [
+ "pkg.whl",
+ # A literal "%2F" is a normal file name, not a separator.
+ "a%2Fb.whl",
+ ],
+)
+def test_join_within_directory_stays_inside(name: str) -> None:
+ # The component is joined onto the directory as its final element, so the
+ # result stays inside the directory.
+ directory = os.path.join("base", "downloads")
+ joined = join_within_directory(directory, as_path_component(name))
+ assert joined == os.path.join(directory, name)
+ assert os.path.basename(joined) == name

29
4c6d7471de.patch Normal file
View file

@ -0,0 +1,29 @@
From 4c6d7471dec62fb004a47a7c2164b6b5b089ac06 Mon Sep 17 00:00:00 2001
From: Richard Si <sichard26@gmail.com>
Date: Fri, 5 Jun 2026 15:44:14 -0400
Subject: [PATCH] Also fix user site patching in test suite
---
tests/lib/venv.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/tests/lib/venv.py b/tests/lib/venv.py
index 67b01d9f31..4e86b92b3b 100644
--- a/tests/lib/venv.py
+++ b/tests/lib/venv.py
@@ -174,11 +174,13 @@ def _customize_site(self) -> None:
site.ENABLE_USER_SITE = {self._user_site_packages}
# First, drop system-sites related paths.
original_sys_path = sys.path[:]
+ # To discover system-sites related paths, clear sys.path
+ # and build a new one with only system paths.
+ sys.path = []
known_paths = set()
for path in site.getsitepackages():
site.addsitedir(path, known_paths=known_paths)
- system_paths = sys.path[len(original_sys_path):]
- for path in system_paths:
+ for path in sys.path:
if path in original_sys_path:
original_sys_path.remove(path)
sys.path = original_sys_path

62
6099a54ddd.patch Normal file
View file

@ -0,0 +1,62 @@
From 6099a54dddbfbc7fb912d53b6adad5ff6b8d1745 Mon Sep 17 00:00:00 2001
From: Richard Si <sichard26@gmail.com>
Date: Fri, 5 Jun 2026 15:03:38 -0400
Subject: [PATCH] Fix sitecustomize.py used for build isolation on Python 3.15+
The sitecustomize.py file pip uses to isolate build subprocesses from
the parent environment discovers system related paths by calling
site.addsitedir() for every system site-packages path and observing
what new entries are appended to sys.path.
This breaks since Python 3.15b2 due to two changes:
- site.addsitedir() won't add a path if it already exists in sys.path
- site.addsitedir() won't re-execute .pth files if called for a known
directory (which includes the system sites because known_path is
mutated by addsitedir before it checks for .pth files)
To cope with this, temporarily clear sys.path before using
site.addsitedir() to discover all system paths for exclusion.
---
news/14033.bugfix.rst | 1 +
src/pip/_internal/build_env.py | 14 +++++++++-----
2 files changed, 10 insertions(+), 5 deletions(-)
create mode 100644 news/14033.bugfix.rst
diff --git a/news/14033.bugfix.rst b/news/14033.bugfix.rst
new file mode 100644
index 0000000000..404196a2f0
--- /dev/null
+++ b/news/14033.bugfix.rst
@@ -0,0 +1 @@
+Prevent system packages from leaking into isolated build environments on Python 3.15
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 1a42a9d411..7639dabcad 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -468,15 +468,19 @@ def __init__(self, installer: BuildEnvironmentInstaller) -> None:
"""
import os, site, sys
- # First, drop system-sites related paths.
+ # First, discover all system-sites related paths.
original_sys_path = sys.path[:]
+ # Clear sys.path so addsitedir() will add system site paths and paths
+ # added by contained .pth files to sys.path reliably. This is necessary
+ # since Python 3.15, which notably no longer re-executes .pth files for
+ # known paths.
+ sys.path = []
known_paths = set()
for path in {system_sites!r}:
site.addsitedir(path, known_paths=known_paths)
- system_paths = set(
- os.path.normcase(path)
- for path in sys.path[len(original_sys_path):]
- )
+ system_paths = set(os.path.normcase(path) for path in sys.path)
+
+ # Drop discovered system-sites related paths.
original_sys_path = [
path for path in original_sys_path
if os.path.normcase(path) not in system_paths

View file

@ -6,12 +6,17 @@ discover:
how: shell
url: https://src.fedoraproject.org/tests/python.git
tests:
- name: smoke36
- name: smoke
path: /smoke
test: VERSION=3.6 TOX=false ./venv.sh
- name: smoke39
test: ./venv.sh
- name: smoke_virtualenv
path: /smoke
test: VERSION=3.9 ./venv.sh
test: METHOD=virtualenv ./venv.sh
- name: tests_python_fedora_only
how: shell
url: https://src.fedoraproject.org/tests/python.git
when: distro != fedora-eln and distro == fedora
tests:
- name: smoke310
path: /smoke
test: VERSION=3.10 ./venv.sh
@ -30,9 +35,6 @@ discover:
- name: smoke315
path: /smoke
test: VERSION=3.15 ./venv.sh
- name: smoke39_virtualenv
path: /smoke
test: VERSION=3.9 METHOD=virtualenv ./venv.sh
- name: smoke310_virtualenv
path: /smoke
test: VERSION=3.10 METHOD=virtualenv ./venv.sh
@ -88,14 +90,6 @@ prepare:
package:
- gcc
- virtualenv
- python3.6-devel
- python3.9-devel
- python3.10-devel
- python3.11-devel
- python3.12-devel
- python3.13-devel
- python3.14-devel
- python3.15-devel
- python3-devel
- python3-tox
- mock
@ -106,6 +100,16 @@ prepare:
- shadow-utils
- expect
- dnf
- name: Install dependencies (Fedora not ELN)
how: install
when: distro != fedora-eln and distro == fedora
package:
- python3.10-devel
- python3.11-devel
- python3.12-devel
- python3.13-devel
- python3.14-devel
- python3.15-devel
- name: Update packages
how: shell
script: dnf upgrade -y

View file

@ -6,7 +6,7 @@
%bcond man 1
%global srcname pip
%global base_version 26.0.1
%global base_version 26.1.2
%global upstream_version %{base_version}%{?prerel}
%global python_wheel_name %{srcname}-%{upstream_version}-py3-none-any.whl
@ -20,7 +20,6 @@ Summary: A tool for installing and managing Python packages
# certifi: MPL-2.0
# CacheControl: Apache-2.0
# dependency-groups: MIT
# distlib: Python-2.0.1
# distro: Apache-2.0
# idna: BSD-3-Clause
@ -96,10 +95,24 @@ Patch: dummy-certifi.patch
# We don't need a layer to check that, as we're by default in an offline environment
Patch: downstream-remove-pytest-subket.patch
# Patch for the bundled urllib3 for CVE-2025-50181
# Redirects are not disabled when retries are disabled on PoolManager instantiation
# Upstream fix: https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857
Patch: urllib3-CVE-2025-50181.patch
# Fix sitecustomize.py used for build isolation on Python 3.15+
Patch: https://github.com/pypa/pip/commit/6099a54ddd.patch
# Fix user-site path ordering in the test suite on Python 3.15+
# The same CPython gh-149819 change that broke build env isolation also broke
# _customize_site() in tests/lib/venv.py: site.addsitedir() no longer
# re-appends paths already in sys.path, so the detection of system-site paths
# produces an empty list and user site ends up after venv site-packages instead
# of before it, causing user-site install/uninstall tests to operate on the
# wrong installation.
Patch: https://github.com/pypa/pip/commit/4c6d7471de.patch
# Allow flit-core 4 to build pip
# https://github.com/pypa/pip/commit/09a03f6cfa (non-existing files removed)
Patch: 09a03f6cfa.patch
# CVE-2026-13346: Link.filename double URL decode allows path traversal
Patch: https://github.com/pypa/pip/commit/10dfb6b900.patch
# Remove -s from Python shebang - ensure that packages installed with pip
# to user locations are seen by pip itself
@ -118,24 +131,23 @@ Packages" or "Pip Installs Python".
# %%{_rpmconfigdir}/pythonbundles.py --namespace 'python%%{1}dist' src/pip/_vendor/vendor.txt
%global bundled() %{expand:
Provides: bundled(python%{1}dist(cachecontrol)) = 0.14.4
Provides: bundled(python%{1}dist(certifi)) = 2026.1.4
Provides: bundled(python%{1}dist(dependency-groups)) = 1.3.1
Provides: bundled(python%{1}dist(certifi)) = 2026.2.25
Provides: bundled(python%{1}dist(distlib)) = 0.4
Provides: bundled(python%{1}dist(distro)) = 1.9
Provides: bundled(python%{1}dist(idna)) = 3.11
Provides: bundled(python%{1}dist(msgpack)) = 1.1.2
Provides: bundled(python%{1}dist(packaging)) = 26
Provides: bundled(python%{1}dist(packaging)) = 26.2
Provides: bundled(python%{1}dist(platformdirs)) = 4.5.1
Provides: bundled(python%{1}dist(pygments)) = 2.19.2
Provides: bundled(python%{1}dist(pyproject-hooks)) = 1.2
Provides: bundled(python%{1}dist(requests)) = 2.32.5
Provides: bundled(python%{1}dist(requests)) = 2.33.1
Provides: bundled(python%{1}dist(resolvelib)) = 1.2.1
Provides: bundled(python%{1}dist(rich)) = 14.2
Provides: bundled(python%{1}dist(setuptools)) = 70.3
Provides: bundled(python%{1}dist(tomli)) = 2.3
Provides: bundled(python%{1}dist(tomli)) = 2.3.1
Provides: bundled(python%{1}dist(tomli-w)) = 1.2
Provides: bundled(python%{1}dist(truststore)) = 0.10.4
Provides: bundled(python%{1}dist(urllib3)) = 1.26.20
Provides: bundled(python%{1}dist(urllib3)) = 2.6.3
}
# Some manylinux1 wheels need libcrypt.so.1.
@ -177,7 +189,9 @@ Requires: ca-certificates
# Virtual provides for the packages bundled by pip:
%{bundled %{python3_pkgversion}}
%if "%{python3_pkgversion}" == "3"
Provides: pip = %{version}-%{release}
%endif
%description -n python%{python3_pkgversion}-%{srcname}
pip is a package management system used to install and manage software packages
@ -291,7 +305,7 @@ sed -i -e "s/^\\(complete.*\\) pip%{python3_version}\$/\\1 pip%{python3_version}
# Install the built wheel and inject SBOM into it (if the macro is available)
mkdir -p %{buildroot}%{python_wheel_dir}
install -p %{_pyproject_wheeldir}/%{python_wheel_name} -t %{buildroot}%{python_wheel_dir}
install -pm0644 %{_pyproject_wheeldir}/%{python_wheel_name} -t %{buildroot}%{python_wheel_dir}
%{?python_wheel_inject_sbom:%python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/%{python_wheel_name}}
@ -314,9 +328,8 @@ grep "pem$" %{pyproject_files} && exit 1 || true
pytest_k='not completion'
# this clashes with our PYTHONPATH
pytest_k="$pytest_k and not environments_with_no_pip"
# this seems to require internet (despite no network marker)
# added in https://github.com/pypa/pip/pull/13378 TODO drop this in the next release
pytest_k="$pytest_k and not test_prompt_for_keyring_if_needed and not test_double_install_fail and not test_install_sdist_links and not test_lock_vcs and not test_lock_archive and not test_backend_sees_config_via_sdist"
# this requires internet without the keyring local wheel
pytest_k="$pytest_k and not test_prompt_for_keyring_if_needed"
# this cannot import breezy, TODO investigate
pytest_k="$pytest_k and not (functional and bazaar)"
# failures to investigate

View file

@ -1,4 +1,4 @@
From d5cb806a14bb50a96484bca6536e81f2ac316b9d Mon Sep 17 00:00:00 2001
From a12217cf8f9bf3ab8e39d9bcc6b42e7043e79b08 Mon Sep 17 00:00:00 2001
From: Karolina Surma <ksurma@redhat.com>
Date: Wed, 16 Feb 2022 08:36:21 +0100
Subject: [PATCH] Prevent removing of the system packages installed under
@ -53,10 +53,10 @@ index 230e114..8bd5d31 100644
class BaseEnvironment:
"""An environment containing distributions to introspect."""
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index c9f6bff..c101826 100644
index aad2f0e..7b385b1 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -453,7 +453,7 @@ class InstallRequirement:
@@ -439,7 +439,7 @@ class InstallRequirement:
f"lack sys.path precedence to {existing_dist.raw_name} "
f"in {existing_dist.location}"
)
@ -66,7 +66,7 @@ index c9f6bff..c101826 100644
else:
if self.editable:
diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py
index 33a4fdc..1fe886e 100644
index 6cc6311..4188ef1 100644
--- a/src/pip/_internal/resolution/legacy/resolver.py
+++ b/src/pip/_internal/resolution/legacy/resolver.py
@@ -322,7 +322,9 @@ class Resolver(BaseResolver):
@ -81,11 +81,11 @@ index 33a4fdc..1fe886e 100644
req.satisfied_by = None
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index f23e4cd..1bada79 100644
index a74200a..99738cc 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -3,6 +3,8 @@ from __future__ import annotations
import contextlib
@@ -4,6 +4,8 @@ import contextlib
import copy
import functools
import logging
+import sys
@ -93,7 +93,7 @@ index f23e4cd..1bada79 100644
from collections.abc import Iterable, Iterator, Mapping, Sequence
from typing import (
TYPE_CHECKING,
@@ -615,6 +617,16 @@ class Factory:
@@ -674,6 +676,16 @@ class Factory:
if dist is None: # Not installed, no uninstallation required.
return None
@ -111,5 +111,5 @@ index f23e4cd..1bada79 100644
# be uninstalled, no matter it's in global or user site, because the
# user site installation has precedence over global.
--
2.50.1
2.54.0

View file

@ -1,4 +1,4 @@
SHA512 (pip-26.0.1.tar.gz) = 3786df7522ea65bc20fb9885ce5c2ddc60200a536a1f754a8d7dc278115c73258863e4c51ac7e9a60dda0b70263730dc194f70e1e8f8d00178a8b3c724333bf0
SHA512 (setuptools-79.0.1-py3-none-any.whl) = fef6cfc6f95a5bb7320f1680e1c665cb8d9a4e4227cde4d8aab8a50bed4bcf04320085b9d7d5343359f887008db5c5a861e57f3d08b7b0b2311a28adaeee6b4a
SHA512 (flit_core-3.12.0-py3-none-any.whl) = 790c12b1f43201e365fb3f8f2f0a54e1a578876799dfdf8bfeea679a25ea096bf62946d006618c1458ae6e37ce6d00998f37e9aba426d5ab80d32ef2d75da4e0
SHA512 (pip-26.1.2.tar.gz) = e29c98a7da5e329183b7eef86a66f9d6c3473051f64aa6e762714306148547eb0de4220824484071822a9a62bd01a62a09ab16bba4c26e4b847bfc2609728608
SHA512 (coverage-0-py3-none-any.whl) = e734192565347010efe68f8ba600254259c9b647f3c553fd4e5d87b1d7f955cb15d6f7d807716f4a6415d239beed945fbec7210feaf502e9cc849c332845926e

View file

@ -1,51 +0,0 @@
From b3d543d7e16af844394316360ef1bf0b9d10f1b1 Mon Sep 17 00:00:00 2001
From: Illia Volochii <illia.volochii@gmail.com>
Date: Wed, 18 Jun 2025 16:25:01 +0300
Subject: [PATCH] Security fix for CVE-2025-50181
Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>
Co-authored-by: Quentin Pradet <quentin.pradet@gmail.com>
Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>
---
src/pip/_vendor/urllib3/poolmanager.py | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py
index fb51bf7..a8de7c6 100644
--- a/src/pip/_vendor/urllib3/poolmanager.py
+++ b/src/pip/_vendor/urllib3/poolmanager.py
@@ -170,6 +170,22 @@ class PoolManager(RequestMethods):
def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
RequestMethods.__init__(self, headers)
+ if "retries" in connection_pool_kw:
+ retries = connection_pool_kw["retries"]
+ if not isinstance(retries, Retry):
+ # When Retry is initialized, raise_on_redirect is based
+ # on a redirect boolean value.
+ # But requests made via a pool manager always set
+ # redirect to False, and raise_on_redirect always ends
+ # up being False consequently.
+ # Here we fix the issue by setting raise_on_redirect to
+ # a value needed by the pool manager without considering
+ # the redirect boolean.
+ raise_on_redirect = retries is not False
+ retries = Retry.from_int(retries, redirect=False)
+ retries.raise_on_redirect = raise_on_redirect
+ connection_pool_kw = connection_pool_kw.copy()
+ connection_pool_kw["retries"] = retries
self.connection_pool_kw = connection_pool_kw
self.pools = RecentlyUsedContainer(num_pools)
@@ -389,7 +405,7 @@ class PoolManager(RequestMethods):
kw["body"] = None
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
- retries = kw.get("retries")
+ retries = kw.get("retries", response.retries)
if not isinstance(retries, Retry):
retries = Retry.from_int(retries, redirect=redirect)
--
2.51.0