Skip to content

Commit 3c2c629

Browse files
authored
Merge branch 'main' into add_docstrings_non_trivial_code
2 parents 39f5da2 + 297ed76 commit 3c2c629

15 files changed

Lines changed: 736 additions & 18 deletions

.github/workflows/codeql.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,14 @@ jobs:
3838
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
3939

4040
- name: Initialize CodeQL
41-
uses: github/codeql-action/init@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3
41+
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
4242
with:
4343
languages: ${{ matrix.language }}
4444

4545
- name: Autobuild
46-
uses: github/codeql-action/autobuild@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3
46+
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
4747

4848
- name: Perform CodeQL Analysis
49-
uses: github/codeql-action/analyze@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3
49+
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
5050
with:
5151
category: "/language:${{ matrix.language }}"

.github/workflows/python-publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
python -m build
4040
- name: Publish distribution to PyPI
4141
if: startsWith(github.ref, 'refs/tags')
42-
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
42+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1

.github/workflows/scorecard.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ jobs:
4646
publish_results: true
4747

4848
- name: Upload artifact
49-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
49+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
5050
with:
5151
name: SARIF file
5252
path: results.sarif
5353
retention-days: 5
5454

5555
- name: Upload to code-scanning
56-
uses: github/codeql-action/upload-sarif@5c8a8a642e79153f5d047b10ec1cba1d1cc65699 # v3
56+
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
5757
with:
5858
sarif_file: results.sarif

.github/workflows/test.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
run: hatch run test:test --with-network
5858

5959
- name: Upload coverage
60-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
60+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
6161
with:
6262
name: coverage-unit-py${{ matrix.python-version }}-rs${{ matrix.rust-version }}-${{ matrix.os }}
6363
path: .coverage.*
@@ -129,15 +129,15 @@ jobs:
129129

130130
- name: Upload logs for debugging
131131
if: ${{ failure() }}
132-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
132+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
133133
with:
134134
name: ${{ matrix.test-script }}-py${{ matrix.python-version }}-rs${{ matrix.rust-version }}-${{ matrix.os }}
135135
path: |
136136
e2e-output
137137
e2e-failed-*
138138
139139
- name: Upload coverage
140-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
140+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
141141
with:
142142
name: coverage-e2e-${{ matrix.test-script }}-py${{ matrix.python-version }}-rs${{ matrix.rust-version }}-${{ matrix.os }}
143143
path: .coverage.*
@@ -182,7 +182,7 @@ jobs:
182182
hatch run test:coverage report --fail-under=60
183183
184184
- name: Upload report
185-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
185+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
186186
with:
187187
path: htmlcov
188188
name: htmlcov

.mergify.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,24 @@ pull_request_rules:
7777
actions:
7878
merge:
7979
method: merge
80+
81+
- name: Auto-update approved PRs that are behind
82+
conditions:
83+
- "-draft"
84+
- "-conflict"
85+
- "#commits-behind>0"
86+
- "base=main"
87+
- "#approved-reviews-by>=1"
88+
- "author!=dependabot[bot]"
89+
actions:
90+
update:
91+
92+
- name: Ping author on conflicts
93+
conditions:
94+
- "conflict"
95+
- "-closed"
96+
actions:
97+
comment:
98+
message: |
99+
This pull request has merge conflicts that must be resolved before it can be merged.
100+
@{{ author }} please rebase your branch.

docs/proposals/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ Fromager Enhancement Proposals
55
:maxdepth: 1
66

