Skip to content

Commit 00c94bd

Browse files
committed
feat(settings): add version-specific prebuilt wheel configuration
Enable fine-grained control over prebuilt vs source builds within variants by allowing per-version overrides. This resolves conflicts where different collections need different build methods for the same package version. - Add VersionSpecificSettings model with pre_built and wheel_server_url - Extend VariantInfo with versions mapping for version-specific overrides - Update PackageBuildInfo with version-aware methods (is_pre_built, get_wheel_server_url) - Modify bootstrapper, wheels, and build commands to use version-specific logic - Add comprehensive tests validating precedence and backward compatibility - Create detailed how-to documentation with examples and migration guide Maintains full backward compatibility with existing variant-wide settings. Version-specific settings take precedence over variant defaults when present. Fixes #801
1 parent e88a7c2 commit 00c94bd

File tree

7 files changed

+515
-26
lines changed

7 files changed

+515
-26
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
Version-Specific Prebuilt Settings
2+
===================================
3+
4+
When working with multiple collections that share the same variant but need
5+
different versions of a package, you may want some versions to use prebuilt
6+
wheels while others are built from source.
7+
8+
Version-specific prebuilt settings allow you to configure ``pre_built`` and
9+
``wheel_server_url`` on a per-version basis within a variant, providing
10+
fine-grained control over which package versions use prebuilt wheels.
11+
12+
Configuration
13+
-------------
14+
15+
Add version-specific settings under the ``versions`` key within a variant:
16+
17+
.. code-block:: yaml
18+
19+
# overrides/settings/torchvision.yaml
20+
variants:
21+
tpu-ubi9:
22+
# Default behavior for unlisted versions
23+
pre_built: false
24+
25+
# Version-specific overrides
26+
versions:
27+
# Use prebuilt wheel for this version
28+
"0.24.0.dev20250730":
29+
pre_built: true
30+
wheel_server_url: https://gitlab.com/api/v4/projects/12345/packages/pypi/simple
31+
32+
# Build from source for this version
33+
"0.23.0":
34+
pre_built: false
35+
36+
Available Settings
37+
------------------
38+
39+
Within each version-specific block, you can configure:
40+
41+
``pre_built``
42+
Boolean indicating whether to use prebuilt wheels for this version.
43+
44+
``wheel_server_url``
45+
URL to download prebuilt wheels from for this version.
46+
47+
``env``
48+
Environment variables specific to this version.
49+
50+
``annotations``
51+
Version-specific annotations.
52+
53+
Precedence Rules
54+
----------------
55+
56+
Version-specific settings override variant-wide settings. If both are defined,
57+
environment variables are merged with version-specific values taking precedence
58+
for conflicting keys.
59+
60+
Example Use Case
61+
----------------
62+
63+
Consider two TPU collections using different ``torchvision`` versions:
64+
65+
**Global Collection** (``collections/accelerated/tpu-ubi9/requirements.txt``):
66+
67+
.. code-block:: text
68+
69+
torchvision==0.24.0.dev20250730
70+
71+
**Torch-2.8.0 Collection** (``collections/torch-2.8.0/tpu-ubi9/requirements.txt``):
72+
73+
.. code-block:: text
74+
75+
torchvision==0.23.0
76+
77+
With the configuration above:
78+
79+
- Global collection downloads prebuilt ``torchvision==0.24.0.dev20250730`` wheels
80+
- Torch-2.8.0 collection builds ``torchvision==0.23.0`` from source
81+
- Both use the same variant (``tpu-ubi9``) with different build methods

src/fromager/bootstrapper.py

