Skip to main content

SETUP

Post-install work on the host machine, for things that cannot live inside an immutable package directory: udev rules, OS packages, license activation, a system daemon.

SETUP is a table of named CHECK/INSTALL pairs. CHECK asks whether the host is already correct. INSTALL makes it correct. Nothing in SETUP runs unless a manifest entry names the pair.

SETUP = {
udev_rules = {
PLATFORMS = { "linux" },
CHECK = function(pkg_dir, opts)
local r = envy.run("cmp -s " .. pkg_dir .. "99-jlink.rules /etc/udev/rules.d/99-jlink.rules",
{ quiet = true, check = false })
return r.exit_code == 0
end,
INSTALL = function(pkg_dir, opts)
envy.run({
"sudo cp " .. pkg_dir .. "99-jlink.rules /etc/udev/rules.d/",
"sudo udevadm control -R",
}, { interactive = true })
end,
},
}
envy.lua: the pair runs only because the entry selects it
PACKAGES = {
{ spec = "acme.jlink@r1", source = specs .. "acme.jlink.lua",
options = { version = "9.30" },
setup = { "udev_rules" } },
}

Pair fields​

FieldRequiredMeaning
CHECKyesIs the host already satisfied?
INSTALLyesMake it satisfied.
PLATFORMSnoRestrict the pair, for example { "linux" }. A pair that does not match is skipped rather than failed.
DEPENDSnoNames of sibling pairs that must complete first. Unknown names and cycles are rejected when the spec loads.

Any other field is an error, and so is a top-level CHECK outside a pair. Pair names may contain alphanumerics, _, ., and -.

Verb shapes​

Verbstringfunction
CHECKShell command. Exit 0 means already satisfied.CHECK(pkg_dir, opts) returning true or false, or a string that runs as a shell command whose exit status decides.
INSTALLShell script.INSTALL(pkg_dir, opts) returning nothing, or a string that runs as a shell script.

pkg_dir is the installed package directory, with a trailing separator, for a cache-managed package. It is nil for a user-managed one, which has no package tree.

Both verbs run with the working directory set to the project root rather than a cache directory, because host state is the subject.

A CHECK as a bare string is often enough:

SETUP = {
brew = {
CHECK = "brew --version",
INSTALL = function(pkg_dir, opts)
envy.run({
"sudo -v",
'curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | bash',
}, { env = { NONINTERACTIVE = "1" }, interactive = true })
end,
},
}

Pass interactive = true to envy.run when the command needs the terminal, for example a sudo password prompt or a license agreement. Output streams through instead of being captured.

envy gives the child the terminal for as long as it runs, and prints nothing else meanwhile. Before envy 0.4.3 queued log lines were still flushed during that time, so under --verbose another package's debug output could print over the prompt the child was waiting on.

How a pair executes​

  1. Platform filter. A PLATFORMS mismatch skips the pair.
  2. CHECK. If satisfied, nothing runs.
  3. Lock. envy takes a cross-process lock for this (package, pair), so two concurrent envy sync runs cannot both install the same udev rule.
  4. CHECK again, because the other process may have just done the work.
  5. INSTALL.

A pair is therefore idempotent, and CHECK is the only re-run gate. Nothing about a pair is cached: its lock entry is ephemeral and always purged, so every sync re-asks the question. Keep CHECK a fast query rather than a full audit.

While INSTALL runs, its row shows the package's [identity] and the spec's DISPLAY. On success the row goes away and the package row says setup complete. On failure the row keeps its last output, and the error names the pair:

error: SETUP.udev_rules.INSTALL shell script failed for acme.jlink@r1 (exit code 1)

Before envy 0.4.10 the label also held the options and pair name, as in [acme.jlink@r1{["version"]="9.30"}#setup:udev_rules].

Selection lives in the manifest​

Pairs are opt-in per project, by name:

nothing runs: the pairs are available but unselected
PACKAGES = {
{ spec = "acme.jlink@r1", source = specs .. "acme.jlink.lua",
options = { version = "9.30" } },
}
udev rules on a developer machine, but not on a CI runner
PACKAGES = {
{ spec = "acme.jlink@r1", source = specs .. "acme.jlink.lua",
options = { version = "9.30" },
setup = not os.getenv("CI") and { "udev_rules" } or nil },
}

Selections union across everyone who asks. A spec can demand a pair from one of its own dependencies:

DEPENDENCIES = {
{ spec = "acme.brew@r0", source = "acme.brew.lua", setup = { "brew" } },
}

So a spec that installs a Homebrew formula can insist Homebrew exists first, without the project needing to know. Selecting a pair also selects everything it DEPENDS on.

Per-platform pairs​

One spec can carry a pair per platform and let the filter decide. PLATFORMS on the pair is independent of the spec's own:

SETUP = {
udev_rules = {
PLATFORMS = { "linux" },
CHECK = "test -f /etc/udev/rules.d/99-acme.rules",
INSTALL = "sudo cp packaging/99-acme.rules /etc/udev/rules.d/",
},

driver = {
PLATFORMS = { "windows" },
CHECK = 'pnputil /enum-drivers | Select-String -Quiet "acme.inf"',
INSTALL = function(pkg_dir, opts)
envy.run('pnputil /add-driver "' .. envy.path.join(pkg_dir, "driver", "acme.inf") .. '" /install',
{ interactive = true })
end,
},
}

A manifest can then select { "udev_rules", "driver" } unconditionally, and each runs only where it applies. Remember that the strings run under the platform default interpreter, bash on POSIX and PowerShell on Windows, so Select-String and test -f never meet each other. Anything needing elevation, sudo or a UAC prompt, wants interactive = true.

Selection does not change identity​

A pair selection is not part of the package's identity or hash. The same cached package, and the same depot artifact, serves a machine that selected udev_rules and one that did not. The selection only decides which host-side questions get asked.

Options behave the opposite way. Change an option and you get a different package. Change a setup selection and you get the same package, checked differently.

Pure-setup specs​

A spec can be nothing but pairs, with no fetch, no build, and nothing cached. That is a user-managed package: Homebrew itself, apt packages, a kernel module.

See also​