-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
For curious users and would-be contributors. The full module breakdown is in CLAUDE.md; this page covers the shape of the data flow and the trade-offs behind the design choices.
┌───────────────────────────┐
│ cargo install --list │
└─────────────┬─────────────┘
│ parsed once, cached
▼
┌───────────────────────────┐
│ PackageInfo[] │
│ name, current_version, │
│ PackageSource │
└─────────────┬─────────────┘
│ filter / exclude (globset)
▼
┌───────────────────────────┐
│ filtered PackageInfo[] │
└─────────────┬─────────────┘
│ for each: tokio::spawn
│ (Semaphore(16))
▼
┌──────────────────────┴──────────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ sparse index GET │ │ cargo search │
│ index.crates.io/... │ ── fallback ──→ │ (subprocess) │
│ 1 retry on 5xx/net │ │ slower path │
└──────────┬──────────┘ └──────────┬──────────┘
│ │
└──────────────────┬──────────────────────────┘
▼
┌───────────────────────────┐
│ choose_latest(stable, │
│ prerelease, current, │
│ include_prerelease) │
└─────────────┬─────────────┘
│ select_updates
▼
┌───────────────────────────┐
│ dialoguer multi-select │
│ (or --batch / --no-int) │
└─────────────┬─────────────┘
│ for each selected (up to -j N
│ concurrent, default 4):
▼
┌───────────────────────────────────┐
│ updater::update_package │
│ - in-process downloader path: │
│ · GitHub Releases API tag │
│ lookup (HEAD-probe fallback)│
│ · stream + sha256 + extract │
│ · atomic rename to ~/.cargo │
│ - cargo install fallback for │
│ non-GitHub / git / path │
│ - PbGuard + SlowGuard │
│ - retry 3× backoff 2s │
│ - verify via cache │
│ (invalidate single key) │
└─────────────┬─────────────────────┘
▼
┌───────────────────────────┐
│ summary + exit code │
│ (or JsonReport on stdout) │
└───────────────────────────┘
The dominant cost in "check 20 packages for updates" is how many cargo subprocesses you spawn. cargo-update and earlier versions of cargo-fresh ran cargo search <name> per package — that's 20 cargo startups, each loading the registry index from scratch.
cargo-fresh's primary path is one shared reqwest::Client making 20 concurrent HTTPS requests to https://index.crates.io/{shard}/{name}. The TLS connection pool reuses the same TCP connection across all of them. Total wallclock: ~3s for 20 packages, of which most is TLS handshake + DNS.
cargo search is kept as fallback for environments where sparse index is blocked (corporate firewalls, broken mirrors). The fallback can be disabled with --no-cargo-search-fallback for diagnostic clarity.
Why 16-way concurrent? Semaphore(16) keeps fd usage bounded and avoids overwhelming crates.io's rate limit when someone has 100+ packages installed. Empirically, going higher doesn't help — TLS handshake amortizes well at 16, and individual requests are I/O-bound on the server side.
Each successful update needs to re-check the installed version (to confirm the install actually advanced the version). Without caching, that's an extra cargo install --list per package — N+1 in the worst case.
cargo-fresh populates the version map once in get_installed_packages, then invalidate_installed_version(pkg) removes a single entry after a successful install. The next get_installed_version call for that package re-runs cargo install --list and refreshes the cache — but only for that one query.
The lock type is OnceLock<Mutex<HashMap>> rather than OnceCell because:
- Initialization happens in async code;
OnceLockisSync - The map needs interior mutability for
invalidate_* -
OnceLock::get_or_initruns the initializer at most once and is non-blocking after
Initialization was previously OnceLock::set, which silently returns Err if called twice — a foot-gun if get_installed_packages ever ran more than once in a process (which a future --watch mode will need). 0.10.1 changed it to get_or_init plus lock/clear/extend so re-scans correctly refresh.
cargo's terminal aesthetic depends on clean spinner cleanup. Earlier versions had ghost spinner frames left on screen after errors — every return path needs finish_and_clear(), and missing one is invisible until someone notices ghosts in their scrollback.
PbGuard wraps ProgressBar in an RAII struct; its Drop calls finish_and_clear(). So no matter how update_package exits (success, failure, retry exhausted, dry-run, panic), the spinner gets cleaned up.
SlowGuard does the opposite: it cancels a background "tell user this is taking >30s" watchdog when the update completes. Without SlowGuard, late-fired warnings would print after the main flow ended, confusing users.
--format=json requires that no human-facing output goes to stdout/stderr — scripts piping stdout into jq would choke on color codes or spinner characters. Rather than threading a json_mode boolean through every function that might print, cargo-fresh sets a global atomic flag at startup. Every status* / pb_status* / print_* function short-circuits when it's true. The final JsonReport is the single output line.
This is the kind of global that's normally avoided. It's defensible here because:
- The flag is set exactly once at startup, never mutated again
- The alternative (a
displaycontext parameter on dozens of functions) added more cognitive load than the global removed - The contract is simple: "if JSON_MODE, shut up"
PackageSource is enum { Crates, Git { url, rev }, Path { dir }, Unknown(String) }. updater::update_package dispatches on it to pick the install strategy:
-
Crates→ try the in-process downloader (GitHub Releases API resolves the tag's asset list, picks the binary matching the current target triple, streams + verifies + atomically renames into~/.cargo/bin). On anyUnsupportedoutcome (non-GitHub repo, no matching asset, unsupported archive format) fall back tocargo install --force <name> [--version X] -
Git { url, rev }→cargo install --git URL [--rev REV] <name>(no downloader path — there's no notion of "release asset" for an arbitrary git ref) -
Path { dir }→cargo install --path DIR <name> -
Unknown(raw)→ skip with aSkip [unknown source]line
The Unknown variant exists because earlier code silently treated unknown prefixes as Crates, sending them to the sparse index — which would 404 and the user would see "version check failed" without understanding why. Now the skip is explicit and visible.
The 0.11.0 downloader resolved release assets by HEAD-probing a cross-product of candidate URLs (N names × 6 tag paths × 10 filename templates × 2 archive formats × M target aliases — up to 360 requests per "no prebuilt" verdict). 5s timeout + 16-way concurrency capped the wall-clock at ~5s worst case, but for users running 50+ packages this generated a lot of avoidable traffic against github.com.
0.12.0 switched to GET /repos/{owner}/{repo}/releases/tags/{tag}. One API call returns the full asset list; cargo-fresh applies the same filename-matching logic locally and streams the winning URL directly. Wall-clock for "no prebuilt": one API call (~200ms) instead of 16–360 HEADs.
The API quota is the cost: 60/hr anonymous, 5000/hr with a token (GITHUB_TOKEN / GH_TOKEN / gh auth token, in that lookup order). When the API call fails for any reason — 401 / 403 / 429 / network — the code automatically falls back to the older HEAD-probe path, so the worst case for an unauthenticated user under heavy load is "behaves like 0.11.0", not "fails outright".
0.12.0 made the update loop concurrent via tokio::JoinSet. -j N caps the parallel task count (default 4, 0 means unlimited, 1 restores 0.11.x serial behavior). Default 4 balances throughput against GitHub-CDN friendliness — downloads are network-bound and the inner HEAD-probe pool already caps connection fan-out, so going higher rarely helps.
Two correctness details:
-
MultiProgressrows are pre-registered in input order. Concurrent updates finish in arbitrary order, butUpdatePlan::new(names)lays out the rows up front, so the visual layout stays stable. -
.crates.toml/.crates2.jsonwrites serialize through an in-processMutex<()>(install::CRATES_FILES_LOCK). Two concurrent updates writing different binary entries to the same cargo metadata files would race; the lock makes the read-modify-write atomic.
cargo install fallbacks naturally serialize on cargo's own $CARGO_HOME lock regardless of -j, so there's no extra coordination needed for that path.
cargo-fresh ships a binary, not a library. MSRV exists so users on long-lived toolchains can cargo install cargo-fresh without hitting "this requires a newer rustc". Each MSRV bump is friction for users — we keep it as low as reasonable.
Current floor: 1.86. Constraints:
-
clap_derive 4.6.1requiresedition2024(stable since 1.85) -
icu_*@2.2.0(transitive viareqwest → url → idna → icu) declaresrust-version = 1.86
The MSRV CI job runs cargo check --locked --lib --bins — not --all-targets, because dev-dependencies (wiremock, assert_cmd, etc.) have their own MSRVs unrelated to what we promise users.
0 no updates / all selected succeeded
1 updates available but not applied
2 at least one update failed
130 SIGINT
The "updates available but not applied" path matters for CI gating: a scheduled job runs cargo fresh --no-interactive and exits 1 → the job fails → maintainers get paged → updates get reviewed and applied manually. This is more useful than the boolean "did anything fail" you'd get from a 0/1-only contract.
130 is the POSIX convention for SIGINT (128 + 2). cargo-fresh handles it through Arc<AtomicBool> + tokio::signal::ctrl_c. The cancel is cooperative: a first Ctrl-C flips the flag and prints Aborting Ctrl-C again to force exit — in-flight downloads check the flag between byte-stream chunks and stop quickly; in-flight cargo install subprocesses run to natural completion. A second Ctrl-C calls exit(130) immediately. TempDir Drop + atomic rename guarantee no half-installed state is left behind in either case.
src/locale/texts.rs is two giant match statements (one per language) mapping string keys to translated strings. No .po files, no gettext runtime, no separate translation pipeline.
The trade-off: this scales poorly past ~3 languages. For a CLI with English + Chinese, the simplicity wins. If a third language joins, switching to fluent or gettext would be the right call — that's tracked informally (no ROADMAP entry yet).
detect_language() reads LANG / LC_ALL / LC_CTYPE; tests call detect_from_locale(&str) (pure function) to avoid env::set_var races.
src/lib.rs exists, but only so tests/ can call internals (e.g., cargo_fresh::package::sparse_index::fetch_latest). It's not a downstream API — README's Stability Guarantees explicitly carve it out.
The reasoning: if cargo-fresh's modules became a public API, internal refactors would need semver coordination with downstream users. For a 1-maintainer project, that's a tax not worth paying when nobody has asked for the API. If demand emerges, we'd reshape the lib surface deliberately rather than freezing whatever happens to be in 1.0.
-
src/main.rsfor orchestration and how exit codes flow -
src/package/sparse_index.rsfor the HTTPS path -
src/updater/mod.rsfor the install + retry loop with guards -
src/errors.rsfor the actionable-hint matcher -
tests/cli.rsandtests/sparse_index_http.rsfor the integration test patterns