proto v0.61 - Swift support, GPG verification, and immutable lockfiles
A new language joins the toolchain, downloads can now be verified with GPG signatures, and lockfiles get the CI story they've been missing.
Swift support
proto now ships with unstable Swift support, our first new language since Java landed in v0.59. Swift has grown well beyond Apple platforms, and installing a specific toolchain version has historically meant reaching for a platform specific installer, so it felt like a natural fit.
$ proto install swift
This provides the swift, swiftc, and sourcekit-lsp binaries, and detects versions from
.swift-version and
Package.swift files.
Swift support is still unstable, so please report any issues you run into!
GPG signature verification
Until now, proto could verify a downloaded artifact against a hash (.sha256, .sha512) or a
minisign signature (.minisig). That covers a good chunk of
the ecosystem, but plenty of projects sign their releases with GPG instead, and those downloads were
simply left unverified.
In this release, a plugin can point checksum_url at a .sig or .asc detached signature, and
provide the project's armored public keyring through checksum_public_key. proto handles the rest.
Ok(Json(DownloadPrebuiltOutput {
download_url: format!("https://example.com/v{version}/{filename}"),
checksum_url: Some(format!("https://example.com/v{version}/{filename}.asc")),
checksum_public_key: Some(PUBLIC_KEY.into()),
..DownloadPrebuiltOutput::default()
}))
Real-world signing setups are messier than the specification suggests, so we've been deliberately permissive about what we accept. A keyring may contain multiple certificates in a single armored block, or multiple concatenated blocks. A signature file may contain multiple binary signatures, multiple signatures in one armored block, or multiple concatenated blocks — verification succeeds when any signature matches a trusted key. This is how projects that rotate signing keys, or sign with several, actually publish in practice.
Verification streams the artifact from disk in a blocking worker, so signing a multi-hundred-megabyte toolchain archive doesn't balloon memory. And rather than storing the entire armored keyring in your lockfile, we record only what's needed to prove the check happened: the fingerprint of the signer that was trusted, and a SHA-256 hash of the verified artifact.
checksum = "gpg:0123456789ABCDEF0123456789ABCDEF01234567:sha256:9f86d0..."
Immutable lockfiles
Lockfiles have been steadily maturing since v0.51, but they've been missing the one guarantee that makes a lockfile genuinely useful in CI: a way to fail when the lockfile is out of date, instead of quietly resolving something new.
That's what --immutable-lockfile (and the PROTO_IMMUTABLE_LOCKFILE environment variable) does.
With the flag enabled, the lockfile is treated as read-only.
$ proto install --immutable-lockfile
- Versions are resolved only from existing lockfile records, never from the remote registry.
- The lockfile is never created, updated, or pruned.
- If a tool being installed has no matching record, the install fails with a clear error.
If you've used npm ci, yarn --immutable, or uv sync --locked, this should feel familiar. Drop
it into your CI pipeline, and a teammate who bumped a version without committing the lockfile gets a
red build instead of a machine that silently disagrees with everyone else's.
Environment scoped lockfiles
proto has supported environment specific configuration —
.prototools.production, .prototools.development, and so on — since v0.29. Lockfiles never caught
up though: every configuration file in a directory shared a single .protolock, so whichever
environment ran last would stomp the records of the others.
In this release, every configuration file owns its own lockfile. When PROTO_ENV is set, a
.prototools.<env> file is locked to a sibling .protolock.<env> file.
.prototools -> .protolock
.prototools.production -> .protolock.production
Each lockfile only tracks tools with versions defined in its own configuration file. A version
overridden by an environment configuration is tracked in .protolock.<env>, while versions inherited
from the base .prototools stay in .protolock. Environment configurations inherit the
unstable-lockfile setting from the .prototools file beside them (but can override it), and
lockfiles for environments that aren't currently active are never loaded or modified.
The rest of the command line caught up too. proto pin and
proto unpin now always keep the lockfile of the configuration file
they modified in sync, even when another configuration takes precedence for that tool.
proto uninstall removes records from all applicable lockfiles,
since the same version may be pinned in several places. And
proto outdated --update now updates versions in environment
configurations, which it previously skipped with a warning.
Self-healing lockfiles
When you edit a .prototools file by hand — bumping node = "22" to node = "24" — the record for
the old requirement doesn't go anywhere. proto diagnose has warned
about these stale records since v0.60, but warning is all it did. You were left to clean them up
yourself.
proto install and
proto uninstall now prune these
orphaned records automatically. A record is orphaned when
its tool is still defined in the configuration file that owns the lockfile, but the record's
requirement no longer matches any of that configuration's requirements.
We were careful about the boundaries here, since deleting the wrong record is worse than leaving a
stale one behind. Records for tools the owning configuration doesn't define are ad-hoc installs, and
are never pruned. Only lockfiles that were actually loaded are touched, so inactive environments are
left alone. And pruning is skipped entirely under --immutable-lockfile, because pruning is itself a
modification.
New WASM APIs
Downloading files
Plugins have long been able to fetch a URL with the send_request host function, but the entire
response body comes back through WASM memory. That's fine for a JSON manifest and wasteful for a
300MB toolchain archive.
The new download_file host function writes a URL straight to a file on the host, streaming the
body without it ever passing through the guest.
let output = download(DownloadFileInput::new(
"https://some.com/url/to/file.tar.xz",
temp_dir.join("file.tar.xz"),
))?;
output.file; // Virtual path written to
output.size; // Size in bytes
The input also accepts custom headers, and if you don't need them, the download_from_url
function takes the URL and destination directly. Requests go through proto's own HTTP client, so
they respect your [settings.http] and .netrc configuration.
POST requests
send_request now accepts a method field, supporting GET (the default) and POST. There's a
SendRequestInput::post() constructor for the common case.
let response = send_request!(input, SendRequestInput::post("https://some.com/url/to/post"));
This unblocks plugins for tools whose version APIs are query based rather than a plain GET endpoint.
Lockfile metadata
Plugins can now attach arbitrary key-value information to the lockfile record of an install through
the new ToolLockOptions.metadata field. If your plugin resolves something during installation that
matters for reproducibility — a build variant, a distribution channel, an upstream identifier — it
can now travel with the record instead of being recomputed.
Notable fixes
Two fixes in this release are worth calling out directly, as both could silently corrupt state.
Concurrent config writes no longer erase each other. proto read .prototools under a shared
lock, released it, then acquired an exclusive lock to write — so two processes could read the same
content and the last writer would wipe out the entries added by the first. Running
proto install <tool> --pin in parallel (or pin, unpin, alias, unalias, uninstall,
plugin add, plugin remove) could silently drop tools from your configuration, only surfacing much
later as proto run failing to detect a version for a tool that had just installed successfully.
Reads, modifications, and writes now happen within a single exclusive lock.
The run fallback guard no longer leaks into child processes. An internal environment variable
used to detect fallback loops was inherited by child processes, so an npm script that spawned node
could abort with a false fallback_loop error. The guard is now scoped to the process that performed
the fallback, and proto additionally skips any directory on PATH containing a shims registry.json,
so a foreign proto store can never be selected as the global fallback.
We also fixed proto install <tool> not respecting the
detect-strategy setting. It scanned the working directory for
the tool's own version file before consulting .prototools, unlike proto run, proto status, and a
bare proto install, which all honoured the setting. Single-tool installs now detect versions
identically to everything else, including traversing the configuration file chain and honouring
PROTO_<TOOL>_VERSION.
Other changes
View the official release for a full list of changes.
- Increased the plugin cache duration from 30 days to 180 days (6 months), because of GitHub's unreliability. Re-downloading a plugin that hasn't changed is a common source of transient install failures.
- Fixed versions with a pre-release or build identifier beginning with a hyphen (like
1.0.6--canary.9.0c3f3b7.0) failing to parse, even though they're valid semver. These can appear inyarn.lockfiles transitively through published packages. - Fixed an install that failed very early (before any progress output) hanging when not running in a TTY.
- Updated Rust to v1.98.0.
