You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add sync_toolchains/check_toolchains actions and Python interpreter derivation (ADR-0053)
Implements the toolchain-axis derivation promised by the previous
commit's requires-python backfill: fine_envs.sync_toolchains derives
each env's toolchain axis from the project's declared support range
and materializes it into the project definition file, dispatching per
language to a subaction (fine_python_lang.sync_python_interpreters).
check_toolchains re-derives with save=False and fails on drift, for
wiring into precommit/CI (bridged via fine_git_hooks).
The Python subaction expands requires-python against the set of
interpreters uv can actually obtain (list_obtainable_toolchains /
list_obtainable_python_interpreters, backed by
fine_python_uv.UvListObtainablePythonInterpretersHandler), so an open
upper bound is resolved against what is installable rather than
rejected or left to silently backtrack. The axis is materialized
rather than recomputed at read time so config resolution stays a pure
read and a preset bump can't change a project's matrix without a
diff — see docs/reference/actions.md and ADR-0053 for the full
rationale on both actions and the derive/materialize/check shape.
To update lock files, run `lock_dependencies` locally or in a scheduled CI job and commit the result. For multi-platform projects, use a CI matrix to generate lock files on each target platform.
354
354
355
+
## `requires-python`: no upper bound
356
+
357
+
FineCode's own packages declare a **lower bound only** on `requires-python` (e.g. `>=3.11`), never an upper bound (`< 3.15`, `<= 3.14`).
358
+
359
+
An upper bound is a packaging anti-pattern for published packages: a resolver that cannot satisfy the cap **backtracks to an older release** of the package rather than failing cleanly, so a consumer on a newer Python silently gets a stale version instead of a clear "not supported yet" error. See [ADR-0053](../../../finecode_internal_docs/adr/0053-derived-interpreter-axis-is-materialized-into-config.md) for the full rationale.
360
+
361
+
The cap also has no remaining job now that the interpreter matrix exists. The set of Python versions an action is tested against is **derived from `requires-python` and bounded by what the provisioning toolchain (uv) can actually obtain** (ADR-0053, part 5), not by a hand-written ceiling. So removing the upper bound does not widen the test matrix to unreleased versions — the obtainable-versions ceiling does that job, on the developer's clock and in a reviewable diff.
362
+
363
+
If a genuinely newer Python breaks a package, fix it when that version exists — do not pre-emptively cap. New packages must follow this: declare `requires-python = ">=<min>"` with no upper component.
364
+
355
365
## JSON-RPC key naming convention
356
366
357
367
All JSON-RPC channels in FineCode use **camelCase** for message keys:
Copy file name to clipboardExpand all lines: docs/reference/actions.md
+123Lines changed: 123 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -265,6 +265,129 @@ Install dependencies into a specific environment.
265
265
266
266
---
267
267
268
+
## `sync_toolchains`
269
+
270
+
Derive each environment's toolchain axis from the project's declared support range and write it into the project definition file.
271
+
272
+
-**Source:**`fine_envs.SyncToolchainsAction`
273
+
274
+
**Payload fields:**
275
+
276
+
| Field | Type | Default | Description |
277
+
|---|---|---|---|
278
+
|`project_def_path`|`Path \| None`|`None`| Project definition file declaring the envs. `None` means the current project. |
279
+
|`save`|`bool`|`True`| Write the derived axis to the file. `False` derives and reports without writing. |
280
+
281
+
**Result fields:**
282
+
283
+
| Field | Type | Description |
284
+
|---|---|---|
285
+
|`axes`|`list[EnvToolchainAxis]`| Per env: `declared`, `derived`, and whether it `changed`|
286
+
|`saved`|`bool`| Whether a derived axis was written |
287
+
288
+
A **toolchain** is the implementation-and-version a project is executed against; in Python it is an [interpreter](../glossary.md#interpreter). Every ecosystem declares its support range somewhere (`requires-python`, `engines`, `required_ruby_version`), and a language handler expands that range into toolchain identities. The action dispatches on project language to the matching subaction.
289
+
290
+
The axis is *materialized* — written to the file rather than recomputed on each run — so that config resolution stays a pure read of already-declared data. See [ADR-0053](../adr/0053-derived-interpreter-axis-is-materialized-into-config.md) for why, and note the consequence: the axis is wholly generated, so extra toolchains are configured as *inputs to the source* (`extra_interpreters`) rather than hand-added to its output.
291
+
292
+
---
293
+
294
+
## `check_toolchains`
295
+
296
+
Check whether each environment's materialized toolchain axis still matches what the source derives. Fails with a non-zero return code on drift.
297
+
298
+
-**Source:**`fine_envs.CheckToolchainsAction`
299
+
300
+
**Payload fields:**
301
+
302
+
| Field | Type | Default | Description |
303
+
|---|---|---|---|
304
+
|`project_def_path`|`Path \| None`|`None`| Project definition file declaring the envs. `None` means the current project. |
305
+
306
+
**Result fields:**
307
+
308
+
| Field | Type | Description |
309
+
|---|---|---|
310
+
|`stale_axes`|`list[EnvToolchainAxis]`| Envs whose declared axis differs from the derived one |
311
+
312
+
A generated, committed axis can go stale — the support range changes, or the source learns about a newer toolchain. That is the same staleness a lock file has, and it is caught the same way: re-derive and compare. Wire this into `precommit` and CI. Runs `sync_toolchains` with `save = False` and reports what would change.
313
+
314
+
---
315
+
316
+
## `sync_python_interpreters`
317
+
318
+
Derive an environment's Python interpreter axis from `requires-python`. Language-specific subaction of `sync_toolchains`.
**Payload fields:** same as `sync_toolchains`. **Result fields:** same as `sync_toolchains`.
325
+
326
+
**Handler config:**
327
+
328
+
| Field | Type | Default | Description |
329
+
|---|---|---|---|
330
+
|`envs`|`list[str]`|`[]`| Envs whose interpreter axis is derived. Empty means none — the action is a no-op. An env either derives its axis or has one pinned, never both. |
331
+
|`max_supported_python`|`str \| None`|`None`| Cap the newest CPython to derive. `None` means no cap beyond what is obtainable. |
332
+
|`extra_interpreters`|`list[str]`|`[]`| Interpreters beyond the derived CPython rows, e.g. `["pypy@3.11"]`. |
333
+
334
+
`requires-python` is a *specifier*, not an enumeration, so it is expanded against the set of **obtainable** interpreters (see `list_obtainable_toolchains` below). An open upper bound (`>=3.11`) — the correct form for a published package — is bounded by that set rather than rejected. The result therefore depends on something outside the specifier, which is exactly why it is persisted.
335
+
336
+
`requires-python` constrains version only and carries no implementation, so the derived axis is CPython-only. PyPy and friends are configured via `extra_interpreters`.
337
+
338
+
Matrices stay opt-in: with no `envs` configured, nothing is derived and every action keeps running in a single environment with an unchanged result shape.
339
+
340
+
The derived axis is written into the **project's own** definition file, and project config beats preset config. So if a preset pins `interpreters` for an env that is also listed in `envs`, the derived axis is materialized over it and the run warns once, since the pin stops having any effect. To keep the preset's axis instead, drop that env from `envs` in your own config. Deriving into your own file is also the only way to override a pinned axis at all, because config layering can replace a key but never unset one.
341
+
342
+
Materializing once per project rather than sharing one axis from a preset is deliberate: the axis derives from `requires-python`, which is per-project, and a project that states its own axis cannot have its matrix changed by a preset bump without a diff. See [ADR-0053](../adr/0053-derived-interpreter-axis-is-materialized-into-config.md) and `SyncPythonInterpretersPyHandler`'s docstring.
343
+
344
+
---
345
+
346
+
## `list_obtainable_toolchains`
347
+
348
+
List the toolchains the environment provisioner is able to obtain.
|`include_prereleases`|`bool`|`False`| Include prerelease toolchains (e.g. a Python beta). |
357
+
358
+
**Result fields:**
359
+
360
+
| Field | Type | Description |
361
+
|---|---|---|
362
+
|`toolchains`|`list[str]`| Canonical identities, e.g. `cpython@3.13` — no patch level, variant, or platform tag |
363
+
364
+
**"Obtainable" is deliberately not "installed".** This reports what the *provisioner* can get — a property of a locked dependency — not what happens to be present on this machine. Only the former may feed a derived matrix axis: an axis sourced from local installs would differ between developers on the same commit. Whether a toolchain is available *here* is a separate question, and would be a separate action.
365
+
366
+
The provisioner is the authority because deriving a version it cannot obtain yields an axis whose environments cannot be created. This is what `sync_toolchains` expands `requires-python` against.
367
+
368
+
---
369
+
370
+
## `list_obtainable_python_interpreters`
371
+
372
+
Language-specific subaction of `list_obtainable_toolchains`. Backed by uv.
**Payload and result fields:** same as `list_obtainable_toolchains`.
379
+
380
+
**Handler config:**
381
+
382
+
| Field | Type | Default | Description |
383
+
|---|---|---|---|
384
+
|`variant`|`str`|`"default"`| Build variant to report. `freethreaded` builds are a separate variant the `(implementation, version)` identity cannot express. |
385
+
386
+
Runs `uv python list --only-downloads`, which reports uv's own manifest rather than the machine's installed Pythons. uv's listing is far finer-grained than a matrix axis — patch levels, prereleases, freethreaded variants, platform tags — and all of that is collapsed to one identity per implementation and minor version. Prereleases are excluded by default, so a released beta (`cpython-3.15.0b1`) never enters an axis.
387
+
388
+
---
389
+
390
+
268
391
## `dump_config`
269
392
270
393
Dump the resolved configuration for a source artifact that includes FineCode configuration.
0 commit comments