1212from packaging .requirements import Requirement
1313from packaging .version import Version
1414
15- from . import resolver , sources , wheels
15+ from . import finders , resolver , sources , wheels
1616from .dependency_graph import DependencyGraph
1717from .requirements_file import RequirementType
1818
@@ -40,15 +40,24 @@ def __init__(
4040 self ,
4141 ctx : context .WorkContext ,
4242 prev_graph : DependencyGraph | None = None ,
43+ multiple_versions : bool = False ,
44+ cache_wheel_server_url : str = "" ,
4345 ) -> None :
4446 """Initialize requirement resolver.
4547
4648 Args:
4749 ctx: Work context with constraints and settings
4850 prev_graph: Optional previous dependency graph for caching
51+ multiple_versions: If ``True``, age filtering returns an empty
52+ list instead of falling back to all candidates, letting the
53+ caller decide how to handle the case.
54+ cache_wheel_server_url: URL of the remote wheel cache server.
55+ Used as a fallback when age filtering produces no candidates.
4956 """
5057 self .ctx = ctx
5158 self .prev_graph = prev_graph
59+ self .multiple_versions = multiple_versions
60+ self .cache_wheel_server_url = cache_wheel_server_url
5261 # Session-level resolution cache to avoid re-resolving same requirements
5362 # Key: (requirement_string, pre_built) to distinguish source vs prebuilt
5463 # Value: tuple of (url, version) tuples sorted by version (highest first)
@@ -72,6 +81,8 @@ def resolve(
7281 1. Session cache (if previously resolved)
7382 2. Previous dependency graph
7483 3. PyPI resolution (source or prebuilt based on package build info)
84+ 4. Remote wheel cache server (multi-version mode only, when age
85+ filtering produced no candidates)
7586
7687 Args:
7788 req: Package requirement
@@ -110,36 +121,7 @@ def resolve(
110121 logger .debug (f"resolved { req } from cache" )
111122 return list (cached_result ) if return_all_versions else [cached_result [0 ]]
112123
113- # Resolve using strategies
114- results = self ._resolve (req , req_type , parent_req , pre_built )
115-
116- # Cache the result
117- self .cache_resolution (req , pre_built , results )
118- return results if return_all_versions else [results [0 ]]
119-
120- def _resolve (
121- self ,
122- req : Requirement ,
123- req_type : RequirementType ,
124- parent_req : Requirement | None ,
125- pre_built : bool ,
126- ) -> list [tuple [str , Version ]]:
127- """Internal resolution logic without caching.
128-
129- Tries resolution strategies in order:
130- 1. Previous dependency graph
131- 2. PyPI resolution (source or prebuilt)
132-
133- Args:
134- req: Package requirement
135- req_type: Type of requirement
136- parent_req: Parent requirement from dependency chain
137- pre_built: Whether to resolve prebuilt (True) or source (False)
138-
139- Returns:
140- List of (url, version) tuples sorted by version (highest first)
141- """
142- # Try graph
124+ # Try previous dependency graph
143125 cached_resolution = self ._resolve_from_graph (
144126 req = req ,
145127 req_type = req_type ,
@@ -151,17 +133,13 @@ def _resolve(
151133 logger .debug (
152134 f"resolved from previous bootstrap: { len (cached_resolution )} version(s)"
153135 )
154- return cached_resolution
155-
156- # Fallback to PyPI using provider pattern
157- if pre_built :
136+ results = cached_resolution
137+ elif pre_built :
158138 # Resolve prebuilt wheel
159- # Get wheel server URLs (use PyPI as cache/fallback server)
160139 wheel_server_urls = wheels .get_wheel_server_urls (
161140 self .ctx , req , cache_wheel_server_url = resolver .PYPI_SERVER_URL
162141 )
163- # Use shared retry loop logic from wheels module
164- return wheels .resolve_all_prebuilt_wheels (
142+ results = wheels .resolve_all_prebuilt_wheels (
165143 ctx = self .ctx ,
166144 req = req ,
167145 wheel_server_urls = wheel_server_urls ,
@@ -176,10 +154,76 @@ def _resolve(
176154 req_type = req_type ,
177155 )
178156 max_age_cutoff = resolver ._compute_max_age_cutoff (self .ctx )
179- return resolver .find_all_matching_from_provider (
180- provider , req , max_age_cutoff = max_age_cutoff
157+ results = resolver .find_all_matching_from_provider (
158+ provider ,
159+ req ,
160+ max_age_cutoff = max_age_cutoff ,
161+ fallback_on_empty_age_filter = not self .multiple_versions ,
181162 )
182163
164+ if not results and self .multiple_versions and self .cache_wheel_server_url :
165+ logger .info (
166+ "no results found with normal resolution, "
167+ "falling back to the cache server %s" ,
168+ self .cache_wheel_server_url ,
169+ )
170+ results = self ._resolve_from_cache_server (req )
171+
172+ if not results :
173+ logger .warning (
174+ "resolver returned no results with "
175+ "fallback wheel server URL %s and %s version mode" ,
176+ self .cache_wheel_server_url or "(none)" ,
177+ "multiple" if self .multiple_versions else "single" ,
178+ )
179+
180+ # Only cache non-empty results.
181+ if results :
182+ self .cache_resolution (req , pre_built , results )
183+
184+ if not results :
185+ return []
186+ if return_all_versions :
187+ return results
188+ return [results [0 ]]
189+
190+ def _resolve_from_cache_server (self , req : Requirement ) -> list [tuple [str , Version ]]:
191+ """Fall back to the remote wheel cache server for a cached version.
192+
193+ When age filtering removes all candidates in multi-version mode,
194+ queries the remote cache server for the newest available wheel.
195+ Returns at most one version so that transitive dependencies are
196+ re-processed without rebuilding every old version.
197+ """
198+ logger .info (
199+ "checking cache server %s for existing build" ,
200+ self .cache_wheel_server_url ,
201+ )
202+ best : tuple [str , Version ] | None = None
203+ for include_sdists , include_wheels in [(False , True ), (True , False )]:
204+ try :
205+ provider = finders .PyPICacheProvider (
206+ cache_server_url = self .cache_wheel_server_url ,
207+ constraints = self .ctx .constraints ,
208+ include_sdists = include_sdists ,
209+ include_wheels = include_wheels ,
210+ )
211+ results = resolver .find_all_matching_from_provider (provider , req )
212+ if results :
213+ url , version = results [0 ]
214+ if best is None or version > best [1 ]:
215+ best = (url , version )
216+ except Exception as err :
217+ logger .warning (
218+ "error checking cache server %s: %s" ,
219+ self .cache_wheel_server_url ,
220+ err ,
221+ )
222+ if best is not None :
223+ logger .info ("found version %s on cache server" , best [1 ])
224+ return [best ]
225+ return []
226+
183227 def get_cached_resolution (
184228 self ,
185229 req : Requirement ,
0 commit comments