|
| 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