From f458012ea180b9995fca1abf2e61886f641061f6 Mon Sep 17 00:00:00 2001 From: Lalatendu Mohanty Date: Wed, 25 Mar 2026 17:51:52 -0400 Subject: [PATCH 1/2] fix(bootstrapper): drop extras from _resolved_key to prevent re-entrant bootstrap failure Extras were included in the seen-check key, so the same package with different extras (e.g. setuptools-scm vs setuptools-scm[toml]) bypassed cycle detection and tried to install dependencies whose wheels hadn't been built yet. Closes: #984 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Lalatendu Mohanty --- src/fromager/bootstrapper.py | 16 +++++++++------- tests/test_bootstrapper.py | 20 +++++++++++++++++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/fromager/bootstrapper.py b/src/fromager/bootstrapper.py index 11440b5d..9cac8de1 100644 --- a/src/fromager/bootstrapper.py +++ b/src/fromager/bootstrapper.py @@ -40,7 +40,7 @@ logger = logging.getLogger(__name__) # package name, extras, version, sdist/wheel -SeenKey = tuple[NormalizedName, tuple[str, ...], str, typing.Literal["sdist", "wheel"]] +SeenKey = tuple[NormalizedName, str, typing.Literal["sdist", "wheel"]] @dataclasses.dataclass @@ -1287,9 +1287,13 @@ def _sort_requirements( def _resolved_key( self, req: Requirement, version: Version, typ: typing.Literal["sdist", "wheel"] ) -> SeenKey: + """Return a key for tracking whether a package has already been resolved. + + Extras are intentionally excluded because a build is the same + regardless of extras. + """ return ( canonicalize_name(req.name), - tuple(sorted(req.extras)), str(version), typ, ) @@ -1331,11 +1335,9 @@ def _add_to_build_order( prebuilt: bool = False, constraint: Requirement | None = None, ) -> None: - # We only care if this version of this package has been built, - # and don't want to trigger building it twice. The "extras" - # value, included in the _resolved_key() output, can confuse - # that so we ignore itand build our own key using just the - # name and version. + # Deduplicate by (name, version) only. We don't distinguish + # sdist vs wheel here because a package should appear in the + # build order at most once per version. key = (canonicalize_name(req.name), str(version)) if key in self._build_requirements: return diff --git a/tests/test_bootstrapper.py b/tests/test_bootstrapper.py index 46b78c79..686921d2 100644 --- a/tests/test_bootstrapper.py +++ b/tests/test_bootstrapper.py @@ -20,6 +20,12 @@ def test_seen(tmp_context: WorkContext) -> None: def test_seen_extras(tmp_context: WorkContext) -> None: + """Verify that extras are ignored for seen-checks. + + A wheel build is the same regardless of extras, so + ``testdist`` and ``testdist[extra]`` must be treated as + the same package. + """ req1 = Requirement("testdist") req2 = Requirement("testdist[extra]") version = Version("1.2") @@ -27,11 +33,19 @@ def test_seen_extras(tmp_context: WorkContext) -> None: assert not bt._has_been_seen(req1, version) bt._mark_as_seen(req1, version) assert bt._has_been_seen(req1, version) - assert not bt._has_been_seen(req2, version) - bt._mark_as_seen(req2, version) - assert bt._has_been_seen(req1, version) + # Same package with extras should also be seen assert bt._has_been_seen(req2, version) + # Reverse: mark with extras, check without + bt2 = bootstrapper.Bootstrapper(tmp_context) + bt2._mark_as_seen(req2, version) + assert bt2._has_been_seen(req1, version) + + # Extras are also ignored for sdist_only path + bt3 = bootstrapper.Bootstrapper(tmp_context) + bt3._mark_as_seen(req1, version, sdist_only=True) + assert bt3._has_been_seen(req2, version, sdist_only=True) + def test_seen_name_canonicalization(tmp_context: WorkContext) -> None: req = Requirement("flit_core") From 344fc0119cf0c492c49ae5057aa00bc83d415334 Mon Sep 17 00:00:00 2001 From: Lalatendu Mohanty Date: Wed, 25 Mar 2026 18:43:07 -0400 Subject: [PATCH 2/2] revert(e2e): remove setuptools-scm<10 constraint The constraint was a workaround for the issue (commit 141d857df467d4a8d64d2cc164df7e823b921501), which was caused by extras in _resolved_key triggering re-entrant bootstrap failures. The root cause is now fixed by PR 985 [1], making this pin unnecessary. [1] https://github.com/python-wheel-build/fromager/pull/985 Reverts: #983 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Lalatendu Mohanty --- e2e/constraints.txt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/e2e/constraints.txt b/e2e/constraints.txt index 4ae9444e..e1132df9 100644 --- a/e2e/constraints.txt +++ b/e2e/constraints.txt @@ -1,9 +1,2 @@ # This file is here in case we need to quickly add a constraint to # fix CI jobs. - -# setuptools-scm 10.0.2 added a build-system dependency on -# vcs-versioning, which itself requires hatchling. This creates a -# circular bootstrap dependency that causes e2e tests to fail because -# hatchling's wheel is not yet available on the local wheel server -# when vcs-versioning tries to install it. See #982. -setuptools-scm<10