Releasing miniextendr
How to cut a tagged release of the two shippable artifacts — the miniextendr CLI (a Rust binary) and rpkg (the R package, which installs as library(miniextendr)). There is no release-automation workflow today; releases are manual tag-and-push. This document is the recipe.
How to cut a tagged release of the two shippable artifacts — the miniextendr
CLI (a Rust binary) and rpkg (the R package, which installs as
library(miniextendr)). There is no release-automation workflow today; releases
are manual tag-and-push. This document is the recipe.
Not to be confused with
RELEASE_WORKFLOW.md, which is about the CI workflow downstream consumers scaffold (minirextendr::use_release_workflow()) — not about releasing miniextendr itself.
🔗Version bumping
One script bumps every version-bearing file in lockstep:
just bump-version 0.2.0 # or: bash scripts/bump-version.sh 0.2.0
It updates [workspace.package].version in the root Cargo.toml (which the CLI
inherits via version.workspace = true) and the Version: field of every
DESCRIPTION (rpkg, minirextendr, the cross-package test fixtures). Commit the
result, then tag:
git commit -am "release: v0.2.0"
git tag -a v0.2.0 -m "v0.2.0"
git push && git push --tags🔗Releasing the CLI
The CLI is a plain binary crate ([[bin]] name = "miniextendr"). Its only
dependencies are crates.io packages — it does not depend on
miniextendr-api/-macros, so it has no vendoring or git-pin entanglement.
Once the tag is pushed, users install straight from it — no crates.io publish, no CI infrastructure required:
cargo install --git https://github.com/A2-ai/miniextendr --tag v0.2.0 miniextendr-cli
# add --features dev for the contributor-only `dev` subcommands
cargo install --path miniextendr-cli (or just cli-install) remains the
from-checkout path for local development.
crates.io publishing is not set up. The crate carries no
publish = false, so it could be published, but the git-tag install above covers distribution without it.
🔗Releasing rpkg
rpkg ships in two modes, and they behave differently. Both start from the
pushed tag above.
🔗Mode A — GitHub source install (remotes/pak/devtools)
remotes::install_github("A2-ai/miniextendr", subdir = "rpkg", ref = "v0.2.0")
# pak::pak("A2-ai/miniextendr/rpkg@v0.2.0")
remotes downloads the whole-repo zipball (no .git) — so the framework
crates (miniextendr-api, -lint, -macros) travel along as sibling
directories — and builds via pkgbuild. Because DESCRIPTION sets
Config/build/bootstrap: TRUE, pkgbuild runs rpkg/bootstrap.R first.
On the default build = TRUE path, pkgbuild runs bootstrap.R, which never
vendors. Only a path dependency outside the package (path = "../…", which
a staged/git install would strand) needs work: tools/dev-bootstrap.R stages
those crates under src/rust/vendor/ with cargo package (#1580), and registry
and git dependencies resolve over the network. A package whose path
dependencies all live inside it (the exemplar’s satellite) or that has none
(the typical scaffold) falls through to a plain source build (configure’s
[patch] for the framework siblings, or cargo fetching the git URL).
cargo-revendor is never involved.
How the framework crates resolve (read from configure.ac):
- configure detects the monorepo by a filesystem probe — it walks up ≤5
levels looking for
miniextendr-api/Cargo.toml(configure.ac:463–473), not by a.gitancestor. The full-repo zipball satisfies this, so the siblings are found and (in pure source mode) a[patch."https://github.com/A2-ai/miniextendr"]block redirects the three crates to those local, tag-state sources. - The framework version is pinned by the committed
src/rust/Cargo.lock(shipped viaConfig/build/extra-sources); its framework entries carrysource = "git+…#<sha>", and the vendor/offline branch reproduces that sha. The unpinnedgit = …inCargo.tomlonly decides resolution when the lock is regenerated — see “Known gap”.
Prerequisites: Rust toolchain (SystemRequirements: Cargo, rustc >= 1.85)
and network access. cargo-revendor is not needed.
autoconf is not needed — configure is committed.
🔗Mode B — released vendored tarball (reproducible, offline, CRAN-shaped)
This is the recommended release artifact. Vendoring is an explicit step that
only the release build runs; devtools::build() alone produces a Mode A
(source-mode) tarball.
just r-cmd-build # → miniextendr_0.2.0.tar.gz, vendored
just r-cmd-build (and just r-cmd-check) run just vendor first, which
writes inst/vendor.tar.xz with cargo-revendor and asserts that the
framework crates were vendored from the local workspace rather than git@main
(#876). The recipe removes the archive and restores the committed Cargo.lock
when it exits. A scaffolded package does the same with
minirextendr::miniextendr_build_tarball(). This needs cargo-revendor on
PATH. Before attaching a tarball, confirm it contains inst/vendor.tar.xz
(tar -tzf … | grep inst/vendor.tar.xz).
Attach the resulting miniextendr_0.2.0.tar.gz to a GitHub Release. Users install
it offline and reproducibly:
install.packages("miniextendr_0.2.0.tar.gz", repos = NULL, type = "source")
# remotes::install_url(".../releases/download/v0.2.0/miniextendr_0.2.0.tar.gz")
At install, configure detects inst/vendor.tar.xz — the single latch — and
switches to tarball mode: offline build from vendored-sources, no network, no
cargo-revendor.
Why prefer this over Mode A: the tarball is self-contained and offline —
vendored sources are sealed inside, so the user needs no network and no
cargo-revendor, and the framework code is frozen at build time (built inside
the monorepo, configure writes a [patch."git+url"] override and
cargo-revendor vendors the framework crates from the local checkout). Mode A
is a from-source build that still depends, at the user’s machine, on a Rust
toolchain, network access, and the committed lock sha.
Building release artifacts on CI (esp. macOS): the Rust toolchain ABI must match CRAN’s R, or the
.sofails to load / trips--as-cran. configure bakes a per-installMACOSX_DEPLOYMENT_TARGETfloor into.cargo/config.toml(configure.ac:520–577), but a release runner should also pin the SDK. See RELEASE_WORKFLOW.md Gotchas 5–6 — and noteminirextendr::use_release_workflow(rpkg_subdir = "rpkg")scaffolds a known-good workflow for this exact subdir layout.
🔗Known gap
The framework git dependencies in rpkg/src/rust/Cargo.toml (and the
tests/model_project / minirextendr fixtures) are declared unpinned
(git = "…", no tag/rev). Reproducibility today rests entirely on the
committed Cargo.lock sha — not the manifest. If anything regenerates the lock
during a release build (a cargo update, or a cargo old enough to rewrite a v4
lock — see configure.ac:920), the framework jumps to whatever main HEAD is at
that moment, silently. Pinning tag =/rev = in the manifest would make the
release self-describing instead of lock-dependent. Track as an issue rather than
leave implicit.