Skip to content

Commit 3356160

Browse files
committed
[proposal] New resolver configuration
Add design proposal for new resolver configuration. See: #937 Signed-off-by: Christian Heimes <cheimes@redhat.com>
1 parent 8d850ff commit 3356160

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ those special cases directly into fromager.
3939
cli.rst
4040
glossary.rst
4141
develop.md
42+
proposals/index.rst
4243

4344
What's with the name?
4445
---------------------

docs/proposals/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fromager Enhancement Proposals
2+
==============================
3+
4+
.. toctree::
5+
:maxdepth: 1
6+
7+
new-resolver-config
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# New resolver and download configuration
2+
3+
- Author: Christian Heimes
4+
- Created: 2026-02-24
5+
- Status: Open
6+
7+
## What
8+
9+
This enhancement document proposal a new approach to configure the package
10+
resolver and source / sdist downloader. The new settings are covering a
11+
wider range of use cases. Common patterns like building a package from a
12+
git checkout will no longer need custom Python plugins.
13+
14+
## Why
15+
16+
In downstream, we are encountering an increasing amount of packages that do
17+
not build from sdists on PyPI. Either package maintainers are not uploading
18+
source distributions to PyPI or sdists have issues. In some cases, packages
19+
use a midstream fork that is not on PyPI. The sources need to be build from
20+
git.
21+
22+
Because Fromager <= 0.76 does not have declarative settings for GitHub/GitLab
23+
resolver or cloning git repositories, we have to write custom Python plugins.
24+
The plugins are a maintenance burden.
25+
26+
## Goals
27+
28+
- support common use cases with package settings instead of custom plugin code
29+
- cover most common resolver scenarios:
30+
- resolve package on PyPI (sdist, wheel, or both)
31+
- resolve package on GitHub or GitLab with custom tag matcher
32+
- cover common sdist download and build scenarios:
33+
- sdist from PyPI
34+
- prebuilt wheel from PyPI
35+
- download tarball from URL
36+
- clone git repository
37+
- download an artifact from GitHub / GitLab release or tag
38+
- build sdist with PEP 517 hook or plain tarball
39+
- support per-variant setting, e.g. one variant uses prebuilt wheel while the
40+
rest uses sdist.
41+
- gradual migration path from old system to new configuration
42+
43+
## Non-goals
44+
45+
- The new system will not cover all use cases. Some specific use cases will
46+
still require custom code.
47+
- Retrieval of additional sources is out of scope, e.g. a package `egg` that
48+
needs `libegg-{version}.tar.gz`.
49+
- Provide SSH transport for git. The feature can be added at a later point
50+
when it's needed.
51+
- Extra options for authentication. The `requests` library and `git` CLI can
52+
use `$HOME/.netrc` for authentication.
53+
> **NOTE:** `requests` also supports `NETRC` environment variable,
54+
`libcurl` and `git` _only_ support `$HOME/.netrc`.
55+
56+
## How
57+
58+
The new system will use a new top-level configuration key `source`. The old
59+
`download_source` and `resolver_dist` settings will stay supported for a
60+
while. Eventually the old options will be deprecated and removed.
61+
62+
The resolver and source downloader can be configuration for all variants of
63+
a package as well as re-defined for each variant. A package can be configured
64+
as prebuilt for all variants or a variant can have a different resolver and
65+
sources than other.
66+
67+
Each use case is handled a provider profile. The profile name acts as a tag
68+
([discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions)).
69+
Each use case has a well-defined set of mandatory and optional arguments.
70+
71+
**Example:**
72+
73+
```yaml
74+
source:
75+
# `pypi-sdist` is the default provider
76+
provider: pypi-sdist
77+
variants:
78+
egg:
79+
source:
80+
# resolve and download prebuilt wheel
81+
provider: pypi-prebuilt
82+
index_url: https://custom-index.example/simple
83+
spam:
84+
source:
85+
# resolve on GitLab, clone tag over https, build an sdist with PEP 517 hook
86+
provider: gitlab-git
87+
url: https://gitlab.example/spam/spam
88+
matcher_factory: package_plugins.matchers:midstream_matcher_factory
89+
build_sdist: pep517
90+
viking:
91+
source:
92+
# resolve on PyPI, git clone, and build as tarball
93+
provider: pypi-git
94+
clone_url: https://git.example/viking/viking.git
95+
tag: 'v{version}'
96+
build_sdist: tarball
97+
camelot:
98+
source:
99+
# On second thought, let's not go to Camelot. It is a silly place.
100+
provider: not-available
101+
```
102+
103+
### Profiles
104+
105+
- The `pypi-sdist` profile resolve versions on PyPI or PyPI-compatible index.
106+
It only takes sdists into account and downloads the sdist from the index.
107+
The profile is equivalent to the current default settings with
108+
`include_sdists: true` and `include_wheels: false`.
109+
110+
- The `pypi-prebuilt` profile resolve versions of platform-specific wheels
111+
on PyPI and downloads the pre-built wheel. The profile is equivalent to
112+
`include_sdists: false`, `include_wheels: true`, and variant setting
113+
`pre_build: true`.
114+
115+
- The `pypi-download` resolve versions of any package on PyPI and downloads
116+
a tarball from an external URL (with `{version}` variable in download URL).
117+
It takes any sdist and any wheel into account. The profile is equivalent
118+
with `include_sdists: true`, `include_wheels: true`, `ignore_platform: true`,
119+
and a `download_source.url`.
120+
121+
- The `pypi-git` is similar to the `pypi-download` profile. Instead of
122+
downloading a tarball, it clones a git repository at a specific tag.
123+
124+
- The `gitlab-git` and `github-git` profiles use the `GitLabTagProvider` or
125+
`GitHubTagProvider` to resolve versions. The profiles git clone a project
126+
over `https` or `ssh` protocol.
127+
128+
- The `gitlab-download` and `github-download` are similar to `gitlab-git` and
129+
`github-git` profiles. Instead of cloning a git repository, they download
130+
a git tarball or an release artifact.
131+
132+
- The `not-available` profile raises an error. It can be used to block a
133+
package and only enable it for a single variant.
134+
135+
Like pip's VCS feature, all git clone operations automatically retrieve all
136+
submodules recursively.
137+
138+
### URL schema for git
139+
140+
The resolver and `Candidate` class do not support VCS URLs, yet. Fromager can
141+
adopt pip's [VCS support](https://pip.pypa.io/en/stable/topics/vcs-support/)
142+
syntax. The URL `git+https://git.example/viking/viking.git@v1.1.0` clones the
143+
git repository over HTTPS and checks out the tag `v1.1.0`.
144+
145+
### Matcher factory
146+
147+
The matcher factory argument is an import string. The string must resolve to
148+
a callable that accepts a `ctx` argument and returns a `re.Pattern`
149+
(recommended) or `MatchFunction`. If the return value is a pattern object,
150+
then it must have exactly one match group. The pattern is matched with
151+
`re.match`.
152+
153+
The default matcher factory parsed the tag with `packaging.version.Version`
154+
and ignores any error.
155+
156+
```python
157+
import re
158+
159+
from fromager import context, resolver
160+
from packaging.version import Version
161+
162+
163+
def matcher_factory_pat(ctx: context.WorkContext) -> re.Pattern | resolver.MatchFunction:
164+
# tag must must v1.2+midstream.1.cpu and results in Version("1.2+midstream.1")
165+
variant = re.escape(ctx.variant)
166+
pat = rf"^v(.*\+midstream\.\d+)\.{variant}$"
167+
return re.compile(pat)
168+
169+
170+
def matcher_factory_func(ctx: context.WorkContext) -> re.Pattern | resolver.MatchFunction:
171+
def pep440_matcher(identifier: str, item: str) -> Version | None:
172+
try:
173+
return Version(item)
174+
except ValueError:
175+
return None
176+
return pep440_matcher
177+
```
178+
179+
### Deprecations
180+
181+
- `download_source.url` is handled by `pypi-download` profile or
182+
`release_artifact` parameter of `github` or `gitlab` provider
183+
- `download_source.destination_filename` is not needed. All sdists use
184+
standard `{dist_name}-{version}.tar.gz` file name
185+
- `resolver_dist.sdist_server_url` is replaced by `index_url` parameter.
186+
All `pypi-*` profile support a custom index.
187+
- `git_options.submodules` is not needed. Like pip, Fromager will always
188+
clone all submodules.
189+
- variant settings `wheel_server_url` and `pre_build` are replaced by
190+
`pypi-prebuilt` profile

0 commit comments

Comments
 (0)