Reproducibility
How a clone of a repo becomes the same toolchain on every machine, and where the edges are.
There is no lockfile
envy does not solve version ranges, so there is nothing to lock. Every version is already exact, and every pin lives in the manifest or in a spec that the manifest pins. The manifest is the lockfile.
That removes a whole class of problems. There is no lockfile to conflict in a merge, no "regenerate the lockfile" step, and no gap between what the lockfile says and what the manifest asks for.
The pin inventory
Four tiers, all visible in one file:
-- @envy schema "1"
-- @envy version "0.2.0" -- 1
-- @envy sha256sums "a17e9c4fbb2d1e07c5a9f0d3e8b47c61f2a09d5e4c3b8a7f6e5d4c3b2a1908f7e"
-- @envy mirror "https://envy-mirror.acme.example"
-- @envy bin "bin"
-- @envy deploy "true"
BUNDLES = {
envy = {
identity = "envy.package-specs@r3",
source = "https://github.com/envy-package-manager/package-specs.git",
-- envy git-resolve https://github.com/envy-package-manager/package-specs main
ref = "ded36a39bbf13744f5a0e539f2f4741fecb61dd0", -- 2
},
}
PACKAGES = {
{ spec = "envy.cmake@r0", bundle = "envy",
options = { version = "4.4.0" } }, -- 4
{ spec = "acme.protoc-gen-acme@r0",
source = "https://specs.acme.example/protoc-gen-acme@r0.lua",
sha256 = "9f2c1d5b8e47a03f6c2d9b1e4a7f0c3d8b5e2a9f4c1d7b0e3a6f9c2d5b8e1a4f", -- 2
options = { version = "2.1.0" } },
}
- envy itself.
@envy versionnames the release, and@envy sha256sumsattests it. - Specs. A
sha256for a spec fetched over https, a full commitreffor git, and a bundlereffor a whole bundle of specs. - Artifacts. A
sha256inside each spec'sFETCH, usually from a fingerprint table keyed by version and platform. See FETCH. - Tool versions.
options, which are part of the package's identity, so changing one names a different package rather than mutating an existing one.
Use envy git-resolve to turn a branch or tag
into the sha you pin, and leave the command in a comment so the next person knows
how to advance it.
envy verifies itself
The committed bootstrap script is plain shell and batch, and it does the verification before running anything:
- It reads
@envy versionand@envy sha256sumsout of the manifest header as text. - It downloads
<mirror>/v<version>/SHA256SUMSand hashes that file. A mismatch against the pin aborts. - It looks up the platform archive's entry inside that now-trusted
SHA256SUMS, downloads the archive, and verifies it too.
One pin therefore covers all six platform archives, so a single
@envy sha256sums value works in a cross-platform repo. It also means a
mirror cannot tamper undetected: republishing requires producing a SHA256SUMS
whose hash matches a value already committed to your git history.
Two details that keep this from degrading quietly. @envy sha256sums without
@envy version is rejected, by envy and by the bootstrap script, because a pin
that names one release is meaningless when the version is resolved dynamically.
And if the pin is set but the machine has no sha256sum, shasum, or openssl,
bootstrap fails rather than skipping verification.
Re-exec: running the version the manifest pins
The bootstrap script handles a machine with no envy. Re-exec handles the more common case: a machine that has one, but not the version this project pins.
Every manifest-aware command reads @envy version before doing any work. If
that is not the version running, envy gets that release, replaces itself with
it, and the original process stops there. So the version that installs your
packages is always the one the manifest names, not whichever one was on your
PATH.
envy looks for the release in the project's own cache tree first, then the
user-wide tree, and downloads it from the mirror into a temporary
directory only if neither has it. If the manifest pins @envy sha256sums, a
downloaded archive is verified before it is unpacked, on the terms described
above. The new binary installs itself into the cache,
so the next run finds it instead of downloading it again.
The hand-off happens once: the child knows it is the result of a re-exec and
does not check the pin again, so there is no loop. The child also gets your
arguments minus any option that selected which envy to run. envy init --envy-version 0.4.2 drops that flag, because it means nothing to the binary it
selected and an older release would reject it as unknown.
envy does not re-exec at all when the manifest has no @envy version, when the
version already matches, when the running binary is a development build
(0.0.0, built from a working tree, so its version says nothing about what it
supports), or when ENVY_NO_REEXEC is set. That variable is for debugging. It
makes the binary you typed do the work, which is also the only way a deploy
can stamp scripts from a version the manifest does not pin. envy warns when that
happens.
One case is an error rather than a hand-off. A manifest that resolves a
project-local cache — through @envy cache-local, cache-mode,
state-dir, or a marker left by envy cache --local — while pinning an envy
older than 0.2.0 is refused. That envy does not understand those directives, so
it would use the shared cache and exit 0, which is exactly what a local cache
exists to prevent.
Three commands do not re-exec, and all three read the manifest header as text
instead of running it: use retargets the pin,
cache reports where things live, and
version reports the running binary rather than
the pinned one. Each has to work when the pinned envy is the thing that cannot
run, which is usually why you are running them.
What happens without a version pin
An unpinned project resolves a version at run time, in this order: a latest
marker in the local cache, then a latest file at the mirror, then GitHub's
redirect for the newest release, then the fallback version stamped into the
script when envy init wrote it.
That is convenient and it is not reproducible. Pin the version for anything shared.
Mirrors
@envy mirror and ENVY_MIRROR redirect where releases come from.
envy mirror-envy populates one, copying
SHA256SUMS byte for byte so an existing pin keeps verifying.
Prefer the https:// form of a bucket, for example
https://acme-envy-mirror.s3.us-east-1.amazonaws.com. envy itself speaks
s3:// natively, but the bootstrap script is plain shell with no AWS SDK, so an
s3:// mirror makes it require the AWS CLI on every machine that bootstraps.
The same pins on every platform
One manifest covers all three platforms, and the pins are shared where they can be:
| Pin | Scope |
|---|---|
@envy version | all platforms. One release, one version number. |
@envy sha256sums | all platforms. It pins the release's SHA256SUMS file, which lists every platform's archive, so one value verifies the macOS, Linux, and Windows binaries. |
A spec's sha256 | per artifact. A spec that downloads per-platform archives records one hash each. |
A bundle ref | all platforms. Git commits are not platform-specific. |
So a Windows machine and a Linux machine reading the same manifest install the same envy, the same specs, and the same tool versions. What differs is only the artifact each spec picks for the host, and each of those is hashed.
Two things to keep in the checklist for a cross-platform repo:
- Commit the Windows bootstrap and wrappers, not just the POSIX ones. Run
envy sync --platform allso both flavors exist even if nobody on the team uses Windows today. - Cover each platform in CI. A spec's Windows branch is only proven by a Windows runner, and a per-platform hash table is only proven where it is used.
Known limits
Stated plainly, because each one is a place where "reproducible" needs a qualifier.
- Verification is opt-in per artifact. A
FETCHwith nosha256is a reproducibility hole, and a performance one: an unverified download is re-fetched on every attempt rather than trusted from cache. - Spec contents are not part of the cache key. An entry is named by identity
plus options, so editing a spec in place, changing its URL or its build, reuses
the existing package. Bump the spec revision,
@r0to@r1, when behavior changes. That is what revisions are for. - User-managed packages are as reproducible as the host.
Those specs run
aptorbrewagainst a machine envy does not own. envy guarantees the ordering and the idempotence check, not the resulting version. - Depots guarantee equivalent inputs, not bit-identical rebuilds. A depot hit
means an artifact was built from the same identity, options, and platform. If
you need to prove a build reproduces, run it with
--ignore-depotand compare, and usemerge-depot --strictin publishing so a changed hash fails the pipeline instead of quietly winning. envy runand shell hooks inherit your environment. Package resolution is hermetic, but a build script that readsCCorPATHfor something envy did not provide is not.
A checklist for teams
@envy versionand@envy sha256sumsin every manifest, including subproject manifests.- Every git source pinned to a full commit sha, never a branch or tag.
- Every remote spec pinned with
sha256. - Every
FETCHpinned withsha256, in a fingerprint table so adding a version is a reviewable diff. - A nightly CI job that builds with
ENVY_IGNORE_DEPOT=1, so a spec that stopped building from source is caught before someone needs it. - Spec revisions bumped when behavior changes, not just when content does.
See Pinning & Updating for the workflow that moves these pins on purpose.
See also
envy usefor retargeting the envy pin.- The Cache for what the identity hash covers.
- Package Depots for the accelerator and its trade-off.