Lines changed: 108 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,28 @@ def resolve_version(
8888
return self._resolved_requirements[req_str]
8989

9090
pbi = self.ctx.package_build_info(req)
91-
if pbi.pre_built:
92-
source_url, resolved_version = self._resolve_prebuilt_with_history(
93-
req=req,
94-
req_type=req_type,
95-
)
91+
92+
# Check if package has version-specific settings (any version-specific config)
93+
variant_info = pbi._ps.variants.get(pbi._variant)
94+
has_version_specific_prebuilt = (
95+
variant_info and variant_info.versions and len(variant_info.versions) > 0
96+
)
97+
98+
if pbi.pre_built or has_version_specific_prebuilt:
99+
try:
100+
source_url, resolved_version = self._resolve_prebuilt_with_history(
101+
req=req,
102+
req_type=req_type,
103+
)
104+
except Exception as e:
105+
# Version-specific prebuilt resolution failed, fall back to source
106+
logger.debug(
107+
f"{req.name}: prebuilt resolution failed, falling back to source: {e}"
108+
)
109+
source_url, resolved_version = self._resolve_source_with_history(
110+
req=req,
111+
req_type=req_type,
112+
)
96113
else:
97114
source_url, resolved_version = self._resolve_source_with_history(
98115
req=req,
@@ -185,7 +202,8 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> Version:
185202

186203
source_url_type = sources.get_source_type(self.ctx, req)
187204

188-
if pbi.pre_built:
205+
# Use version-aware prebuilt check now that we have resolved_version
206+
if pbi.is_pre_built(resolved_version):
189207
wheel_filename, unpack_dir = self._download_prebuilt(
190208
req=req,
191209
req_type=req_type,
@@ -826,6 +844,19 @@ def _resolve_prebuilt_with_history(
826844
req: Requirement,
827845
req_type: RequirementType,
828846
) -> tuple[str, Version]:
847+
# Try version-specific resolution FIRST (highest priority)
848+
# This allows version-specific wheel_server_url settings to override
849+
# any cached resolutions from previous bootstraps or current graph
850+
try:
851+
wheel_url, resolved_version = (
852+
self._resolve_prebuilt_with_version_specific_urls(req, req_type)
853+
)
854+
return (wheel_url, resolved_version)
855+
except Exception:
856+
# No version-specific settings matched, fall back to cached resolution
857+
pass
858+
859+
# Fall back to cached resolution from graph
829860
cached_resolution = self._resolve_from_graph(
830861
req=req,
831862
req_type=req_type,
@@ -835,15 +866,77 @@ def _resolve_prebuilt_with_history(
835866
if cached_resolution and not req.url:
836867
wheel_url, resolved_version = cached_resolution
837868
logger.debug(f"resolved from previous bootstrap to {resolved_version}")
838-
else:
839-
servers = wheels.get_wheel_server_urls(
840-
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
841-
)
842-
wheel_url, resolved_version = wheels.resolve_prebuilt_wheel(
843-
ctx=self.ctx, req=req, wheel_server_urls=servers, req_type=req_type
844-
)
869+
return (wheel_url, resolved_version)
870+
871+
# Fall back to regular prebuilt wheel resolution (no version-specific or cached)
872+
servers = wheels.get_wheel_server_urls(
873+
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
874+
)
875+
wheel_url, resolved_version = wheels.resolve_prebuilt_wheel(
876+
ctx=self.ctx, req=req, wheel_server_urls=servers, req_type=req_type
877+
)
845878
return (wheel_url, resolved_version)
846879

880+
def _resolve_prebuilt_with_version_specific_urls(
881+
self,
882+
req: Requirement,
883+
req_type: RequirementType,
884+
) -> tuple[str, Version]:
885+
"""Resolve prebuilt wheel using version-specific wheel server URLs if configured."""
886+
pbi = self.ctx.package_build_info(req)
887+
888+
# Check if there are version-specific settings
889+
variant_info = pbi._ps.variants.get(pbi._variant)
890+
if not variant_info or not variant_info.versions:
891+
raise ValueError("No version-specific settings configured")
892+
893+
# Get the constraint for this package
894+
constraint = self.ctx.constraints.get_constraint(req.name)
895+
896+
# Try to resolve using version-specific wheel server URLs
897+
for version_str, version_settings in variant_info.versions.items():
898+
# Only process versions that have both wheel_server_url and pre_built=True
899+
if not (version_settings.wheel_server_url and version_settings.pre_built):
900+
continue
901+
902+
# Only try this version if it satisfies the requirement specifier AND constraint
903+
try:
904+
version_obj = Version(version_str)
905+
906+
# Check requirement specifier (if present)
907+
if req.specifier and version_obj not in req.specifier:
908+
continue
909+
910+
# Check constraint (if present) - this is the version from constraints.txt
911+
if constraint and version_obj not in constraint.specifier:
912+
continue
913+
914+
except Exception:
915+
continue # Skip invalid version strings
916+
917+
# Create a constraint for this specific version
918+
version_req = Requirement(f"{req.name}=={version_str}")
919+
920+
try:
921+
# Try to resolve this specific version from the version-specific server
922+
wheel_url, resolved_version = wheels.resolve_prebuilt_wheel(
923+
ctx=self.ctx,
924+
req=version_req,
925+
wheel_server_urls=[version_settings.wheel_server_url],
926+
req_type=req_type,
927+
)
928+
logger.info(
929+
f"{req.name}: using version-specific prebuilt wheel "
930+
f"{resolved_version} from {version_settings.wheel_server_url}"
931+
)
932+
return (wheel_url, resolved_version)
933+
except Exception:
934+
# Version not found on this server, try next
935+
continue
936+
937+
# No matching version-specific prebuilt settings found
938+
raise ValueError("No matching version-specific prebuilt settings found")
939+
847940
def _resolve_from_graph(
848941
self,
849942
req: Requirement,
@@ -949,7 +1042,8 @@ def _add_to_graph(
9491042
req=req,
9501043
req_version=req_version,
9511044
download_url=download_url,
952-
pre_built=pbi.pre_built,
1045+
# Use version-aware prebuilt check for dependency graph
1046+
pre_built=pbi.is_pre_built(req_version),
9531047
)
9541048
self.ctx.write_to_graph_to_file()
9551049

src/fromager/commands/build.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,14 @@ def _build(
345345

346346
logger.info("starting processing")
347347
pbi = wkctx.package_build_info(req)
348-
prebuilt = pbi.pre_built
348+
# Use version-aware prebuilt check now that we have resolved_version
349+
prebuilt = pbi.is_pre_built(resolved_version)
349350

350351
wheel_server_urls = wheels.get_wheel_server_urls(
351-
wkctx, req, cache_wheel_server_url=cache_wheel_server_url
352+
wkctx,
353+
req,
354+
cache_wheel_server_url=cache_wheel_server_url,
355+
version=resolved_version,
352356
)
353357

354358
# See if we can reuse an existing wheel.

0 commit comments

Comments
 (0)