Skip to content

Commit 7f87cdb

Browse files
andre-mottaclaude
andcommitted
fix(resolver): resolve sdist URL via source provider in cache fallback
`_resolve_from_cache_server` returned a wheel URL as `source_url` when only wheels existed on the cache server, but `source_url_type` in build-order.json was set to "sdist" causing downstream failures. Now the method queries the cache server for both wheels and sdists to find the newest version, then re-resolves the sdist URL through `sources.get_source_provider()`. This uses the normal source resolution path including overrides (`get_resolver_provider` hook), custom download URLs, and `resolver_sdist_server_url` settings. If the source provider cannot find a sdist for the pinned version, an empty list is returned and the caller logs a warning. Closes: #1184 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Andre Lustosa <alustosa@redhat.com>
1 parent ed6da67 commit 7f87cdb

2 files changed

Lines changed: 107 additions & 21 deletions

File tree

src/fromager/bootstrap_requirement_resolver.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def resolve(
168168
"falling back to the cache server %s",
169169
self.cache_wheel_server_url,
170170
)
171-
results = self._resolve_from_cache_server(req)
171+
results = self._resolve_from_cache_server(req, req_type)
172172

173173
if not results:
174174
logger.warning(
@@ -188,11 +188,16 @@ def resolve(
188188
return results
189189
return [results[0]]
190190

191-
def _resolve_from_cache_server(self, req: Requirement) -> list[tuple[str, Version]]:
191+
def _resolve_from_cache_server(
192+
self, req: Requirement, req_type: RequirementType
193+
) -> list[tuple[str, Version]]:
192194
"""Fall back to the remote wheel cache server for a cached version.
193195
194196
When age filtering removes all candidates in multi-version mode,
195-
queries the remote cache server for the newest available wheel.
197+
queries the remote cache server (both wheels and sdists) to find
198+
the newest available version, then re-resolves the sdist URL
199+
through the normal source provider (including overrides).
200+
196201
Returns at most one version so that transitive dependencies are
197202
re-processed without rebuilding every old version.
198203
"""
@@ -220,9 +225,49 @@ def _resolve_from_cache_server(self, req: Requirement) -> list[tuple[str, Versio
220225
self.cache_wheel_server_url,
221226
err,
222227
)
223-
if best is not None:
224-
logger.info("found version %s on cache server", best[1])
225-
return [best]
228+
229+
if best is None:
230+
logger.debug(
231+
"no versions found on cache server %s for %s",
232+
self.cache_wheel_server_url,
233+
req.name,
234+
)
235+
return []
236+
237+
_, version = best
238+
logger.info("found version %s on cache server, resolving sdist URL", version)
239+
240+
pinned_req = Requirement(f"{req.name}=={version}")
241+
try:
242+
source_provider = sources.get_source_provider(
243+
ctx=self.ctx,
244+
req=pinned_req,
245+
sdist_server_url=resolver.PYPI_SERVER_URL,
246+
req_type=req_type,
247+
)
248+
sdist_results = resolver.find_all_matching_from_provider(
249+
source_provider, pinned_req
250+
)
251+
if sdist_results:
252+
logger.info(
253+
"resolved sdist URL for %s==%s from source provider",
254+
req.name,
255+
version,
256+
)
257+
return [sdist_results[0]]
258+
except Exception as err:
259+
logger.warning(
260+
"failed to resolve sdist URL for %s==%s: %s",
261+
req.name,
262+
version,
263+
err,
264+
)
265+
266+
logger.warning(
267+
"cache server has %s==%s but source provider returned no sdist",
268+
req.name,
269+
version,
270+
)
226271
return []
227272

228273
def get_cached_resolution(

tests/test_bootstrap_requirement_resolver.py

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -699,27 +699,39 @@ def test_resolve_prebuilt_after_source_uses_separate_cache(
699699
class TestResolveFromCacheServer:
700700
"""Tests for the cache server fallback in _resolve_from_cache_server."""
701701

702-
def test_returns_newest_version_from_cache(self, tmp_context: WorkContext) -> None:
703-
"""Falls back to cache server and returns only the newest version."""
702+
def test_resolves_sdist_url_via_source_provider(
703+
self, tmp_context: WorkContext
704+
) -> None:
705+
"""Finds version on cache, then resolves sdist URL via source provider."""
704706
brr = BootstrapRequirementResolver(
705707
tmp_context,
706708
multiple_versions=True,
707709
cache_wheel_server_url="http://cache.test/simple",
708710
)
709711
req = Requirement("testpkg")
710712

711-
with patch(
712-
"fromager.bootstrap_requirement_resolver.resolver"
713-
".find_all_matching_from_provider",
714-
return_value=[
715-
("http://cache.test/testpkg-3.0.whl", Version("3.0")),
716-
("http://cache.test/testpkg-2.0.whl", Version("2.0")),
717-
],
713+
cache_results = [
714+
("http://cache.test/testpkg-3.0-py3-none-any.whl", Version("3.0")),
715+
]
716+
sdist_results = [
717+
("https://pypi.test/testpkg-3.0.tar.gz", Version("3.0")),
718+
]
719+
720+
with (
721+
patch(
722+
"fromager.bootstrap_requirement_resolver.resolver"
723+
".find_all_matching_from_provider",
724+
side_effect=[cache_results, [], sdist_results],
725+
),
726+
patch(
727+
"fromager.bootstrap_requirement_resolver.sources.get_source_provider",
728+
),
718729
):
719-
result = brr._resolve_from_cache_server(req)
730+
result = brr._resolve_from_cache_server(req, RequirementType.INSTALL)
720731

721732
assert len(result) == 1
722733
assert result[0][1] == Version("3.0")
734+
assert result[0][0] == "https://pypi.test/testpkg-3.0.tar.gz"
723735

724736
def test_returns_empty_when_cache_has_no_match(
725737
self, tmp_context: WorkContext
@@ -737,11 +749,11 @@ def test_returns_empty_when_cache_has_no_match(
737749
".find_all_matching_from_provider",
738750
return_value=[],
739751
):
740-
result = brr._resolve_from_cache_server(req)
752+
result = brr._resolve_from_cache_server(req, RequirementType.INSTALL)
741753

742754
assert result == []
743755

744-
def test_returns_empty_on_exception(self, tmp_context: WorkContext) -> None:
756+
def test_returns_empty_on_cache_exception(self, tmp_context: WorkContext) -> None:
745757
"""Returns empty list when cache server query fails."""
746758
brr = BootstrapRequirementResolver(
747759
tmp_context,
@@ -755,7 +767,36 @@ def test_returns_empty_on_exception(self, tmp_context: WorkContext) -> None:
755767
".find_all_matching_from_provider",
756768
side_effect=RuntimeError("connection refused"),
757769
):
758-
result = brr._resolve_from_cache_server(req)
770+
result = brr._resolve_from_cache_server(req, RequirementType.INSTALL)
771+
772+
assert result == []
773+
774+
def test_returns_empty_when_source_provider_has_no_sdist(
775+
self, tmp_context: WorkContext
776+
) -> None:
777+
"""Returns empty when cache has version but source provider has no sdist."""
778+
brr = BootstrapRequirementResolver(
779+
tmp_context,
780+
multiple_versions=True,
781+
cache_wheel_server_url="http://cache.test/simple",
782+
)
783+
req = Requirement("testpkg")
784+
785+
cache_results = [
786+
("http://cache.test/testpkg-3.0-py3-none-any.whl", Version("3.0")),
787+
]
788+
789+
with (
790+
patch(
791+
"fromager.bootstrap_requirement_resolver.resolver"
792+
".find_all_matching_from_provider",
793+
side_effect=[cache_results, [], []],
794+
),
795+
patch(
796+
"fromager.bootstrap_requirement_resolver.sources.get_source_provider",
797+
),
798+
):
799+
result = brr._resolve_from_cache_server(req, RequirementType.INSTALL)
759800

760801
assert result == []
761802

@@ -783,7 +824,7 @@ def test_resolve_uses_cache_fallback_when_age_filter_empties(
783824
patch.object(
784825
brr,
785826
"_resolve_from_cache_server",
786-
return_value=[("http://cache.test/testpkg-1.0.whl", Version("1.0"))],
827+
return_value=[("https://pypi.test/testpkg-1.0.tar.gz", Version("1.0"))],
787828
) as mock_cache,
788829
):
789830
result = brr.resolve(
@@ -795,7 +836,7 @@ def test_resolve_uses_cache_fallback_when_age_filter_empties(
795836
)
796837

797838
mock_pypi.assert_called_once()
798-
mock_cache.assert_called_once_with(req)
839+
mock_cache.assert_called_once_with(req, RequirementType.INSTALL)
799840
assert len(result) == 1
800841
assert result[0][1] == Version("1.0")
801842

0 commit comments

Comments
 (0)