Files
computer-playbook/utils/env/handlers/README.md
kevinveenbirkenbachandClaude Opus 5 79ba511bb7 fix(env): keep the .env generator stdlib-only
The env generator runs on the bare bootstrap python, before any
dependency is installed. utils/distros.py reached PyYAML through
utils.cache.yaml, so `python -m cli.meta.env` died with
ModuleNotFoundError on every dev-runtime image that ships python3
without PyYAML.

utils/yaml_bootstrap.load_block reads the block-YAML subset the SPOT
files use with stdlib only. It raises BootstrapYamlError on anything it
cannot parse identically to yaml.safe_load, so meta/distros.yml fails
loud instead of drifting.

The per-line regex lint meant to guard this is gone: one hop of
indirection defeated it and it stayed green while the invariant was
broken. It is replaced by a test that runs the real generator with
every non-stdlib import blocked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:47:01 +02:00

67 lines
3.9 KiB
Markdown

# utils/env/handlers 🧩
Per-variable mapping logic for the `.env` generator.
For the package's overall role see [README.md](../README.md).
For general README conventions see [documentation.md](../../../docs/contributing/documentation.md).
## Purpose 🎯
Each module in this directory owns exactly one variable, or one tightly-coupled group resolved by a single helper call.
The orchestrator in [builder.py](../builder.py) iterates the registry in [__init__.py](__init__.py) and calls each handler's `apply(eb, ctx)` in order.
## File Naming 🏷️
- A handler module name MUST match the lowercase of the variable it owns:
- `INFINITO_CONTAINER` -> `infinito/container.py`
- `INFINITO_GHCR_MIRROR_PREFIX` -> `infinito/ghcr_mirror_prefix.py`
- The `infinito_` prefix in the filename MUST be present iff the owned key carries the `INFINITO_` prefix.
- Aggregate handlers (one helper call resolves several keys at once) MUST take the singular topic name without a key suffix.
Example: `infinito/inventory.py` resolves `INFINITO_INVENTORY_DIR`, `INFINITO_INVENTORY_FILE`, and `INFINITO_INVENTORY_HOST_VARS_FILE` from one `scripts/inventory/resolve.sh` invocation.
- The trivial static passthroughs live in two collector files: `passthrough.py` (always emitted) and `gha_passthrough.py` (emitted only when `ctx.on_gha` is true).
## Module Shape 📐
Every handler module MUST expose the same surface so the registry can call it uniformly:
```python
"""<one-line docstring: what variable, what it derives from>."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
KEY = "INFINITO_..."
COMMENT = "<single-line per-key comment, mirrors default.env style>"
def apply(eb: EnvBuilder, ctx: BuildContext) -> None: ...
```
- `KEY` MUST be the owned variable's name.
- `COMMENT` MUST be a single line, no em dashes, that ends up above the key in the generated `.env`.
- `apply` MUST take `(eb, ctx)` and return `None`.
- A handler that targets a conditional branch (GHA-only, NIX-only, ...) MUST self-check the relevant `ctx` flag and return early.
The registry calls every handler unconditionally.
## Import Rules 🔗
- A handler MUST NOT import another handler.
Cross-handler data flows through `eb.get(...)` / `eb.set(...)` only.
- A handler MAY import stdlib, [parser.py](../parser.py), [runtime.py](../runtime.py), and any repo module whose own import closure is stdlib-only.
The whole closure MUST import and run on the bare bootstrap python, before any dependency is installed.
`utils.cache.files` and [yaml_bootstrap.py](../../yaml_bootstrap.py) qualify; `utils.cache.yaml` does not, because it imports PyYAML.
Read SPOT files with `utils.cache.files.read_text` plus `utils.yaml_bootstrap.load_block` or a stdlib line parse.
`load_block` accepts block mappings, block lists and string scalars only, and raises `BootstrapYamlError` on everything else, so a SPOT file read this way MUST stay inside that subset.
[test_bare_bootstrap.py](../../../tests/integration/meta/env/test_bare_bootstrap.py) enforces this by running the generator with every non-stdlib import blocked.
- A handler MUST NOT import from [writer.py](../writer.py) or [builder.py](../builder.py) at runtime.
The `EnvBuilder` and `BuildContext` types MAY be referenced under `TYPE_CHECKING` for annotations only.
## Registry 📋
- [__init__.py](__init__.py) MUST list every handler module in `ORDERED_HANDLERS`.
- The order in `ORDERED_HANDLERS` MUST respect data dependencies.
Example: `infinito_container` runs after `passthrough` so it can read the resolved `INFINITO_DISTRO`.
- Adding a handler MUST also bump the relevant baseline test in [test_dotenv_generator.py](../../../tests/integration/meta/env/test_dotenv_generator.py) when the new key is a static-default in [default.env](../../../default.env), so the drift test stays honest.