5.6 The provider layer
Registry
PROVIDERS in denver_providers/__init__.py maps a provider name to its
class:
PROVIDERS = {
"uv": UvProvider,
"conan": ConanProvider,
"zephyr": ZephyrProvider,
"docker": DockerProvider,
"custom": CustomProvider,
"python": PythonProvider,
"download": DownloadProvider,
"git": GitProvider,
"netrc": NetrcProvider,
"nix": NixProvider,
}
make_stage(stage_id, config) is the only place a stages: entry becomes a
live object. It reads the section’s provider: key, looks it up, and dies
with the known names if the key is missing or unknown. The stage id is never
used to guess a type.
Lifecycle
classDiagram
class Provider {
+str name
+str kind = "setup"
+tuple KEYS
+str stage
+config_section(ctx)
+resolve_defaults(ctx, cfg, config)$
+setup(ctx)
+wrap(ctx, cmd)
}
class UvProvider {
name = "uv"
+setup(ctx)
}
class ConanProvider {
name = "conan"
+setup(ctx)
}
class ZephyrProvider {
name = "zephyr"
+setup(ctx)
}
class DownloadProvider {
name = "download"
+setup(ctx)
}
class GitProvider {
name = "git"
+setup(ctx)
}
class DockerProvider {
name = "docker"
kind = "wrapper"
+setup(ctx)
+wrap(ctx, cmd)
}
class CustomProvider {
name = "custom"
+kind() str
+setup(ctx)
+wrap(ctx, cmd)
}
Provider <|-- UvProvider
Provider <|-- ConanProvider
Provider <|-- ZephyrProvider
Provider <|-- DownloadProvider
Provider <|-- GitProvider
Provider <|-- DockerProvider
Provider <|-- CustomProvider
kind is a plain class attribute everywhere except CustomProvider, where
it is a read-only property: "wrapper" when the stage sets launcher:, else
"setup".
Every provider extends Provider (base.py). Four members matter.
Member |
Meaning |
|---|---|
|
The provider type, as written in |
|
|
|
Every config key this provider understands. Drives |
|
Classmethod. Computes the complete section, once, centrally |
|
Build the env piece. Mutate |
|
Wrappers only. Return the command that should really run |
Order per run: all resolve_defaults() first, in stages: order, for every
stage. Then setup()/wrap(). So setup() never computes a default — it
reads what is already there.
--fast is read inside setup(), not centrally. Rule: skip the build, only
activate what an earlier full run produced. Nothing to activate → die with a
clear message. A provider with no such state (custom) skips itself.
Built-in providers
Provider |
Kind |
Does |
Talks to |
|---|---|---|---|
|
setup |
Create/activate a venv, install requirements, apply venv patches |
|
|
setup |
Detect profile, export recipes, install the conanfile, source |
|
|
setup |
Init/update a West workspace, apply patches, fetch blobs, set |
|
|
setup |
Fetch release archives, verify checksums, unpack, extend |
HTTP(S) |
|
setup |
Clone or fetch a checkout, pinned to one revision (detached) |
|
|
setup |
Prepare a |
none (HTTP only) |
|
setup |
Evaluate a flake’s devShell once, cache it, source it into the environment |
|
|
wrapper |
Build/enter a compose service, re-invoke denver inside it |
|
|
setup, or wrapper with |
Run one command, source a script, or relocate the final command |
whatever the command is |
|
setup |
Import a |
whatever the function does |
Each provider’s own page under Providers is the key reference. These chapters only cover the design.
download and git exist because the same shell script kept getting
rewritten per project: download, checksum, unpack, PATH — or clone, fetch,
checkout a pinned tag. Both are idempotent and --fast-aware once they are
providers.
nix is the same argument for a project that already has a flake: it
sources nix print-dev-env’s output rather than relocating into nix develop --command, so it stays a setup provider — stages after it still
extend the environment it built, and its cached evaluation is what makes it
cheap enough to sit in front of every command.
python sits between custom and an extension provider: one function, no
class, no registration. It runs in denver’s own process, so it can be
debugged together with denver.
Extension providers
A project can add its own provider without forking denver:
extensions:
providers:
dirs: [my_providers]
Every
*.pydirectly in a listed dir is imported. Files starting with_are skipped.Each file must define
PROVIDER, aProvidersubclass.It registers under its own
nameand is then usable asprovider: <name>, exactly like a built-in.Unknown keys under
extensions:are an error, so a typo cannot silently disable the mechanism.
Why this shape
Adding a provider touches one file plus one registry line.
run_stages()never changes.The orchestrator knows no provider specifics, apart from one table of default resolvers.
Test doubles are trivial: patch
Context.run()/which()and the whole layer is offline.