77
new-patcher-config
8+
new-resolver-config
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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 proposes 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 therefore `git` did not support `NETRC` before
55+
> libcurl [8.16.0](https://curl.se/ch/8.16.0.html) (2025-09-10). Before
56+
> `git` _only_ supports `$HOME/.netrc`.
57+
58+
## How
59+
60+
The new system will use a new top-level configuration key `source`. The old
61+
`download_source` and `resolver_dist` settings will stay supported for a
62+
while. Eventually the old options will be deprecated and removed.
63+
64+
In addition to a top-level `source` entry, the resolver and downloader can
65+
be overwritten in a variant `source` entry. This enables variant-specific
66+
settings. For example a package resolve and download from GitHub and one
67+
variant uses a pre-built wheel from a custom index.
68+
69+
Each use case is handled a provider profile. The profile name acts as a tag
70+
([discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions)).
71+
Each use case has a well-defined set of mandatory and optional arguments.
72+
73+
**Example:**
74+
75+
```yaml
76+
source:
77+
# `pypi-sdist` is the default provider
78+
provider: pypi-sdist
79+
variants:
80+
egg:
81+
source:
82+
# resolve and download prebuilt wheel
83+
provider: pypi-prebuilt
84+
index_url: https://custom-index.example/simple
85+
spam:
86+
source:
87+
# resolve tag on GitLab, clone tag over https, build an sdist with PEP 517 hook
88+
provider: gitlab-tag-git
89+
project_url: https://gitlab.example/spam/spam
90+
matcher_factory: package_plugins.matchers:midstream_matcher_factory
91+
build_sdist: pep517
92+
viking:
93+
source:
94+
# resolve on PyPI, git clone, and build as tarball
95+
provider: pypi-git
96+
clone_url: https://git.example/viking/viking.git
97+
tag: 'v{version}'
98+
build_sdist: tarball
99+
caerbannog:
100+
# resolve with a mapping of version number to git refs, git clone, and build with PEP 517 hook
101+
source:
102+
provider: versionmap-git
103+
clone_url: https://git.example/viking/viking.git
104+
build_sdist: pep517
105+
versionmap:
106+
'1.0': abad1dea
107+
'1.1': refs/tags/1.1
108+
camelot:
109+
source:
110+
# On second thought, let's not go to Camelot. It is a silly place.
111+
provider: not-available
112+
```
113+
114+
### Profiles
115+
116+
- The `pypi-sdist` profile resolve versions on PyPI or PyPI-compatible index.
117+
It only takes sdists into account and downloads the sdist from the index.
118+
The profile is equivalent to the current default settings with
119+
`include_sdists: true` and `include_wheels: false`.
120+
121+
- The `pypi-prebuilt` profile resolve versions of platform-specific wheels
122+
on PyPI and downloads the pre-built wheel. The profile is equivalent to
123+
`include_sdists: false`, `include_wheels: true`, and variant setting
124+
`pre_build: true`.
125+
126+
- The `pypi-download` resolve versions of any package on PyPI and downloads
127+
a tarball from an external URL (with `{version}` variable in download URL).
128+
It takes any sdist and any wheel into account. The profile is equivalent
129+
with `include_sdists: true`, `include_wheels: true`, `ignore_platform: true`,
130+
and a `download_source.url`.
131+
132+
- The `pypi-git` is similar to the `pypi-download` profile. Instead of
133+
downloading a tarball, it clones a git repository at a specific tag.
134+
135+
- The `versionmap-git` profiles maps known version numbers to known git
136+
commits. It clones a git repo at the configured tag.
137+
138+
- The `gitlab-tag-git` and `github-tag-git` profiles use the
139+
`GitLabTagProvider` or `GitHubTagProvider` to resolve versions. The
140+
profiles git clone a project over `https` or `ssh` protocol.
141+
142+
- The `gitlab-tag-download` and `github-tag-download` are similar to
143+
`gitlab-tag-git` and `github-tag-git` profiles. Instead of cloning a git
144+
repository, they download a git tarball or an release artifact.
145+
146+
- The `hooks` profile calls the `resolver_provider` and `download_source`
147+
[hooks](../reference/hooks.rst).
148+
149+
- The `not-available` profile raises an error. It can be used to block a
150+
package and only enable it for a single variant.
151+
152+
### default behavior and hooks
153+
154+
When a package setting file does not have a top-level `source` configuration,
155+
then Fromager keep its old behavior. It first looks for `resolver_provider`
156+
and `download_source` [hooks](../reference/hooks.rst), then looks for source
157+
distributions on PyPI.
158+
159+
When a package has a plugin with a `resolver_provider` or `download_source`
160+
hook and `source` settings, then at least one `source` setting (top-level or
161+
variant) must use `provider: hooks`. The rule ensures that the hooks are
162+
used.
163+
164+
### git clone
165+
166+
Like pip's VCS feature, all git clone operations automatically retrieve all
167+
submodules recursively. The final sdist does not include a `.git` directory.
168+
Instead Fromager generates a `.git_archival.txt` file for setuptools-scm's
169+
[builtin mechanism for obtaining version numbers](https://setuptools-scm.readthedocs.io/en/latest/usage/#builtin-mechanisms-for-obtaining-version-numbers).
170+
171+
The resolver and `Candidate` class do not support VCS URLs, yet. Fromager can
172+
adopt pip's [VCS support](https://pip.pypa.io/en/stable/topics/vcs-support/)
173+
syntax. The URL `git+https://git.example/viking/viking.git@v1.1.0` clones the
174+
git repository over HTTPS and checks out the tag `v1.1.0`.
175+
176+
### Matcher factory
177+
178+
The matcher factory argument is an import string. The string must resolve to
179+
a callable that accepts a `ctx` argument and returns a `re.Pattern`
180+
(recommended) or `MatchFunction`. If the return value is a pattern object,
181+
then it must have exactly one match group. The pattern is matched with
182+
`re.match`.
183+
184+
The default matcher factory parsed the tag with `packaging.version.Version`
185+
and ignores any error. Fromager will provide additional matcher factories for
186+
common tag patterns like `v1.2`, `1.2`, and `v1.2-stable`.
187+
188+
```python
189+
import re
190+
191+
from fromager import context, resolver
192+
from packaging.version import Version
193+
194+
195+
def matcher_factory_pat(ctx: context.WorkContext) -> re.Pattern | resolver.MatchFunction:
196+
# tag must match 'v1.2+midstream.1.cpu' and results in Version("1.2+midstream.1")
197+
variant = re.escape(ctx.variant)
198+
pat = rf"^v(.*\+midstream\.\d+)\.{variant}$"
199+
return re.compile(pat)
200+
201+
202+
def matcher_factory_func(ctx: context.WorkContext) -> re.Pattern | resolver.MatchFunction:
203+
def pep440_matcher(identifier: str, item: str) -> Version | None:
204+
try:
205+
return Version(item)
206+
except ValueError:
207+
return None
208+
return pep440_matcher
209+
```
210+
211+
### Deprecations
212+
213+
- `download_source.url` is handled by `pypi-download` profile or
214+
`release_artifact` parameter of `github` or `gitlab` provider
215+
- `download_source.destination_filename` is not needed. All sdists use
216+
standard `{dist_name}-{version}.tar.gz` file name
217+
- `resolver_dist.sdist_server_url` is replaced by `index_url` parameter.
218+
All `pypi-*` profile support a custom index.
219+
- `git_options.submodules` is not needed. Like pip, Fromager will always
220+
clone all submodules.
221+
- variant settings `wheel_server_url` and `pre_build` are replaced by
222+
`pypi-prebuilt` profile
223+
224+
### Migration
225+
226+
Top-level and variant-specific `source` settings are mutually exclusive with
227+
`download_source`, `resolver_dist`, `wheel_server_url`, and `pre_build`
228+
settings. It is an error to combine the new `source` settings with any of the
229+
old settings.

docs/reference/hooks.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ Resolver hooks
121121

122122
.. currentmodule:: fromager.resolver
123123

124+
.. _resolver_provider_hook:
125+
124126
.. autofromagerhook:: default_resolver_provider
125127

126128
The ``get_resolver_provider()`` function allows an override to change
@@ -191,6 +193,11 @@ Source hooks
191193

192194
.. currentmodule:: fromager.sources
193195

196+
.. versionremoved:: 0.80.0
197+
The ``resolve_source`` hook and ``default_resolve_source`` function
198+
were removed. Define a :ref:`resolver_provider <resolver_provider_hook>` hook
199+
to resolve sources.
200+
194201
.. autofromagerhook:: default_download_source
195202

196203
The ``download_source()`` function is responsible for downloading the

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ iterable
3131
iteratively
3232
json
3333
lexicographically
34+
libcurl
3435
linter
3536
localhost
3637
matcher

0 commit comments

Comments
 (0)