Dependencies & lockfiles
#Declaring dependencies
[dependencies]
base = "[email protected]" # exactly one base per app (owns /, libc, /bin/sh)
node = "22" # any 22.x — range, not a pin
ffmpeg = { source = "github:someorg/ffmpeg-pkg", version = "6.1" }Ranges are semver prefixes: "22" means ≥22.0.0 <23, "6.1" means
≥6.1.0 <6.2. The name@version form (used for base) is the same thing
with the package name attached.
Every dependency occupies its own prefix in the final filesystem —
/opt/node-22.6.0/, /opt/ffmpeg-6.1.1/ — so two packages can never
conflict over a file. Their PATH and LD_LIBRARY_PATH contributions are
composed automatically in dependency order.
#Resolution: Minimal Version Selection
ply build resolves ranges the way Go modules do: pick the lowest
version that satisfies every constraint. No SAT solver, no "latest wins",
no surprise upgrades:
- The same manifest resolves to the same versions, forever.
- Resolution output changes only when you edit the manifest — an upgrade is a deliberate act, never a side effect of rebuilding.
- One version per package name per graph, enforced by the resolver (an overlay can't mount two ffmpegs at the same prefix — so the model doesn't pretend it can).
#The lockfile
ply build writes ply.lock next to your manifest:
- exact resolved version of every package
- the sha256 of every package file
The manifest is human intent; the lockfile is machine truth. Deploys use
the lockfile only — a production host never resolves anything, it fetches
exact hashes. Commit ply.lock to your repo like you would package-lock.json.
The hash is the trust boundary: sources are untrusted by design. A compromised mirror can serve whatever it wants — wrong bytes fail the hash check and the fetch is rejected.
#Upgrading
ply outdated # list dependencies with newer versions availableTo upgrade, widen or bump the range in ply.toml and rebuild. To patch a
runtime under an already-built app without rebuilding it:
ply rebase app.img --runtime [email protected]Only the embedded lockfile changes — fleet-wide security patching becomes a metadata operation plus one new store file.
#Vendoring vs declaring
For system-level needs there's a ladder — use the lowest rung that works:
- Vendor a static binary in your app directory (what npm's
ffmpeg-staticalready does — needs zero ply support) - Declare a package (shared in the store, cached, content-addressed)
- Fat mode (
ply bundle) — flatten everything for airgapped hosts