Skip to content

Commit 55aca35

Browse files
smoparthclaude
andcommitted
feat(resolver): wire source_resolver into resolver and download pipelines
Activate the `source` field on `PackageSettings` and `VariantInfo` so YAML-configured providers are used by the resolver and download pipelines. This is the wiring layer for the model classes introduced in PR #1052. - Uncomment `source: SourceResolver | None` in `PackageSettings` and `VariantInfo` - Add `PackageBuildInfo.source_resolver` property (variant overrides package level) - `default_resolver_provider()` checks `pbi.source_resolver` before returning the default `PyPIProvider` - `default_download_source()` detects `git+https://` / `git+ssh://` URLs and routes to `download_git_source()` - Add `GitOptions.remove_dot_git` field (default `True`) and wire it into `download_git_source()` - Update existing test snapshots and add 20 new tests Closes: #1048 Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Shanmukh Pawan <smoparth@redhat.com>
1 parent 24af91e commit 55aca35

6 files changed

Lines changed: 489 additions & 20 deletions

File tree

src/fromager/packagesettings/_models.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pydantic import AnyUrl, Field
1616
from pydantic_core import core_schema
1717

18-
# from ._resolver import SourceResolver
18+
from ._resolver import SourceResolver
1919
from ._typedefs import (
2020
MODEL_CONFIG,
2121
BuildDirectory,
@@ -324,9 +324,10 @@ class VariantInfo(pydantic.BaseModel):
324324
pre_built: bool = False
325325
"""Use pre-built wheel from index server?"""
326326

327-
# TODO
328-
# source: SourceResolver | None
329-
# """Source resolver and downloader"""
327+
source: typing.Annotated[
328+
SourceResolver | None,
329+
pydantic.Field(default=None, discriminator="provider"),
330+
] = None
330331

331332

332333
class GitOptions(pydantic.BaseModel):
@@ -336,6 +337,7 @@ class GitOptions(pydantic.BaseModel):
336337
337338
submodules: False
338339
submodule_paths: []
340+
remove_dot_git: True
339341
"""
340342

341343
model_config = MODEL_CONFIG
@@ -358,6 +360,18 @@ class GitOptions(pydantic.BaseModel):
358360
- ["vendor/lib1", "vendor/lib2"]
359361
"""
360362

363+
remove_dot_git: bool = False
364+
"""Remove ``.git`` directory after cloning?
365+
366+
When True, the ``.git`` directory is removed from the cloned source
367+
tree so it does not end up in the built sdist. Defaults to False
368+
to preserve backward compatibility with existing ``req.url`` git
369+
clones that rely on ``.git`` for version detection (e.g. via
370+
setuptools-scm).
371+
372+
.. versionadded:: 0.85
373+
"""
374+
361375

362376
_DictStrAny = dict[str, typing.Any]
363377

@@ -452,9 +466,10 @@ class PackageSettings(pydantic.BaseModel):
452466
project_override: ProjectOverride = Field(default_factory=ProjectOverride)
453467
"""Patch project settings"""
454468

455-
# TODO
456-
# source: SourceResolver | None
457-
# """Source resolver and downloader"""
469+
source: typing.Annotated[
470+
SourceResolver | None,
471+
pydantic.Field(default=None, discriminator="provider"),
472+
] = None
458473

459474
variants: Mapping[Variant, VariantInfo] = Field(default_factory=dict)
460475
"""Variant configuration"""

src/fromager/packagesettings/_pbi.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
if typing.TYPE_CHECKING:
2828
from .. import build_environment
29+
from ._resolver import SourceResolver
2930
from ._settings import Settings
3031

3132
logger = logging.getLogger(__name__)
@@ -176,6 +177,18 @@ def pre_built(self) -> bool:
176177
return vi.pre_built
177178
return False
178179

180+
@property
181+
def source_resolver(self) -> SourceResolver | None:
182+
"""Effective source resolver for this package and variant.
183+
184+
Returns the variant-level ``source`` override if set, otherwise
185+
the package-level ``source``, or ``None`` when neither is configured.
186+
"""
187+
vi = self._ps.variants.get(self._variant)
188+
if vi is not None and vi.source is not None:
189+
return vi.source
190+
return self._ps.source
191+
179192
@property
180193
def wheel_server_url(self) -> str | None:
181194
"""Alternative package index for pre-build wheel"""

src/fromager/resolver.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,23 @@ def default_resolver_provider(
119119
include_wheels: bool,
120120
req_type: RequirementType | None = None,
121121
ignore_platform: bool = False,
122-
) -> (
123-
PyPIProvider
124-
| GenericProvider
125-
| GitHubTagProvider
126-
| GitLabTagProvider
127-
| VersionMapProvider
128-
):
129-
"""Lookup resolver provider to resolve package versions"""
122+
) -> BaseProvider:
123+
"""Lookup resolver provider to resolve package versions.
124+
125+
When the package has a ``source`` configuration, the provider is
126+
created from the declarative resolver model. Otherwise the default
127+
``PyPIProvider`` is returned.
128+
"""
129+
pbi = ctx.package_build_info(req)
130+
source = pbi.source_resolver
131+
if source is not None:
132+
logger.info(
133+
"%s: using source resolver provider %r",
134+
req.name,
135+
source.provider,
136+
)
137+
return source.resolver_provider(ctx, typing.cast(RequirementType, req_type))
138+
130139
return PyPIProvider(
131140
include_sdists=include_sdists,
132141
include_wheels=include_wheels,

src/fromager/sources.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,30 @@ def resolve_source(
199199
raise
200200

201201

202+
def _is_git_url(url: str) -> bool:
203+
"""Return True if *url* is a VCS-style ``git+https://`` or ``git+ssh://`` URL."""
204+
return url.startswith("git+https://") or url.startswith("git+ssh://")
205+
206+
207+
def _parse_git_url(url: str) -> tuple[str, str | None]:
208+
"""Split a VCS URL into clone URL and optional ref.
209+
210+
``git+https://host/repo@ref`` -> ``(https://host/repo, ref)``
211+
``git+https://host/repo`` -> ``(https://host/repo, None)``
212+
"""
213+
clone_url = url
214+
if clone_url.startswith("git+"):
215+
clone_url = clone_url[len("git+") :]
216+
217+
parsed = urlparse(clone_url)
218+
ref: str | None = None
219+
if "@" in parsed.path:
220+
new_path, _, ref = parsed.path.rpartition("@")
221+
clone_url = parsed._replace(path=new_path).geturl()
222+
223+
return clone_url, ref
224+
225+
202226
def default_download_source(
203227
ctx: context.WorkContext,
204228
req: Requirement,
@@ -208,10 +232,24 @@ def default_download_source(
208232
) -> pathlib.Path:
209233
"Download the requirement and return the name of the output path."
210234
pbi = ctx.package_build_info(req)
211-
destination_filename = pbi.download_source_destination_filename(version=version)
212235
url = pbi.download_source_url(version=version, default=download_url)
213236
if url is None:
214237
raise ValueError(f"Could not determine download URL for {req}")
238+
239+
if _is_git_url(url):
240+
clone_url, ref = _parse_git_url(url)
241+
download_path = ctx.work_dir / f"{req.name}-{version}" / f"{req.name}-{version}"
242+
download_path.mkdir(parents=True, exist_ok=True)
243+
download_git_source(
244+
ctx=ctx,
245+
req=req,
246+
url_to_clone=clone_url,
247+
destination_dir=download_path,
248+
ref=ref,
249+
)
250+
return download_path
251+
252+
destination_filename = pbi.download_source_destination_filename(version=version)
215253
if destination_filename is None:
216254
url_filename = resolver.extract_filename_from_url(url)
217255
if url_filename.endswith(".zip"):
@@ -239,21 +277,22 @@ def download_git_source(
239277
destination_dir: pathlib.Path,
240278
ref: str | None = None,
241279
) -> None:
280+
"""Clone a git repository into *destination_dir*.
281+
282+
Applies ``git_options`` from the package settings (submodules,
283+
``remove_dot_git``).
284+
"""
242285
if url_to_clone.startswith("git+"):
243286
url_to_clone = url_to_clone[len("git+") :]
244287

245288
logger.info(f"cloning source from {url_to_clone}@{ref} to {destination_dir}")
246-
# Get git options from package settings
247289
pbi = ctx.package_build_info(req)
248290
git_opts = pbi.git_options
249291

250-
# Configure submodules based on package settings
251292
submodules: bool | list[str] = False
252293
if git_opts.submodule_paths:
253-
# If specific paths are configured, use those
254294
submodules = git_opts.submodule_paths
255295
elif git_opts.submodules:
256-
# If general submodule support is enabled, clone all submodules
257296
submodules = True
258297

259298
gitutils.git_clone(
@@ -265,6 +304,12 @@ def download_git_source(
265304
ref=ref,
266305
)
267306

307+
if git_opts.remove_dot_git:
308+
dot_git = destination_dir / ".git"
309+
if dot_git.exists():
310+
logger.info("removing %s", dot_git)
311+
shutil.rmtree(dot_git)
312+
268313

269314
# Helper method to check whether .zip /.tar / .tgz is able to extract and check its content.
270315
# It will throw exception if any other file is encountered. Eg: index.html

tests/test_packagesettings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"git_options": {
7272
"submodules": False,
7373
"submodule_paths": [],
74+
"remove_dot_git": False,
7475
},
7576
"name": "test-pkg",
7677
"has_config": True,
@@ -88,6 +89,7 @@
8889
"use_pypi_org_metadata": True,
8990
"min_release_age": None,
9091
},
92+
"source": None,
9193
"variants": {
9294
"cpu": {
9395
"annotations": {
@@ -96,6 +98,7 @@
9698
"env": {"EGG": "spam ${EGG}", "EGG_AGAIN": "$EGG"},
9799
"wheel_server_url": "https://wheel.test/simple",
98100
"pre_built": False,
101+
"source": None,
99102
},
100103
"rocm": {
101104
"annotations": {
@@ -104,12 +107,14 @@
104107
"env": {"SPAM": ""},
105108
"wheel_server_url": None,
106109
"pre_built": True,
110+
"source": None,
107111
},
108112
"cuda": {
109113
"annotations": None,
110114
"env": {},
111115
"wheel_server_url": None,
112116
"pre_built": False,
117+
"source": None,
113118
},
114119
},
115120
}
@@ -134,6 +139,7 @@
134139
"git_options": {
135140
"submodules": False,
136141
"submodule_paths": [],
142+
"remove_dot_git": False,
137143
},
138144
"has_config": True,
139145
"purl": None,
@@ -150,6 +156,7 @@
150156
"use_pypi_org_metadata": None,
151157
"min_release_age": None,
152158
},
159+
"source": None,
153160
"variants": {},
154161
}
155162

@@ -175,6 +182,7 @@
175182
"git_options": {
176183
"submodules": False,
177184
"submodule_paths": [],
185+
"remove_dot_git": False,
178186
},
179187
"has_config": True,
180188
"purl": None,
@@ -191,12 +199,14 @@
191199
"use_pypi_org_metadata": None,
192200
"min_release_age": None,
193201
},
202+
"source": None,
194203
"variants": {
195204
"cpu": {
196205
"annotations": None,
197206
"env": {},
198207
"pre_built": True,
199208
"wheel_server_url": None,
209+
"source": None,
200210
},
201211
},
202212
}

0 commit comments

Comments
 (0)