//! FJ-51: the cargo package provider — cached `cargo install` plus the
//! `.crates.toml` registration that tells cargo what forjar installed.
//!
//! Split out of package.rs to keep every file under the 500-line limit.
use crate::core::shell_escape::sh_squote;
use crate::core::types::Resource;
use super::{parse_cargo_features, per_package_query, per_package_script};
/// FJ-51: Cargo binary cache — skip recompilation when cached binary exists.
///
/// Cache layout: `$FORJAR_CACHE_DIR/<pkg>-<version>-<arch>/bin/`
/// Default cache dir: `~/.forjar/cache/cargo`
/// Disable: `FORJAR_NO_CARGO_CACHE=1`
///
/// Supports `crate[feat1,feat2]` syntax in package names to pass `--features`
/// to `cargo install`. Example: `packages: ["whisper-apr[cli]"]`.
/// True if a cargo crate name / feature uses only the cargo-legal charset
/// (`[A-Za-z0-9._-]`). Used to reject names that would otherwise be
/// interpolated into the double-quoted cache key, where `$(...)`/backticks
/// would otherwise be live.
fn is_safe_cargo_token(tok: &str) -> bool {
!tok.is_empty()
&& tok
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
}
/// True if `version` is a safe cargo version requirement charset.
fn is_safe_cargo_version(ver: &str) -> bool {
!ver.is_empty()
&& ver.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | '+' | '*' | '~' | '^')
})
}
/// Validate every cargo package spec (crate name + features) and the optional
/// version against the cargo-legal charset. Returns the offending token on
/// the first failure.
fn first_unsafe_cargo_token<'a>(
packages: &'a [String],
version: Option<&'a str>,
) -> Option<&'a str> {
if let Some(v) = version {
if !is_safe_cargo_version(v) {
return Some(v);
}
}
for p in packages {
let (crate_name, features) = parse_cargo_features(p);
if !is_safe_cargo_token(crate_name) {
return Some(crate_name);
}
if let Some(bad) = features.into_iter().find(|f| !is_safe_cargo_token(f)) {
return Some(bad);
}
}
None
}
/// The PATH repair that EVERY cargo-provider script emits before it looks for
/// `cargo` or for a crate's binary.
///
/// # Why this exists (forjar#489)
///
/// The install action bootstraps rustup with `--no-modify-path` and then
/// repairs its own `PATH`. The CHECK and the DRIFT observable did not, so on a
/// host where cargo is absent from the NON-INTERACTIVE PATH, forjar installed a
/// crate successfully and then reported it `missing:` forever.
///
/// forjar CREATES that host itself. `rustup-init -y --no-modify-path` appends
/// `. "$HOME/.cargo/env"` to the BOTTOM of `~/.bashrc` (line 118 on the box
/// this was measured on), and Ubuntu's stock `~/.bashrc` returns at line 8 when
/// the shell is not interactive — which is what `ssh host 'cmd'` gives you. So
/// the sourcing line never runs, and the PATH forjar's own check inherits has
/// no cargo in it.
///
/// Measured on `yoga` against 1.25.2, freshly reimaged: `~/.cargo/bin/rg`
/// present and executable, `rg --version` -> `ripgrep 15.1.0`,
/// `~/.cargo/.crates.toml` valid and listing it, `cargo install --list` listing
/// it — and `forjar check` printing `missing:ripgrep`. Same for `bat` and
/// `fd-find`. The second symptom is the same root cause seen from the apply
/// side: the install action's `command -v cargo ||` guard kept missing, so
/// every apply re-ran the rustup installer over a toolchain already there.
///
/// # Why one helper rather than three copies
///
/// The three sites disagreeing is the defect. A shared emitter means a future
/// edit cannot fix the check and forget the observable.
///
/// # Shape
///
/// - Honours `CARGO_HOME` exactly as `_CARGO_BIN`/`_CRATES_TOML` below do; the
/// fleet runs a shared `CARGO_HOME` on several boxes.
/// - IDEMPOTENT: the `case` guard means re-running it (or an apply that also
/// repairs PATH inside its rustup block) cannot grow `$PATH` without bound.
/// - Safe under `set -u`: `${PATH:-}` rather than `$PATH`, since the check
/// scripts run with `set -euo pipefail` in force.
/// - It modifies THIS script's environment only. It writes no shell rc file
/// and does not pass `--modify-path` to rustup: forjar does not own the
/// operator's login shell.
pub(crate) fn path_prelude() -> &'static str {
"# forjar#489: cargo may be installed and absent from the NON-INTERACTIVE\n\
# PATH -- rustup is bootstrapped with --no-modify-path and Ubuntu's\n\
# ~/.bashrc returns before the line rustup appends. Repair PATH here, in\n\
# this script's own environment, or the check reports `missing:` for a\n\
# crate it just installed. Idempotent, and honours CARGO_HOME.\n\
case \":${PATH:-}:\" in\n\
*\":${CARGO_HOME:-$HOME/.cargo}/bin:\"*) ;;\n\
*) export PATH=\"${CARGO_HOME:-$HOME/.cargo}/bin:${PATH:-}\" ;;\n\
esac"
}
pub(crate) fn apply_cargo_present(resource: &Resource) -> String {
let packages = &resource.packages;
let version = resource.version.as_deref();
let source = resource.source.as_deref();
// FJ-154: reject crate/feature/version tokens that aren't cargo-legal,
// since they flow into a double-quoted cache key where command
// substitution would otherwise be live. Path installs (source set) skip
// the cache, so they only need the install arg escaped (done below).
if source.is_none() {
if let Some(bad) = first_unsafe_cargo_token(packages, version) {
return format!(
"echo {} >&2; exit 1",
sh_squote(&format!("ERROR: unsafe cargo package/version token: {bad}"))
);
}
}
let installs: Vec<String> = packages
.iter()
.map(|p| match (source, version) {
// Local path installs — no caching, always rebuild
(Some(s), _) => {
let (_, features) = parse_cargo_features(p);
let features_arg = if features.is_empty() {
String::new()
} else {
format!(" --features {}", sh_squote(&features.join(",")))
};
format!(
"cargo install --force --locked --path {}{features_arg}",
sh_squote(s)
)
}
(None, ver) => cargo_cached_install(p, ver),
})
.collect();
// Limit build parallelism to avoid OOM on high-core-count machines.
// Respects CARGO_BUILD_JOBS if already set; defaults to min(nproc/2, 8).
format!(
"set -euo pipefail\n\
{path_prelude}\n\
command -v cargo >/dev/null 2>&1 || {{\n\
RUSTUP_INIT=$(mktemp /tmp/rustup-init.XXXXXX)\n\
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o \"$RUSTUP_INIT\"\n\
chmod +x \"$RUSTUP_INIT\"\n\
\"$RUSTUP_INIT\" -y --no-modify-path\n\
rm -f \"$RUSTUP_INIT\"\n\
export PATH=\"${{CARGO_HOME:-$HOME/.cargo}}/bin:$PATH\"\n\
}}\n\
if [ -z \"${{CARGO_BUILD_JOBS:-}}\" ]; then\n\
_nproc=$(nproc 2>/dev/null || echo 4)\n\
_half=$(( _nproc / 2 ))\n\
[ \"$_half\" -lt 1 ] && _half=1\n\
[ \"$_half\" -gt 8 ] && _half=8\n\
export CARGO_BUILD_JOBS=$_half\n\
fi\n\
_CARGO_BIN=\"${{CARGO_HOME:-$HOME/.cargo}}/bin\"\n\
_CRATES_TOML=\"${{CARGO_HOME:-$HOME/.cargo}}/.crates.toml\"\n\
{install_fns}\n\
# TELL CARGO WHAT WE INSTALLED (forjar#320).\n\
#\n\
# `cargo install --root $_STAGING` writes its registry entry to\n\
# $_STAGING/.crates.toml. We copy only bin/* out and then delete the\n\
# staging dir, so $CARGO_HOME/.crates.toml never learns about the\n\
# binaries we just put in $CARGO_HOME/bin. `cargo install --list` then\n\
# reports the crate MISSING forever, and package_check.rs reads exactly\n\
# that -- so forjar failed its own check for work it had done.\n\
#\n\
# Measured on gx10: rg/fd/bat/hyperfine installed and working, cargo\n\
# naming none of them; the same registry claiming forjar 1.16.0 on a\n\
# box running 1.18.0. Wrong in BOTH directions.\n\
#\n\
# APPEND-ONLY AND KEYED. `.crates.toml` is `[v1]` followed by one line\n\
# per install, keyed `\"name ver (source)\" = [\"bin\", ...]`. We drop any\n\
# existing line for this crate name and append the new one, so a\n\
# reinstall updates rather than duplicating.\n\
#\n\
# Deliberately NOT a TOML parser: this is generated POSIX shell running\n\
# on hosts that may lack python, and a half-written .crates.toml breaks\n\
# `cargo install` for every crate on the machine. Write to a temp file\n\
# and `mv` -- atomic within a filesystem -- so an interrupted run leaves\n\
# the original intact.\n\
#\n\
# ASK CARGO WHETHER THE BYTES ARE READABLE, BEFORE COMMITTING THEM.\n\
#\n\
# forjar#345: the merge below is entry-aware now, but \"we wrote it\" is\n\
# not \"cargo can read it\". The destination on a real host may ALREADY\n\
# be wreckage left by an older forjar, and a correct merge INTO invalid\n\
# TOML is still invalid TOML. cargo rejects the WHOLE file for one bad\n\
# entry, so committing a bad merge costs every crate on the machine:\n\
# `cargo install --list` names none of them while every binary still\n\
# runs, and package_check reports `missing:<crate>` forever.\n\
#\n\
# Ask CARGO, not a TOML library. cargo is the only consumer that\n\
# matters and it is the parser that rejected the file on intel. The\n\
# throwaway CARGO_HOME means the probe cannot touch the real one.\n\
#\n\
# Fail-OPEN on absent cargo, failed mktemp or failed cp: a broken /tmp\n\
# must not wedge every install on the box. Costs 0.015s per crate.\n\
_fj_crates_ok() {{\n\
command -v cargo >/dev/null 2>&1 || return 0\n\
_vh=$(mktemp -d /tmp/forjar-crates.XXXXXX) || return 0\n\
if ! cp \"$1\" \"$_vh/.crates.toml\" 2>/dev/null; then\n\
if [ -n \"$_vh\" ]; then rm -rf \"$_vh\"; fi\n\
return 0\n\
fi\n\
if CARGO_HOME=\"$_vh\" cargo install --list >/dev/null 2>\"$_vh/err\"; then\n\
if [ -n \"$_vh\" ]; then rm -rf \"$_vh\"; fi\n\
return 0\n\
fi\n\
sed 's/^/forjar: cargo: /' \"$_vh/err\" >&2\n\
if [ -n \"$_vh\" ]; then rm -rf \"$_vh\"; fi\n\
return 1\n\
}}\n\
_fj_register() {{\n\
_src=\"$1\"\n\
[ -f \"$_src\" ] || return 0\n\
_key=$(grep -v '^\\[v1\\]' \"$_src\" | grep -v '^[[:space:]]*$' | head -1 | sed 's/^\"\\([^ ]*\\) .*/\\1/')\n\
[ -n \"$_key\" ] || return 0\n\
_tmp=$(mktemp \"${{_CRATES_TOML}}.forjar.XXXXXX\") || return 0\n\
echo '[v1]' > \"$_tmp\"\n\
if [ -f \"$_CRATES_TOML\" ]; then\n\
awk -v k=\"$_key\" 'index($0, \"\\\"\" k \" \") == 1 {{ if ($0 ~ /=[[:space:]]*\\[$/) skip=1; next }} skip {{ if ($0 ~ /^[[:space:]]*\\]/) skip=0; next }} /^\\[v1\\]$/ {{ next }} {{ print }}' \"$_CRATES_TOML\" >> \"$_tmp\" || true\n\
fi\n\
awk -v k=\"$_key\" 'index($0, \"\\\"\" k \" \") == 1 {{ print; if ($0 ~ /=[[:space:]]*\\[$/) inarr=1; next }} inarr {{ print; if ($0 ~ /^[[:space:]]*\\]/) inarr=0 }}' \"$_src\" >> \"$_tmp\"\n\
# READ IT BACK BEFORE COMMITTING IT (forjar#345). `mv` cannot fail\n\
# on content, so an unconditional commit here reported CONVERGED for\n\
# a registry cargo could no longer parse. Refuse instead, loudly, and\n\
# leave the destination byte-identical -- `return 1` under\n\
# `set -euo pipefail` fails the resource rather than lying about it.\n\
if ! _fj_crates_ok \"$_tmp\"; then\n\
rm -f \"$_tmp\"\n\
if [ -f \"$_CRATES_TOML\" ] && ! _fj_crates_ok \"$_CRATES_TOML\" 2>/dev/null; then\n\
echo \"ERROR: $_CRATES_TOML is ALREADY unparseable; refusing to merge $_key into it\" >&2\n\
echo \"HINT: rebuild it from .crates2.json, or move it aside and re-run\" >&2\n\
else\n\
echo \"ERROR: merging $_key would make $_CRATES_TOML unparseable; nothing written\" >&2\n\
fi\n\
return 1\n\
fi\n\
mv -f \"$_tmp\" \"$_CRATES_TOML\"\n\
}}\n\
{}",
installs.join("\n"),
path_prelude = path_prelude(),
install_fns = crate::core::shell_install::atomic_install_dir_fn()
)
}
/// Generate a cached cargo install script for a single crate.
///
/// On cache hit: copy pre-built binaries from cache, skip compilation entirely.
/// On cache miss: `cargo install --root <staging>`, then populate cache + install.
///
/// Supports `crate[feat1,feat2]` syntax — features are passed via `--features`
/// and included in the cache key to avoid feature-set collisions.
///
/// Detects empty staging bin dir (no binaries produced) and emits a clear error
/// with a hint about `--features`, instead of failing on `cp` with a cryptic message.
///
/// Places the binaries with `_fj_install_bins` — stage a sibling, `rename(2)`.
/// Not `cp`, and no longer `install(1)`.
///
/// `cp` REFUSES to overwrite a dangling symlink — "cp: not writing through
/// dangling symlink" — and that is precisely the wreckage this resource has to
/// repair: a CI cache-prune step deletes the real files in a shared
/// `~/.cargo/bin` and leaves the symlinks behind, pointing at nothing.
/// Measured on paiml/infra's intel 2026-08-19: with `pzsh` reduced to a
/// dangling symlink, `forjar apply --refresh` correctly DETECTED the divergence
/// and then died on `cp`, so it could see the damage and not fix it. `cp -f`
/// does not help — coreutils refuses that too (verified on the host).
///
/// `install(1)` cleared that, and ETXTBSY with it, which is why this line read
/// `install -m 755` until now. What it did NOT clear is the gap: GNU `install`
/// unlinks the destination and then creates it, so the path is briefly ABSENT.
/// On the host this matters for — sixteen CI runners sharing one
/// `$CARGO_HOME/bin` — an `exec` landing in that window fails ENOENT. Measured
/// on lambda-labs, statting the destination while it was replaced 4000 times:
/// `install(1)` 10611 absent of 396132; temp + `mv` 0 of 741725.
///
/// `rename(2)` has no such window and is not GNU-only, so it also works on the
/// fleet's macOS box. See `core::shell_install` for the full reasoning.
fn cargo_cached_install(pkg: &str, version: Option<&str>) -> String {
let (crate_name, features) = parse_cargo_features(pkg);
let ver_tag = version.unwrap_or("latest");
let install_arg = match version {
Some(v) => sh_squote(&format!("{crate_name}@{v}")),
None => sh_squote(crate_name),
};
let features_arg = if features.is_empty() {
String::new()
} else {
format!(" --features {}", sh_squote(&features.join(",")))
};
let cache_suffix = if features.is_empty() {
String::new()
} else {
format!("+{}", features.join(","))
};
format!(
"_CACHE_KEY=\"{crate_name}-{ver_tag}{cache_suffix}-$(uname -m)\"\n\
_CACHE_DIR=\"${{FORJAR_CACHE_DIR:-$HOME/.forjar/cache/cargo}}/$_CACHE_KEY\"\n\
if [ -z \"${{FORJAR_NO_CARGO_CACHE:-}}\" ] && \
[ -d \"$_CACHE_DIR/bin\" ] && \
ls \"$_CACHE_DIR/bin/\"* >/dev/null 2>&1 && \\\n\
[ -f \"$_CACHE_DIR/.crates.toml\" ]; then\n\
_fj_install_bins \"$_CACHE_DIR/bin\" \"$_CARGO_BIN\"\n\
_fj_register \"$_CACHE_DIR/.crates.toml\"\n\
echo \"forjar: cache-hit {crate_name} [$_CACHE_KEY]\"\n\
else\n\
_STAGING=$(mktemp -d /tmp/forjar-cargo.XXXXXX)\n\
cargo install --force --locked --root \"$_STAGING\"{features_arg} {install_arg}\n\
if [ ! -d \"$_STAGING/bin\" ] || ! ls \"$_STAGING/bin/\"* >/dev/null 2>&1; then\n\
echo \"ERROR: cargo install {crate_name} produced no binaries\" >&2\n\
echo \"HINT: does the crate need --features? Use packages: [\\\"{crate_name}[feature_name]\\\"]\" >&2\n\
rm -rf \"$_STAGING\"\n\
exit 1\n\
fi\n\
if [ -z \"${{FORJAR_NO_CARGO_CACHE:-}}\" ]; then\n\
mkdir -p \"$_CACHE_DIR\"\n\
cp -a \"$_STAGING/bin\" \"$_CACHE_DIR/\"\n\
cp -f \"$_STAGING/.crates.toml\" \"$_CACHE_DIR/.crates.toml\" 2>/dev/null || true\n\
fi\n\
_fj_install_bins \"$_STAGING/bin\" \"$_CARGO_BIN\"\n\
_fj_register \"$_STAGING/.crates.toml\"\n\
rm -rf \"$_STAGING\"\n\
echo \"forjar: cached {crate_name} [$_CACHE_KEY]\"\n\
fi"
)
}
/// Remove cargo-installed crates.
///
/// forjar#278: this arm did not exist, so `(cargo, absent)` fell to the
/// catch-all, echoed, and reported converged — a declared removal that never
/// removed anything.
///
/// `|| true` matches the apt/uv/brew absent arms: uninstalling a crate that is
/// not installed is the desired end state, not a failure. The check script is
/// what decides convergence, and it asks whether the crate is gone.
pub(crate) fn apply_cargo_absent(resource: &Resource) -> String {
per_package_script(&resource.packages, |p| {
let (crate_name, _) = parse_cargo_features(p);
format!(
"cargo uninstall {} 2>/dev/null || true",
sh_squote(crate_name)
)
})
}
/// Query cargo's own registry for what is installed (for state hashing).
///
/// Feeds DRIFT, so its blindness is the expensive half — see the body.
pub(crate) fn state_query(packages: &[String]) -> String {
// GH-257: ask cargo, not the PATH — see package_check.rs for the
// full reasoning. This one feeds DRIFT, so its blindness is the
// more expensive half: with `command -v <crate_name>`, a crate
// whose binary is named differently (kani-verifier -> cargo-kani)
// reads as MISSING forever, and a dangling symlink reads as
// installed. Neither state produces a useful drift signal, which is
// why an intel host lost rustup, cargo and forjar without a single
// drift finding.
// ...AND CHECK THE BINARIES, NOT ONLY THE REGISTRATION.
//
// `cargo install --list` reads $CARGO_HOME/.crates.toml — METADATA.
// It does not stat anything. So when $CARGO_HOME/bin is pruned (which
// on this fleet is routine: Swatinem/rust-cache's POST step does it,
// and 16 runners share one $HOME) every binary dies, .crates.toml
// survives, and this observable keeps reporting `installed`.
//
// Measured 2026-08-24 on intel: `cargo-kani` and `kani` both absent
// from PATH, `~/.kani` intact, and `forjar drift` across the whole
// machine reported "No drift detected" eight times. The comment this
// replaces already named the symptom — "why an intel host lost
// rustup, cargo and forjar without a single drift finding" — and
// then picked an observable that cannot see it either.
//
// Registration alone is not installation. `command -v <crate>` alone
// is worse (kani-verifier installs `cargo-kani`, not `kani-verifier`,
// and a dangling symlink reads as present). So do BOTH: take the
// binary names cargo itself lists under the crate, and require each
// to exist and be executable.
//
// `cargo install --list` prints:
// kani-verifier v0.67.0:
// cargo-kani
// kani
// Top-level lines are unindented; binaries are indented beneath.
// Order is stable, so the digest is stable. (paiml/infra#208.)
let queries = per_package_query(packages, |p| {
let (crate_name, _) = parse_cargo_features(p);
let awk = format!(
"awk -v c={} '/^[^[:space:]]/{{inblk=($1==c)}} inblk&&/^[[:space:]]/{{print $1}}'",
sh_squote(crate_name)
);
format!(
"if cargo install --list 2>/dev/null | grep -q {reg}; then\n bins=$(cargo install --list 2>/dev/null | {awk})\n if [ -z \"$bins\" ]; then echo {noBins}\n else\n st=''\n for b in $bins; do\n if command -v \"$b\" >/dev/null 2>&1; then st=\"$st$b:ok,\"\n else st=\"$st$b:GONE,\"; fi\n done\n echo {crate}=installed:\"$st\"\n fi\n else echo {missing}; fi",
reg = sh_squote(&format!("^{crate_name} v")),
awk = awk,
noBins = sh_squote(&format!("{crate_name}=installed:NO-BINARIES-LISTED")),
crate = crate_name,
missing = sh_squote(&format!("{crate_name}=MISSING")),
)
});
// The PATH repair goes FIRST: both `cargo install --list` and the
// `command -v "$b"` below resolve through PATH, and this observable feeds
// DRIFT, so its blindness costs a re-apply on every run (forjar#489).
format!("{}\n{}", path_prelude(), queries)
}