fix: comprehensive bakery package manager audit and repair

Critical fixes:
- gen-index.sh: emit services, config, optional_system_deps from bakery.toml;
  parse product list from registry TOML instead of hardcoded array; fail loudly
  when bakery.toml is missing (was silently producing empty metadata in prod)
- install.rs: download service units and example configs from dl server at
  install time (were never fetched); check systemctl exit codes (were swallowed);
  save state before file cleanup in remove_package (was inconsistent on error)
- doctor.rs: rewrite dep detection to use `pacman -Q` as primary (no more
  dependency on `which` or pkg-config name mismatches); add optional_system_deps
  support returning (missing, warnings) — warnings print but never block install
- get.sh: fix GitHub fallback URL (was 404 for both latest and versioned
  releases); add SHA-256 checksum verification using published .sha256 file

High priority fixes:
- bakery doctor <unknown-pkg>: exit non-zero (was silently passing)
- bakery update: add --all flag (documented in README but missing from CLI);
  add doctor gate before update (was bypassing dep check)
- bread_deps: now resolved recursively with cycle detection (was ignored)
- manifest.rs: add artifact_urls() helper and optional_system_deps field
- state.rs: atomic save via tmp+rename; cmd_info shows optional_system_deps

Tests: 17 new unit tests across doctor, download, install, state modules;
scripts/test-gen-index.sh fixture test for full pipeline
This commit is contained in:
Breadway 2026-06-11 13:37:09 +08:00
parent 0b38e8cce3
commit 694829c50f
13 changed files with 971 additions and 148 deletions

View file

@ -33,7 +33,12 @@ impl State {
std::fs::create_dir_all(dir)?;
}
let text = serde_json::to_string_pretty(self)?;
std::fs::write(&path, text).context("writing installed.json")
// Write to a temp file then rename for atomicity — avoids a torn write
// if the process is killed mid-save.
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, &text).context("writing installed.json.tmp")?;
std::fs::rename(&tmp, &path).context("atomically replacing installed.json")?;
Ok(())
}
pub fn is_installed(&self, name: &str) -> bool {
@ -58,3 +63,58 @@ fn state_path() -> PathBuf {
})
.join("bakery/installed.json")
}
#[cfg(test)]
mod tests {
use super::*;
fn pkg(name: &str, version: &str) -> InstalledPackage {
InstalledPackage {
name: name.to_string(),
version: version.to_string(),
binaries: vec![],
services: vec![],
installed_at: "2026-01-01T00:00:00Z".to_string(),
}
}
#[test]
fn record_and_is_installed() {
let mut state = State::default();
assert!(!state.is_installed("foo"));
state.record(pkg("foo", "1.0.0"));
assert!(state.is_installed("foo"));
}
#[test]
fn remove_installed() {
let mut state = State::default();
state.record(pkg("foo", "1.0.0"));
let removed = state.remove("foo");
assert!(removed.is_some());
assert!(!state.is_installed("foo"));
}
#[test]
fn remove_unknown_returns_none() {
let mut state = State::default();
assert!(state.remove("nope").is_none());
}
#[test]
fn json_roundtrip() {
let mut state = State::default();
state.record(InstalledPackage {
name: "bar".to_string(),
version: "2.0.0".to_string(),
binaries: vec!["bar".to_string()],
services: vec!["bar.service".to_string()],
installed_at: "2026-06-01T00:00:00Z".to_string(),
});
let json = serde_json::to_string(&state).unwrap();
let restored: State = serde_json::from_str(&json).unwrap();
assert!(restored.is_installed("bar"));
assert_eq!(restored.packages["bar"].version, "2.0.0");
assert_eq!(restored.packages["bar"].services, ["bar.service"]);
}
}