ADR-0019: Prebuilt mnem binaries via cargo-dist
- Status: Accepted
- Date: 2026-09-19
- Issue: #173
Context
cargo install mnem-git is the only way to get the mnem CLI without
building from source, and it needs a Rust toolchain. #165 solved the
equivalent problem for the Python SDK: cross-platform wheels, built and
published by a GitHub Actions workflow. This ADR does the same for the
Rust binary: a prebuilt archive and a one-line installer script per
platform, attached to the GitHub Release.
Two things about this needed a decision before writing any workflow,
because both are hard to walk back cheaply: whether to stand up a
Homebrew tap (a second, permanent GitHub repository under the same
account), and whether to change the release process this project has
used since v0.0.2 (tag by hand, then create the GitHub Release by
hand). Both were put to the maintainer directly rather than decided here.
Decision
Tool: cargo-dist (crates.io cargo-dist, binary dist, v0.32.0
pinned in dist-workspace.toml). It builds the archives, generates the
shell and PowerShell installer scripts, and knows how to attach
everything to a GitHub Release. Hand-rolling the same thing (as
release-pypi.yml does for wheels) would mean maintaining installer
scripts by hand for no reason; this is exactly cargo-dist's job.
Targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu,
x86_64-apple-darwin, aarch64-apple-darwin, x86_64-pc-windows-msvc.
The same five platforms #165 covers for the Python wheel, so the CLI and
the SDK have the same reach.
No Homebrew tap for now. The maintainer's call: ship the raw
archives and the shell/PowerShell installers only. A tap is a second
repository (Nabzx/homebrew-tap) to maintain forever after; add it once
there is real usage to justify it. cargo-dist supports adding one later
(the tap setting) without redoing anything else.
The release process stays manual, cargo-dist included. The
maintainer's call, matching how release-pypi.yml already behaves. Note
for anyone reading ADR-0007 alongside this: ADR-0007 sketches a release
workflow that also cuts the GitHub Release from the tag push; what #68
actually built keeps that step manual (the workflow triggers on
release: published, an event only the manual step produces). This ADR
follows the practice that shipped, not ADR-0007's original sketch of it.
dispatch-releases = true: the workflow triggers onworkflow_dispatch(gh workflow run release.yml -f tag=vX.Y.Z, or the Actions tab), never on a tag push by itself.create-release = false: cargo-dist does not create the GitHub Release. It expects one to already exist for the tag, as a draft, and uploads the binaries to that draft, undrafting it once everything is attached.
That second point changes the manual step slightly: today the maintainer
creates the GitHub Release and publishes it in one action. Under this ADR,
the release is created as a draft first; running the dist workflow
(after the tag is pushed) is what makes it visible, once the binaries are
actually attached. Nothing about the release goes live before there is
something to download, which is arguably the better property, not a worse
one, and the maintainer still decides both when to draft it and when to
run the workflow.
Two release workflows, not one. dist generate writes to
.github/workflows/release.yml unconditionally; that name was already
taken by #165's PyPI workflow. Renamed the PyPI one to
release-pypi.yml (and its display name to release (pypi)) rather than
fight cargo-dist's own regeneration model by hand-merging jobs into a
file it expects to own outright. release.yml (cargo-dist's) is
regenerated by editing dist-workspace.toml and running dist generate
again; it is not meant to be hand-edited.
Consequences
cargo install mnem-gitremains the primary path for anyone with a Rust toolchain; the new installers are for everyone else. Neither changes what the crate publishes to crates.io (#165's sibling ADR did not apply here, so there is no ADR change there).- A
[profile.dist]in the rootCargo.toml(added bydist init,inherits = "release",lto = "thin") is the profile the release binaries build with; it does not affectcargo build --release. dist-workspace.tomlis the source of truth;.github/workflows/release.ymlis generated output, checked in for review but not meant to be edited by hand. A futuredistversion bump or a target change means editing the config and re-runningdist generate, the same disciplinerelease-pypi.ymldoes not need because it has no equivalent generator.- PyPI trusted publishing (#165) is scoped to a workflow filename; the
rename to
release-pypi.ymlmeans a trusted publisher must be added under that name, notrelease.yml, when #68's remaining PyPI packages are configured. - No
format_versionchange, no object-model change, no Rust/Python boundary change. This is packaging and CI, not the substrate.
Alternatives considered
Hand-roll the binary builds and installer, matching how
release-pypi.yml was hand-written for Python. Rejected: an installer
script is easy to get subtly wrong (platform detection, checksum
verification, $PATH handling) and cargo-dist's is maintained by people
who do this for a living. Reinventing it buys nothing.
Let cargo-dist create the GitHub Release (create-release = true,
its default). Rejected per the maintainer: it would mean a tag push (or a
workflow_dispatch) starts producing a public release page without the
explicit "publish" action this project has used since v0.0.2.
A Homebrew tap now. Rejected for now, not permanently: cheap to add later, and a second repository to maintain is a real, ongoing cost with no users yet to justify it.
Merge cargo-dist's jobs into the existing release.yml by hand.
Rejected: dist generate regenerates that file from dist-workspace.toml
on every config change or dist version bump; hand-merged jobs would be
silently destroyed the next time it runs, or fought with forever.