diff --git a/Cargo.lock b/Cargo.lock index 36707a1..78dd279 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,7 +81,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bakery" -version = "0.2.3" +version = "0.1.0" dependencies = [ "anyhow", "chrono", @@ -91,7 +91,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "tempfile", "toml 0.8.23", "ureq", ] @@ -119,7 +118,7 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.2.3" +version = "0.1.0" dependencies = [ "dirs", "gtk4", @@ -323,22 +322,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - [[package]] name = "field-offset" version = "0.3.6" @@ -365,12 +348,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -520,19 +497,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - [[package]] name = "gio" version = "0.22.6" @@ -723,15 +687,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - [[package]] name = "hashbrown" version = "0.17.1" @@ -856,12 +811,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "idna" version = "1.1.0" @@ -890,9 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", + "hashbrown", ] [[package]] @@ -919,12 +866,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -940,12 +881,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.2" @@ -1061,16 +996,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1098,19 +1023,13 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.17", + "getrandom", "libredox", "thiserror", ] @@ -1123,7 +1042,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom", "libc", "untrusted", "windows-sys 0.52.0", @@ -1138,19 +1057,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" version = "0.23.40" @@ -1353,19 +1259,6 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1500,12 +1393,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -1572,24 +1459,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -1635,40 +1504,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "webpki-roots" version = "0.26.11" @@ -1912,100 +1747,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 354c58c..9511358 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["bakery", "bread-theme"] resolver = "2" [workspace.package] -version = "0.2.3" +version = "0.1.0" edition = "2021" license = "MIT" authors = ["Breadway "] diff --git a/README.md b/README.md index 405c193..e44a46a 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ bakery install breadbar | Package | Description | |---------|-------------| -| `bread` | Reactive automation daemon (`breadd`) + CLI — Lua scripting over Hyprland, udev, power, network, and Bluetooth events | -| `breadbar` | GTK4 status bar (workspaces, clock, CPU/RAM/battery/WiFi/Bluetooth) and D-Bus notification daemon for Hyprland | -| `breadbox` | GTK4 fuzzy app launcher for Hyprland with context-aware sorting; ships an icon-sync daemon (`breadbox-sync`) | -| `breadcrumbs` | Profile-aware Wi-Fi state machine with Tailscale exit-node management and a self-healing watch daemon | -| `breadpad` | Quick-capture scratchpad popup with AI-powered note classification, reminders, recurrence, and a full note viewer (`breadman`) | +| `bread` | Reactive automation daemon (`breadd`) + CLI | +| `breadbar` | Status bar and notification daemon | +| `breadbox` | Cloud sync daemon (`breadbox-sync`) + file browser | +| `breadcrumbs` | Network information CLI | +| `breadpad` | Scratchpad / quick-note app | ## Installing bakery @@ -49,18 +49,13 @@ bakery remove # remove a package (data files are never deleted) ## System dependencies by product -`bakery doctor` checks these automatically before any install. Required deps block installation; optional deps generate a warning but never block. - -| Package | Required | Optional | -|---------|----------|---------| -| `bakery` | _(statically linked, none)_ | — | -| `bread` | `systemd-libs` `openssl` `zlib` | `bluez` `hyprland` | -| `breadbar` | `gtk4` `gtk4-layer-shell` `iw` `libpulse` | `hyprland` | -| `breadbox` | `gtk4` `gtk4-layer-shell` `librsvg` | `hyprland` | -| `breadcrumbs` | `networkmanager` | `tailscale` `sudo` `xdg-utils` | -| `breadpad` | `gtk4` `gtk4-layer-shell` | `rocm-hip-runtime` `ollama` `hyprland` | - -Install all required deps with `sudo pacman -S `. Use `pacman -Q ` to check whether any are already present. +| Package | Arch packages | +|---------|--------------| +| `bread` | `libudev` `dbus` | +| `breadbar` | `gtk4` `gtk4-layer-shell` `dbus` `iw` | +| `breadbox` | `gtk4` `librsvg` `dbus` | +| `breadcrumbs` | `networkmanager` | +| `breadpad` | `gtk4` `dbus` | ## Theming @@ -95,22 +90,6 @@ and mirrors the binary to GitHub Releases as a fallback. `bakery` always tries `dl.breadway.dev` first and transparently falls back to the GitHub Release URL recorded in the manifest. -### Release artifact contract - -Each product's `release.yml` **must** upload the following files alongside -the binary to `dl.breadway.dev///`: - -| File | Purpose | -|------|---------| -| `bakery.toml` | Metadata (deps, services, config) read by `gen-index.sh` | -| `-x86_64.sha256` | Checksum verified by `bakery install` and `get.sh` | -| `*.service` | systemd unit files installed by `bakery install` | -| `*.example.toml` / `config.example.toml` | Example configs copied on first install | - -`gen-index.sh` **fails loudly** if `bakery.toml` is missing — this is by -design to catch omissions in the release workflow before they silently -produce empty metadata in production. - ## License MIT diff --git a/bakery.toml b/bakery.toml index c88ee13..057558d 100644 --- a/bakery.toml +++ b/bakery.toml @@ -2,7 +2,6 @@ name = "bakery" description = "Bread ecosystem package manager" binaries = ["bakery"] system_deps = [] -optional_system_deps = [] bread_deps = [] [install] diff --git a/bakery/Cargo.toml b/bakery/Cargo.toml index df5b7c1..b0724b3 100644 --- a/bakery/Cargo.toml +++ b/bakery/Cargo.toml @@ -18,6 +18,3 @@ sha2 = { workspace = true } hex = { workspace = true } clap = { workspace = true } chrono = { workspace = true } - -[dev-dependencies] -tempfile = "3" diff --git a/bakery/src/doctor.rs b/bakery/src/doctor.rs index 0a7194b..f36fe45 100644 --- a/bakery/src/doctor.rs +++ b/bakery/src/doctor.rs @@ -1,45 +1,37 @@ use anyhow::Result; use std::process::Command; -pub struct DepReport { - /// Required deps that are not present — blocks install. - pub missing: Vec, - /// Optional deps that are not present — advisory only, never blocks. - pub warnings: Vec, +/// Check whether a list of system dependencies are present. +/// Returns (missing, warnings) — missing are hard fails, warnings are advisory. +pub fn check_deps(deps: &[String]) -> Result> { + let mut missing = Vec::new(); + for dep in deps { + if !dep_present(dep) { + missing.push(dep.clone()); + } + } + Ok(missing) } -pub fn check_deps(required: &[String], optional: &[String]) -> Result { - Ok(DepReport { - missing: required.iter().filter(|d| !dep_present(d)).cloned().collect(), - warnings: optional.iter().filter(|d| !dep_present(d)).cloned().collect(), - }) -} - -fn dep_present(pkg: &str) -> bool { - // Primary: `pacman -Q` uses the exact Arch package name — no name mapping needed. - if pacman_installed(pkg) { +fn dep_present(dep: &str) -> bool { + // Try `which` first (covers executables like `iw`, `nmcli`). + if which(dep) { return true; } - // Fallback for environments without pacman: native PATH search then pkg-config. - path_has(pkg) || pkg_config_exists(pkg) + // Try `pkg-config --exists` for library packages (gtk4, gtk4-layer-shell, librsvg). + pkg_config_exists(dep) } -fn pacman_installed(pkg: &str) -> bool { - Command::new("pacman") - .args(["-Q", pkg]) +fn which(bin: &str) -> bool { + Command::new("which") + .arg(bin) .output() .map(|o| o.status.success()) .unwrap_or(false) } -/// Check PATH without shelling out to `which` (avoids the external dependency). -fn path_has(bin: &str) -> bool { - std::env::var_os("PATH") - .map(|p| std::env::split_paths(&p).any(|dir| dir.join(bin).is_file())) - .unwrap_or(false) -} - fn pkg_config_exists(lib: &str) -> bool { + // Arch package names map directly to pkg-config names for GTK libs. Command::new("pkg-config") .arg("--exists") .arg(lib) @@ -48,90 +40,33 @@ fn pkg_config_exists(lib: &str) -> bool { .unwrap_or(false) } -/// Print a formatted doctor report for a package's system deps. -/// Returns true if all *required* deps are satisfied. -pub fn report(package_name: &str, required: &[String], optional: &[String]) -> bool { - if required.is_empty() && optional.is_empty() { +/// Print a formatted doctor report for a list of system deps. +/// Returns true if all deps are satisfied. +pub fn report(package_name: &str, deps: &[String]) -> bool { + if deps.is_empty() { println!(" {package_name}: no system deps required"); return true; } - match check_deps(required, optional) { + match check_deps(deps) { Err(e) => { - eprintln!(" error running doctor for {package_name}: {e}"); + eprintln!(" error running doctor: {e}"); false } - Ok(rep) => { - for warn in &rep.warnings { - eprintln!( - " {package_name}: optional dep not found: {warn} \ - (install for full functionality)" - ); - } - if rep.missing.is_empty() { - println!(" {package_name}: all required system deps satisfied"); + Ok(missing) => { + if missing.is_empty() { + println!(" {package_name}: all system deps satisfied"); true } else { eprintln!( " {package_name}: missing system deps: {}", - rep.missing.join(", ") + missing.join(", ") + ); + eprintln!( + " install with: sudo pacman -S {}", + missing.join(" ") ); - eprintln!(" install with: sudo pacman -S {}", rep.missing.join(" ")); false } } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn empty_deps_pass() { - let rep = check_deps(&[], &[]).unwrap(); - assert!(rep.missing.is_empty()); - assert!(rep.warnings.is_empty()); - } - - // This test only runs on systems where pacman is available (Arch Linux). - #[test] - #[ignore] - fn pacman_finds_itself() { - assert!(pacman_installed("pacman")); - } - - #[test] - fn path_has_finds_sh() { - assert!(path_has("sh")); - } - - #[test] - fn missing_required_dep_detected() { - let rep = check_deps( - &["this-package-does-not-exist-xyzzy42".to_string()], - &[], - ) - .unwrap(); - assert_eq!(rep.missing.len(), 1); - assert!(rep.warnings.is_empty()); - } - - #[test] - fn missing_optional_dep_becomes_warning_not_error() { - let rep = check_deps( - &[], - &["this-package-does-not-exist-xyzzy42".to_string()], - ) - .unwrap(); - assert!(rep.missing.is_empty()); - assert_eq!(rep.warnings.len(), 1); - } - - // This test only runs on systems where pacman is available (Arch Linux). - #[test] - #[ignore] - fn installed_dep_not_missing() { - let rep = check_deps(&["pacman".to_string()], &[]).unwrap(); - assert!(rep.missing.is_empty()); - } -} diff --git a/bakery/src/download.rs b/bakery/src/download.rs index c89744c..8701470 100644 --- a/bakery/src/download.rs +++ b/bakery/src/download.rs @@ -45,34 +45,3 @@ fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<()> { } Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use sha2::{Digest, Sha256}; - - fn sha256_hex(data: &[u8]) -> String { - hex::encode(Sha256::digest(data)) - } - - #[test] - fn verify_correct_hash() { - let bytes = b"hello bakery"; - let hash = sha256_hex(bytes); - assert!(verify_sha256(bytes, &hash).is_ok()); - } - - #[test] - fn verify_wrong_hash_fails() { - let bytes = b"hello bakery"; - let wrong = "0".repeat(64); - assert!(verify_sha256(bytes, &wrong).is_err()); - } - - #[test] - fn verify_empty_bytes() { - let bytes = b""; - let hash = sha256_hex(bytes); - assert!(verify_sha256(bytes, &hash).is_ok()); - } -} diff --git a/bakery/src/install.rs b/bakery/src/install.rs index 03fb65f..2562e4d 100644 --- a/bakery/src/install.rs +++ b/bakery/src/install.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use crate::download::fetch_and_place; -use crate::manifest::{fetch_binary, Package, Service}; +use crate::manifest::{Package, Service}; use crate::state::{InstalledPackage, State}; pub fn install_package(pkg: &Package, bin_dir: &Path) -> Result<()> { @@ -18,15 +18,15 @@ pub fn install_package(pkg: &Package, bin_dir: &Path) -> Result<()> { binary_names.push(install_name.to_string()); } - // 2. Scaffold config dir + download example file. + // 2. Scaffold config dir + example file. if let Some(cfg) = &pkg.config { - scaffold_config(cfg, pkg)?; + scaffold_config(cfg)?; } // 3. Install systemd user units. let mut service_names = Vec::new(); for svc in &pkg.services { - install_service(svc, bin_dir, pkg)?; + install_service(svc, bin_dir)?; service_names.push(svc.unit.clone()); } @@ -60,8 +60,6 @@ pub fn remove_package(pkg_name: &str, bin_dir: &Path) -> Result<()> { return Ok(()); } }; - // Commit removal immediately — file cleanup below is best-effort. - state.save()?; // Remove binaries. for bin in &installed.binaries { @@ -106,111 +104,66 @@ pub fn remove_package(pkg_name: &str, bin_dir: &Path) -> Result<()> { println!(" data preserved at {}", data_dir.display()); } + state.save()?; println!(" {pkg_name} removed"); Ok(()) } -fn scaffold_config(cfg: &crate::manifest::ConfigScaffold, pkg: &Package) -> Result<()> { +fn scaffold_config(cfg: &crate::manifest::ConfigScaffold) -> Result<()> { let dir = expand_tilde(&cfg.dir); std::fs::create_dir_all(&dir)?; - if let Some(example) = &cfg.example { let dest = dir.join(example); if !dest.exists() { - if let Some((primary, fallback)) = pkg.artifact_urls(example) { - match fetch_binary(&primary, &fallback) { - Ok(bytes) => { - std::fs::write(&dest, &bytes) - .with_context(|| format!("writing {}", dest.display()))?; - println!(" installed example config at {}", dest.display()); - } - Err(e) => { - eprintln!(" warning: could not download example config {example}: {e}"); - println!(" config dir created at {}", dir.display()); - } - } - } else { - println!(" config dir created at {}", dir.display()); - } + // We don't have the actual example file here at install time — + // the product repo's release bundle should include it. + // For now just note it; release.yml will bundle example configs. + println!(" config dir ready at {}", dir.display()); + println!( + " copy your {example} to {} to configure {}", + dest.display(), + dir.display() + ); } else { println!(" config at {} already exists, skipping", dest.display()); } - } else { - println!(" config dir created at {}", dir.display()); } Ok(()) } -fn install_service(svc: &Service, bin_dir: &Path, pkg: &Package) -> Result<()> { +fn install_service(svc: &Service, bin_dir: &Path) -> Result<()> { let service_dir = systemd_user_dir(); std::fs::create_dir_all(&service_dir)?; let unit_path = service_dir.join(&svc.unit); - // Download the unit file if not already present. - if !unit_path.exists() { - if let Some((primary, fallback)) = pkg.artifact_urls(&svc.unit) { - match fetch_binary(&primary, &fallback) { - Ok(bytes) => { - std::fs::write(&unit_path, &bytes) - .with_context(|| format!("writing {}", unit_path.display()))?; - println!(" downloaded unit {}", unit_path.display()); - } - Err(e) => { - eprintln!(" warning: could not download {}: {e}", svc.unit); - } - } - } else { - eprintln!(" warning: no artifact URL to download {}", svc.unit); - } + // The unit file is expected to be bundled alongside the binary in the + // release artifact (or embedded). For now, patch ExecStart if the unit + // already exists (same pattern as bread/scripts/install.sh). + if unit_path.exists() { + patch_exec_start(&unit_path, bin_dir)?; } - if !unit_path.exists() { - eprintln!( - " warning: unit file {} not found — skipping service setup", - svc.unit - ); - return Ok(()); - } - - patch_exec_start(&unit_path, bin_dir)?; - - if !Command::new("systemctl") + let _ = Command::new("systemctl") .args(["--user", "daemon-reload"]) - .status() - .map(|s| s.success()) - .unwrap_or(false) - { - eprintln!(" warning: systemctl daemon-reload failed"); - } + .status(); if svc.enable { - let already_active = Command::new("systemctl") + if Command::new("systemctl") .args(["--user", "is-active", "--quiet", &svc.unit]) .status() .map(|s| s.success()) - .unwrap_or(false); - - if already_active { - if Command::new("systemctl") - .args(["--user", "restart", &svc.unit]) - .status() - .map(|s| s.success()) - .unwrap_or(false) - { - println!(" {} restarted", svc.unit); - } else { - eprintln!(" warning: failed to restart {}", svc.unit); - } - } else if Command::new("systemctl") - .args(["--user", "enable", "--now", &svc.unit]) - .status() - .map(|s| s.success()) .unwrap_or(false) { - println!(" {} enabled and started", svc.unit); + let _ = Command::new("systemctl") + .args(["--user", "restart", &svc.unit]) + .status(); + println!(" {} restarted", svc.unit); } else { - eprintln!(" warning: failed to enable {}", svc.unit); + let _ = Command::new("systemctl") + .args(["--user", "enable", "--now", &svc.unit]) + .status(); + println!(" {} enabled and started", svc.unit); } } @@ -223,6 +176,7 @@ fn patch_exec_start(unit_path: &Path, bin_dir: &Path) -> Result<()> { .lines() .map(|line| { if line.trim_start().starts_with("ExecStart=") { + // Replace only the path prefix, keep args. let rest = line.splitn(2, '=').nth(1).unwrap_or(""); let argv: Vec<&str> = rest.split_whitespace().collect(); if let Some(bin_name) = argv.first().and_then(|p| Path::new(p).file_name()) { @@ -242,13 +196,7 @@ fn patch_exec_start(unit_path: &Path, bin_dir: &Path) -> Result<()> { }) .collect::>() .join("\n"); - // Preserve trailing newline if the original had one. - let output = if text.ends_with('\n') { - format!("{patched}\n") - } else { - patched - }; - std::fs::write(unit_path, output)?; + std::fs::write(unit_path, patched)?; Ok(()) } @@ -293,7 +241,7 @@ fn expand_tilde(path: &str) -> PathBuf { } } -pub fn strip_arch_suffix(name: &str) -> &str { +fn strip_arch_suffix(name: &str) -> &str { const SUFFIXES: &[&str] = &["-x86_64", "-aarch64", "-arm64", "-armv7"]; for s in SUFFIXES { if let Some(base) = name.strip_suffix(s) { @@ -314,53 +262,3 @@ fn warn_path_if_needed(bin_dir: &Path) { println!(" export PATH=\"{}:$PATH\"", bin_str); } } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::tempdir; - - #[test] - fn strip_known_suffixes() { - assert_eq!(strip_arch_suffix("breadd-x86_64"), "breadd"); - assert_eq!(strip_arch_suffix("breadd-aarch64"), "breadd"); - assert_eq!(strip_arch_suffix("breadd-arm64"), "breadd"); - assert_eq!(strip_arch_suffix("breadd-armv7"), "breadd"); - assert_eq!(strip_arch_suffix("bakery-x86_64"), "bakery"); - assert_eq!(strip_arch_suffix("breadd"), "breadd"); - } - - #[test] - fn patch_exec_start_with_args() { - let dir = tempdir().unwrap(); - let path = dir.path().join("test.service"); - fs::write(&path, "[Service]\nExecStart=/old/path/bin arg1 arg2\n").unwrap(); - patch_exec_start(&path, Path::new("/new/bin")).unwrap(); - let out = fs::read_to_string(&path).unwrap(); - assert!(out.contains("ExecStart=/new/bin/bin arg1 arg2")); - assert!(out.ends_with('\n')); - } - - #[test] - fn patch_exec_start_no_args() { - let dir = tempdir().unwrap(); - let path = dir.path().join("test.service"); - fs::write(&path, "[Service]\nExecStart=/old/path/daemon\n").unwrap(); - patch_exec_start(&path, Path::new("/usr/local/bin")).unwrap(); - let out = fs::read_to_string(&path).unwrap(); - assert!(out.contains("ExecStart=/usr/local/bin/daemon")); - assert!(!out.contains("daemon ")); - } - - #[test] - fn patch_exec_start_non_exec_lines_unchanged() { - let dir = tempdir().unwrap(); - let path = dir.path().join("test.service"); - fs::write(&path, "[Unit]\nDescription=foo\nExecStart=/bin/foo\n").unwrap(); - patch_exec_start(&path, Path::new("/usr/bin")).unwrap(); - let out = fs::read_to_string(&path).unwrap(); - assert!(out.contains("Description=foo")); - assert!(out.contains("ExecStart=/usr/bin/foo")); - } -} diff --git a/bakery/src/main.rs b/bakery/src/main.rs index 821f55a..1fec0bb 100644 --- a/bakery/src/main.rs +++ b/bakery/src/main.rs @@ -6,11 +6,10 @@ mod state; use anyhow::{bail, Result}; use clap::{Parser, Subcommand}; -use std::collections::HashSet; use std::path::PathBuf; #[derive(Parser)] -#[command(name = "bakery", about = "Package manager for the bread ecosystem", version)] +#[command(name = "bakery", about = "Package manager for the bread ecosystem")] struct Cli { #[command(subcommand)] command: Cmd, @@ -21,10 +20,9 @@ struct Cli { #[derive(Subcommand)] enum Cmd { - /// Install one or more packages + /// Install a package Install { - #[arg(required = true, num_args = 1..)] - packages: Vec, + package: String, }, /// Remove an installed package (data files are never deleted) Remove { @@ -32,12 +30,8 @@ enum Cmd { }, /// Update one or all installed packages Update { - /// Package to update (omit or use --all to update everything installed) - #[arg(conflicts_with = "all")] + /// Package to update; omit to update all installed packages package: Option, - /// Update all installed packages - #[arg(long, conflicts_with = "package")] - all: bool, }, /// List packages List { @@ -67,59 +61,27 @@ fn main() -> Result<()> { let bin_dir = cli.bin_dir.unwrap_or_else(default_bin_dir); match cli.command { - Cmd::Install { packages } => { - let index = manifest::load(true)?; - for pkg in &packages { - cmd_install(&index, pkg, &bin_dir)?; - } - Ok(()) - } + Cmd::Install { package } => cmd_install(&package, &bin_dir), Cmd::Remove { package } => cmd_remove(&package, &bin_dir), - Cmd::Update { package, all } => cmd_update(package.as_deref(), all, &bin_dir), + Cmd::Update { package } => cmd_update(package.as_deref(), &bin_dir), Cmd::List { installed } => cmd_list(installed), Cmd::Info { package } => cmd_info(&package), Cmd::Doctor { package } => cmd_doctor(package.as_deref()), } } -fn cmd_install(index: &manifest::Index, name: &str, bin_dir: &std::path::Path) -> Result<()> { - let mut visited = HashSet::new(); - install_with_deps(index, name, bin_dir, &mut visited) -} - -/// Recursively installs `name` and any bread_deps, skipping already-installed -/// packages. The `visited` set prevents cycles. -fn install_with_deps( - index: &manifest::Index, - name: &str, - bin_dir: &std::path::Path, - visited: &mut HashSet, -) -> Result<()> { - if !visited.insert(name.to_string()) { - return Ok(()); - } - +fn cmd_install(name: &str, bin_dir: &std::path::Path) -> Result<()> { + let index = manifest::load(false)?; let pkg = index .get(name) .ok_or_else(|| anyhow::anyhow!("unknown package: {name}"))?; - // Install bread_deps first (skip those already recorded in state). - let state = state::State::load()?; - for dep in pkg.bread_deps.clone() { - if !state.is_installed(&dep) { - println!("installing bread dependency: {dep}"); - install_with_deps(index, &dep, bin_dir, visited)?; - } - } - - println!("checking system dependencies for {name}…"); - let rep = doctor::check_deps(&pkg.system_deps, &pkg.optional_system_deps)?; - for warn in &rep.warnings { - eprintln!(" note: optional dep not installed: {warn}"); - } - if !rep.missing.is_empty() { - eprintln!("missing system deps for {name}: {}", rep.missing.join(", ")); - eprintln!("install with: sudo pacman -S {}", rep.missing.join(" ")); + // Doctor runs first — bail if system deps are missing. + println!("checking system dependencies…"); + let missing = doctor::check_deps(&pkg.system_deps)?; + if !missing.is_empty() { + eprintln!("missing system dependencies for {name}: {}", missing.join(", ")); + eprintln!("install with: sudo pacman -S {}", missing.join(" ")); bail!("system deps not satisfied"); } @@ -130,22 +92,15 @@ fn cmd_remove(name: &str, bin_dir: &std::path::Path) -> Result<()> { install::remove_package(name, bin_dir) } -fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Result<()> { - let index = manifest::load(true)?; +fn cmd_update(name: Option<&str>, bin_dir: &std::path::Path) -> Result<()> { + let index = manifest::load(true)?; // force refresh on update let state = state::State::load()?; - let targets: Vec = if all || name.is_none() { - state.packages.keys().cloned().collect() - } else { - vec![name.unwrap().to_string()] + let targets: Vec = match name { + Some(n) => vec![n.to_string()], + None => state.packages.keys().cloned().collect(), }; - if targets.is_empty() { - println!("no packages installed"); - return Ok(()); - } - - let mut any_failed = false; for pkg_name in &targets { let installed = match state.packages.get(pkg_name.as_str()) { Some(p) => p, @@ -161,45 +116,15 @@ fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Resul continue; } }; - if installed.version == latest.version { println!("{pkg_name} is already at {}", installed.version); - continue; - } - - println!( - "updating {pkg_name} {} → {}", - installed.version, latest.version - ); - - let rep = match doctor::check_deps(&latest.system_deps, &latest.optional_system_deps) { - Ok(r) => r, - Err(e) => { - eprintln!(" doctor check failed for {pkg_name}: {e}"); - any_failed = true; - continue; - } - }; - for warn in &rep.warnings { - eprintln!(" note: optional dep not installed: {warn}"); - } - if !rep.missing.is_empty() { - eprintln!( - " missing deps for {pkg_name}: {} — skipping update", - rep.missing.join(", ") + } else { + println!( + "updating {pkg_name} {} → {}", + installed.version, latest.version ); - any_failed = true; - continue; + install::install_package(latest, bin_dir)?; } - - if let Err(e) = install::install_package(latest, bin_dir) { - eprintln!(" failed to update {pkg_name}: {e}"); - any_failed = true; - } - } - - if any_failed { - bail!("one or more packages could not be updated"); } Ok(()) } @@ -248,32 +173,15 @@ fn cmd_info(name: &str) -> Result<()> { println!("{} {}", pkg.name, pkg.version); println!(" {}", pkg.description); println!(" status: {status}"); - println!( - " binaries: {}", - pkg.binaries - .iter() - .map(|b| b.name.as_str()) - .collect::>() - .join(", ") - ); + println!(" binaries: {}", pkg.binaries.iter().map(|b| b.name.as_str()).collect::>().join(", ")); if !pkg.system_deps.is_empty() { println!(" system deps: {}", pkg.system_deps.join(", ")); } - if !pkg.optional_system_deps.is_empty() { - println!(" optional deps: {}", pkg.optional_system_deps.join(", ")); - } if !pkg.bread_deps.is_empty() { println!(" bread deps: {}", pkg.bread_deps.join(", ")); } if !pkg.services.is_empty() { - println!( - " services: {}", - pkg.services - .iter() - .map(|s| s.unit.as_str()) - .collect::>() - .join(", ") - ); + println!(" services: {}", pkg.services.iter().map(|s| s.unit.as_str()).collect::>().join(", ")); } Ok(()) } @@ -283,12 +191,7 @@ fn cmd_doctor(name: Option<&str>) -> Result<()> { let state = state::State::load()?; let targets: Vec = match name { - Some(n) => { - if index.get(n).is_none() { - bail!("unknown package: {n}"); - } - vec![n.to_string()] - } + Some(n) => vec![n.to_string()], None => state.packages.keys().cloned().collect(), }; @@ -300,12 +203,9 @@ fn cmd_doctor(name: Option<&str>) -> Result<()> { let mut all_ok = true; for pkg_name in &targets { if let Some(pkg) = index.get(pkg_name) { - if !doctor::report(pkg_name, &pkg.system_deps, &pkg.optional_system_deps) { + if !doctor::report(pkg_name, &pkg.system_deps) { all_ok = false; } - } else { - eprintln!(" {pkg_name}: not found in index (removed from registry?)"); - all_ok = false; } } diff --git a/bakery/src/manifest.rs b/bakery/src/manifest.rs index 5106646..5f27249 100644 --- a/bakery/src/manifest.rs +++ b/bakery/src/manifest.rs @@ -23,7 +23,7 @@ pub struct Service { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ConfigScaffold { pub dir: String, - /// Example config filename, relative to the release artifact directory. + /// relative to the product repo root; copied as-is if absent at install time pub example: Option, } @@ -36,8 +36,6 @@ pub struct Package { #[serde(default)] pub system_deps: Vec, #[serde(default)] - pub optional_system_deps: Vec, - #[serde(default)] pub bread_deps: Vec, #[serde(default)] pub services: Vec, @@ -46,21 +44,6 @@ pub struct Package { pub post_install: Vec, } -impl Package { - /// Returns `(primary_url, github_url)` for any artifact filename in this - /// package's release directory. Derived by stripping the filename from the - /// first binary's URLs. - pub fn artifact_urls(&self, filename: &str) -> Option<(String, String)> { - let first = self.binaries.first()?; - let dl_base = first.dl_url.rsplit_once('/')?.0; - let gh_base = first.github_url.rsplit_once('/')?.0; - Some(( - format!("{dl_base}/{filename}"), - format!("{gh_base}/{filename}"), - )) - } -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Index { pub version: String, @@ -84,7 +67,8 @@ pub fn load(force_refresh: bool) -> Result { let cache_path = cache_path(); if !force_refresh && cache_is_fresh(&cache_path) { - let text = std::fs::read_to_string(&cache_path).context("reading cached index")?; + let text = std::fs::read_to_string(&cache_path) + .context("reading cached index")?; return serde_json::from_str(&text).context("parsing cached index"); } @@ -148,6 +132,6 @@ fn fetch_bytes(url: &str) -> Result> { let mut buf = Vec::new(); resp.into_reader() .read_to_end(&mut buf) - .context("reading response")?; + .context("reading binary")?; Ok(buf) } diff --git a/bakery/src/state.rs b/bakery/src/state.rs index 92b9aa9..adb39a4 100644 --- a/bakery/src/state.rs +++ b/bakery/src/state.rs @@ -33,12 +33,7 @@ impl State { std::fs::create_dir_all(dir)?; } let text = serde_json::to_string_pretty(self)?; - // 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(()) + std::fs::write(&path, text).context("writing installed.json") } pub fn is_installed(&self, name: &str) -> bool { @@ -63,58 +58,3 @@ 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"]); - } -} diff --git a/registry/bread-ecosystem.toml b/registry/bread-ecosystem.toml index 530f938..fb641d8 100644 --- a/registry/bread-ecosystem.toml +++ b/registry/bread-ecosystem.toml @@ -7,11 +7,6 @@ description = "Reactive desktop automation ecosystem for Arch Linux / Hyprland" homepage = "https://breadway.dev" dl_base = "https://dl.breadway.dev" -[[products]] -name = "bakery" -repo = "Breadway/bread-ecosystem" -description = "Bread ecosystem package manager" - [[products]] name = "bread" repo = "Breadway/bread" diff --git a/scripts/gen-index.sh b/scripts/gen-index.sh old mode 100755 new mode 100644 index 86d3f40..f695f56 --- a/scripts/gen-index.sh +++ b/scripts/gen-index.sh @@ -1,28 +1,28 @@ #!/usr/bin/env bash # Generate dl.breadway.dev/index.json from: # - registry/bread-ecosystem.toml (product list) -# - //bakery.toml (per-product metadata, uploaded by release.yml) -# - / (built binaries + sha256 files) +# - /bakery.toml (per-product metadata) +# - /srv/breadway-dl/ (built binaries + sha256 files) # -# Fallback for local dev: looks for ../name/bakery.toml (sibling repo checkout). # Run on hestia after each product build, before the dl server is refreshed. -# Requires: jq, python3 (tomllib, stdlib since 3.11), sha256sum +# Requires: jq, python3 (for toml parsing via tomllib), sha256sum set -euo pipefail -SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DL_DIR="${DL_DIR:-/srv/breadway-dl}" DL_BASE="${DL_BASE:-https://dl.breadway.dev}" GH_BASE="https://github.com" OUT="${DL_DIR}/index.json" -# Read the product list from the registry TOML instead of a hardcoded array. -mapfile -t products < <(python3 -c " -import tomllib, sys -with open('${SCRIPT_DIR}/registry/bread-ecosystem.toml', 'rb') as f: - d = tomllib.load(f) -for p in d['products']: - print(p['name'], p['repo']) -") +# Products are read from the registry. Each line is "name repo". +products=( + "bakery Breadway/bread-ecosystem" + "bread Breadway/bread" + "breadbar Breadway/breadbar" + "breadbox Breadway/breadbox" + "breadcrumbs Breadway/breadcrumbs" + "breadpad Breadway/breadpad" +) # Build a JSON package entry for one product. # $1 = product name, $2 = github repo slug @@ -34,14 +34,14 @@ build_package_json() { local pkg_dir="${DL_DIR}/${name}" if [[ ! -d "${pkg_dir}" ]]; then echo " warning: no release dir for ${name} at ${pkg_dir}" >&2 - return 1 + return fi # The latest symlink must point to the current version dir. local latest_link="${pkg_dir}/latest" if [[ ! -L "${latest_link}" ]]; then echo " warning: no 'latest' symlink for ${name}" >&2 - return 1 + return fi local version_dir version_dir="$(readlink -f "${latest_link}")" @@ -51,11 +51,11 @@ build_package_json() { # Collect all binaries in the version dir (executables only; skip metadata files). local binaries_json="[]" for bin_path in "${version_dir}"/*; do - [[ "${bin_path}" == *.sha256 ]] && continue - [[ "${bin_path}" == *.toml ]] && continue + [[ "${bin_path}" == *.sha256 ]] && continue + [[ "${bin_path}" == *.toml ]] && continue [[ "${bin_path}" == *.service ]] && continue - [[ "${bin_path}" == *.css ]] && continue - [[ "${bin_path}" == *.txt ]] && continue + [[ "${bin_path}" == *.css ]] && continue + [[ "${bin_path}" == *.txt ]] && continue [[ -f "${bin_path}" ]] || continue local bin_name bin_name="$(basename "${bin_path}")" @@ -77,78 +77,45 @@ build_package_json() { binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')" done - # Locate bakery.toml. The release workflow copies it into the version dir - # alongside the binaries (${version_dir}/bakery.toml). Fall back to a - # sibling repo checkout for local dev use. - local bakery_toml="${version_dir}/bakery.toml" + # Read bakery.toml: the release workflow copies it to DL_DIR alongside the + # binaries; fall back to a sibling checkout for local dev use. + local bakery_toml="${DL_DIR}/${name}/bakery.toml" if [[ ! -f "${bakery_toml}" ]]; then bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml" fi - if [[ ! -f "${bakery_toml}" ]]; then - echo "ERROR: bakery.toml not found for ${name} — release.yml must copy it to \${DL_DIR}/${name}/\${VERSION}/bakery.toml" >&2 - return 1 - fi + local description="" + local system_deps="[]" + local bread_deps="[]" + local services="[]" + local config="null" + local post_install="[]" - local description system_deps optional_system_deps bread_deps services config post_install - - description="$(python3 -c " -import tomllib + if [[ -f "${bakery_toml}" ]]; then + description="$(python3 -c " +import tomllib, sys with open('${bakery_toml}', 'rb') as f: d = tomllib.load(f) print(d.get('description', '')) " 2>/dev/null || true)" - - system_deps="$(python3 -c " -import tomllib, json + system_deps="$(python3 -c " +import tomllib, json, sys with open('${bakery_toml}', 'rb') as f: d = tomllib.load(f) print(json.dumps(d.get('system_deps', []))) " 2>/dev/null || echo "[]")" - - optional_system_deps="$(python3 -c " -import tomllib, json -with open('${bakery_toml}', 'rb') as f: - d = tomllib.load(f) -print(json.dumps(d.get('optional_system_deps', []))) -" 2>/dev/null || echo "[]")" - - bread_deps="$(python3 -c " -import tomllib, json + bread_deps="$(python3 -c " +import tomllib, json, sys with open('${bakery_toml}', 'rb') as f: d = tomllib.load(f) print(json.dumps(d.get('bread_deps', []))) " 2>/dev/null || echo "[]")" - - # [[service]] entries → [{unit, enable}] - services="$(python3 -c " -import tomllib, json -with open('${bakery_toml}', 'rb') as f: - d = tomllib.load(f) -svcs = d.get('service', []) -print(json.dumps([{'unit': s['unit'], 'enable': s.get('enable', False)} for s in svcs])) -" 2>/dev/null || echo "[]")" - - # [config] → {dir, example?} or null - config="$(python3 -c " -import tomllib, json -with open('${bakery_toml}', 'rb') as f: - d = tomllib.load(f) -cfg = d.get('config') -if cfg: - obj = {'dir': cfg['dir']} - if 'example' in cfg: - obj['example'] = cfg['example'] - print(json.dumps(obj)) -else: - print('null') -" 2>/dev/null || echo "null")" - - post_install="$(python3 -c " -import tomllib, json + post_install="$(python3 -c " +import tomllib, json, sys with open('${bakery_toml}', 'rb') as f: d = tomllib.load(f) print(json.dumps(d.get('install', {}).get('post_install', []))) " 2>/dev/null || echo "[]")" + fi jq -n \ --arg name "${name}" \ @@ -156,10 +123,8 @@ print(json.dumps(d.get('install', {}).get('post_install', []))) --arg version "${version}" \ --argjson binaries "${binaries_json}" \ --argjson system_deps "${system_deps}" \ - --argjson optional_system_deps "${optional_system_deps}" \ --argjson bread_deps "${bread_deps}" \ --argjson services "${services}" \ - --argjson config "${config}" \ --argjson post_install "${post_install}" \ '{ name: $name, @@ -167,10 +132,8 @@ print(json.dumps(d.get('install', {}).get('post_install', []))) version: $version, binaries: $binaries, system_deps: $system_deps, - optional_system_deps: $optional_system_deps, bread_deps: $bread_deps, services: $services, - config: $config, post_install: $post_install }' } diff --git a/scripts/get.sh b/scripts/get.sh old mode 100755 new mode 100644 index cc343f0..bdcf743 --- a/scripts/get.sh +++ b/scripts/get.sh @@ -1,10 +1,12 @@ #!/bin/sh -# Bootstrap script: downloads and installs the `bakery` binary. +# Bootstrap script: installs the `bakery` binary. # Usage: curl https://breadway.dev/get | sh # Or: curl -sSfL https://breadway.dev/get | sh set -eu BAKERY_VERSION="${BAKERY_VERSION:-latest}" +DL_PRIMARY="https://dl.breadway.dev/bakery/${BAKERY_VERSION}/bakery-x86_64" +DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/${BAKERY_VERSION}/bakery-x86_64" BIN_DIR="${BAKERY_BIN_DIR:-$HOME/.local/bin}" die() { echo "error: $*" >&2; exit 1; } @@ -13,20 +15,6 @@ die() { echo "error: $*" >&2; exit 1; } uname -m | grep -q x86_64 || die "bakery only supports x86_64 (got $(uname -m))" uname -s | grep -q Linux || die "bakery only supports Linux (got $(uname -s))" -# Build download URLs. GitHub's "latest" redirect lives at a different path from -# versioned releases, so we handle them separately and always prefix tags with 'v'. -if [ "${BAKERY_VERSION}" = "latest" ]; then - DL_PRIMARY="https://dl.breadway.dev/bakery/latest/bakery-x86_64" - DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/latest/download/bakery-x86_64" - SHA256_URL="https://dl.breadway.dev/bakery/latest/bakery-x86_64.sha256" -else - # Strip a leading 'v' if the caller included it, then add it back consistently. - ver="${BAKERY_VERSION#v}" - DL_PRIMARY="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64" - DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/v${ver}/bakery-x86_64" - SHA256_URL="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64.sha256" -fi - # Pick a download tool. if command -v curl >/dev/null 2>&1; then fetch() { curl -fsSL "$1" -o "$2"; } @@ -38,26 +26,13 @@ fi mkdir -p "${BIN_DIR}" TMP="$(mktemp)" -trap 'rm -f "${TMP}" "${TMP}.sha256"' EXIT +trap 'rm -f "${TMP}"' EXIT echo "downloading bakery…" if fetch "${DL_PRIMARY}" "${TMP}" 2>/dev/null; then echo " from dl.breadway.dev" - # Verify checksum when available from primary. - if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then - expected="$(awk '{print $1}' "${TMP}.sha256")" - actual="$(sha256sum "${TMP}" | awk '{print $1}')" - if [ "${expected}" != "${actual}" ]; then - die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})" - fi - echo " checksum verified" - else - echo " warning: could not fetch checksum — skipping verification" - fi elif fetch "${DL_FALLBACK}" "${TMP}" 2>/dev/null; then echo " from GitHub (fallback)" - # No .sha256 on the GitHub fallback path; proceed without verification. - echo " warning: checksum not verified for GitHub fallback download" else die "failed to download bakery from both primary and fallback URLs" fi diff --git a/scripts/test-gen-index.sh b/scripts/test-gen-index.sh deleted file mode 100755 index 5a2733a..0000000 --- a/scripts/test-gen-index.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash -# Smoke-test gen-index.sh against a minimal fixture DL_DIR tree. -# Verifies that services, config, system_deps, optional_system_deps, -# description, and post_install are all populated correctly. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -FIXTURE="$(mktemp -d)" -FAKE_REGISTRY="$(mktemp -d)" -trap 'rm -rf "${FIXTURE}" "${FAKE_REGISTRY}"' EXIT - -fail() { echo "FAIL: $*" >&2; exit 1; } - -# ── Build a minimal release tree for "fakepkg" ─────────────────────────────── -PKG_VER_DIR="${FIXTURE}/fakepkg/0.1.0" -mkdir -p "${PKG_VER_DIR}" - -printf 'fake-binary-content' > "${PKG_VER_DIR}/fakepkg-x86_64" -sha256sum "${PKG_VER_DIR}/fakepkg-x86_64" | awk '{print $1}' \ - > "${PKG_VER_DIR}/fakepkg-x86_64.sha256" -printf '[Unit]\nDescription=fakepkg\n' > "${PKG_VER_DIR}/fakepkg.service" -printf '# example config\n' > "${PKG_VER_DIR}/fakepkg.example.toml" - -cat > "${PKG_VER_DIR}/bakery.toml" <<'TOML' -name = "fakepkg" -description = "A fake package for testing" -binaries = ["fakepkg"] -system_deps = ["gtk4"] -optional_system_deps = ["hyprland"] -bread_deps = [] - -[[service]] -unit = "fakepkg.service" -enable = true - -[config] -dir = "~/.config/fakepkg" -example = "fakepkg.example.toml" - -[install] -post_install = ["echo installed"] -TOML - -# gen-index looks for bakery.toml at ${DL_DIR}//bakery.toml (no version) -cp "${PKG_VER_DIR}/bakery.toml" "${FIXTURE}/fakepkg/bakery.toml" -ln -s "${PKG_VER_DIR}" "${FIXTURE}/fakepkg/latest" - -# ── Minimal registry pointing only at fakepkg ──────────────────────────────── -mkdir -p "${FAKE_REGISTRY}/registry" -cat > "${FAKE_REGISTRY}/registry/bread-ecosystem.toml" <<'TOML' -[ecosystem] -name = "test" - -[[products]] -name = "fakepkg" -repo = "Test/fakepkg" -description = "A fake package" -TOML - -# ── Run gen-index with overridden SCRIPT_DIR and DL_DIR ────────────────────── -OUT="${FIXTURE}/index.json" -SCRIPT_DIR="${FAKE_REGISTRY}" DL_DIR="${FIXTURE}" DL_BASE="https://dl.test" \ - bash "${REPO_ROOT}/scripts/gen-index.sh" 2>&1 | sed 's/^/ [gen-index] /' - -[[ -f "${OUT}" ]] || fail "index.json was not produced" - -# ── Assertions ──────────────────────────────────────────────────────────────── -jq -e '.packages.fakepkg' "${OUT}" > /dev/null \ - || fail "fakepkg missing from index" - -check() { - local label="$1" expected="$2" actual="$3" - [[ "${actual}" == "${expected}" ]] \ - || fail "${label}: expected '${expected}', got '${actual}'" -} - -check "description" \ - "A fake package for testing" \ - "$(jq -r '.packages.fakepkg.description' "${OUT}")" - -check "system_deps" \ - "gtk4" \ - "$(jq -r '.packages.fakepkg.system_deps | join(",")' "${OUT}")" - -check "optional_system_deps" \ - "hyprland" \ - "$(jq -r '.packages.fakepkg.optional_system_deps | join(",")' "${OUT}")" - -check "services[0].unit" \ - "fakepkg.service" \ - "$(jq -r '.packages.fakepkg.services[0].unit' "${OUT}")" - -check "services[0].enable" \ - "true" \ - "$(jq -r '.packages.fakepkg.services[0].enable' "${OUT}")" - -check "config.dir" \ - "~/.config/fakepkg" \ - "$(jq -r '.packages.fakepkg.config.dir' "${OUT}")" - -check "config.example" \ - "fakepkg.example.toml" \ - "$(jq -r '.packages.fakepkg.config.example' "${OUT}")" - -check "binaries[0].name" \ - "fakepkg-x86_64" \ - "$(jq -r '.packages.fakepkg.binaries[0].name' "${OUT}")" - -check "post_install[0]" \ - "echo installed" \ - "$(jq -r '.packages.fakepkg.post_install[0]' "${OUT}")" - -echo "OK: all gen-index assertions passed"