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,
"download": DownloadProvider,
"git": GitProvider,
"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 |
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 |
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.
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.