custom provider
A custom stage is the escape hatch: an arbitrary shell command, for
whatever a project needs that doesn’t fit uv/conan/zephyr/docker.
[my-stage]
provider = "custom"
cmd = "echo hello"
(provider:/description:/disabled:/depends-on:/scripts:/env:/env-prepend:/env-append: are generic keys every stage has —
see “Generic stage keys” in Configuration. Everything below is specific to custom.)
Key reference
cmd— run viabash -cin an isolated subprocess, so ordinary shell syntax (pipes,&&, quoting,$VARexpansion against the current environment) works the same way it would on a command line — but anything it exports dies with that subprocess, denver never sees it. Denver’s own${VAR}/${VAR:-fallback}interpolation (see “Variable interpolation” in Configuration) is shell-quoted here beforecmd:ever reaches bash — a substituted value lands as one literal argument/word, whatever characters it contains, rather than being re-parsed as shell syntax. This is what makescmd = "echo ${SOME_VAR}"safe even whenSOME_VARcomes from somewhere outside this file’s own control (a-e/--envoverride, a branch name, a CI-injected secret). Write${VAR}bare, the way that example does — don’t additionally wrap it in your own quotes ("${VAR}"): denver’s quoting already makes it one safe word, and bash would then see your literal"/'characters as part of the text, not as quoting, corrupting the value. Bash’s own bare$VAR(no braces, e.g."$DENVER_DOWNLOAD_ARCHIVE"— seedownload.md) is untouched by denver and quotes exactly as it always has.source— different: it names a script sourced (not run) right aftercmd, so its exports fold into the environment and persist into every later stage and the final command. This is the way to make acustomstage hand environment variables forward, scoped to this one stage’s section rather than the globalhooks:mechanism (see Configuration).launcher— makes this stage a wrapper, the same waydockeris one: instead of (only) doing its own work, it prepends its own script(s) ahead of whatever command would otherwise run. Each entry is a string, split on whitespace into its own tokens (plain whitespace splitting, no shell parsing), and every entry’s tokens land in order, ahead of the actual command:launcher = [ "myscript.sh --", "otherscript.sh --", ]
turns a resolved command of
<cmd>intomyscript.sh -- otherscript.sh -- <cmd>.
At least one of cmd/source/launcher must be given. cmd:/source:
(if also given) still run as usual during this stage’s own setup even when
launcher: is set — launcher: only changes what happens to the final
command.
Worked example: bringing a prebuilt binary in by hand
The common shape for “download a release tarball and put it on PATH” uses
both keys, because installing and activating are two different jobs:
[nvim-setup]
provider = "custom"
cmd = "bash ${DENVER_ENV_DIR}/nvim/install.sh" # download, checksum, unpack
source = "nvim/activate.sh" # export PATH=...
cmd:is the build step: an isolated subprocess is exactly right for it (it prints its progress, and it is correctly skipped by--fast), and it needs to export nothing. It must be idempotent by itself — denver fingerprints auv/conanstage’s inputs, but it cannot know what an arbitrary command changed, so the script runs on every start and has to recognise its own previous result.source:is the activation: oneexport PATH=, sourced so it reaches every later stage and the final command. Keeping it out ofcmd:is not style — an export incmd:dies with that subprocess; and keeping the download out ofsource:matters just as much, since a sourced script also runs under--fastand--dry-run.One script would work too: check, download, unpack and
export PATH=all in a singlesource:script, with nocmd:at all. The split buys visible download progress (a sourced script’s output is captured, since denver reads the resulting environment out of it) and--fast/--dry-runskipping the expensive half. A single sourced script must neverexit, which would end denver’s sourcing shell before it reads the environment back.The two scripts share their pin (version, url, checksum, install prefix) via a third file both source, so they cannot drift apart. Unpack into
${DENVER_ENV_WORKDIR}(denver’s per-env state dir) rather than/usr/local/bin: no root needed, no leaking into other environments, and deleting the env deletes the tool.
When there are several such tools, or a cache worth sharing between
environments, this is the point to move the job to
conan — which does all of the above, per tool, for a url and a
checksum.
Design notes
cmd:vssource:. Aprovider: customstage’s owncmd:never sees its own exports (it’s an isolated subprocess) —source:exists for exactly the case where a stage needs to hand something forward.--fast. An arbitrary command has no “already built, just activate” state denver can reason about, socmd:is skipped entirely under--fast.source:still runs under--fast— it’s what later stages’/the final command’s environment depends on, not a build step, so skipping it would break the very propagation it exists for.launcher:is likewise never skipped under--fast— relocating the command isn’t a build step either.--dry-runprintscmd:instead of running it.source:is still sourced, for the same reason it survives--fast: its exports are what every later stage’s commands are rendered against, so skipping it would make the preview show${...}values a real run would never use.