Compare commits
No commits in common. "85a1a867ce8c4fb921e3faeb8bf84c5e799fe083" and "74a3dc5cfa6edbde87cb401cfbfa51e70e531853" have entirely different histories.
85a1a867ce
...
74a3dc5cfa
15 changed files with 161 additions and 999 deletions
269
Cargo.lock
generated
269
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 <rileyhorsham@gmail.com>"]
|
||||
|
|
|
|||
45
README.md
45
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 <pkg> # 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 <packages>`. Use `pacman -Q <pkg>` 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/<name>/<version>/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `bakery.toml` | Metadata (deps, services, config) read by `gen-index.sh` |
|
||||
| `<binary>-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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ name = "bakery"
|
|||
description = "Bread ecosystem package manager"
|
||||
binaries = ["bakery"]
|
||||
system_deps = []
|
||||
optional_system_deps = []
|
||||
bread_deps = []
|
||||
|
||||
[install]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,3 @@ sha2 = { workspace = true }
|
|||
hex = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
/// Optional deps that are not present — advisory only, never blocks.
|
||||
pub warnings: Vec<String>,
|
||||
/// 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<Vec<String>> {
|
||||
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<DepReport> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>()
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
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<String>,
|
||||
/// 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<String>,
|
||||
) -> 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<String> = if all || name.is_none() {
|
||||
state.packages.keys().cloned().collect()
|
||||
} else {
|
||||
vec![name.unwrap().to_string()]
|
||||
let targets: Vec<String> = 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::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
println!(" binaries: {}", pkg.binaries.iter().map(|b| b.name.as_str()).collect::<Vec<_>>().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::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
println!(" services: {}", pkg.services.iter().map(|s| s.unit.as_str()).collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -283,12 +191,7 @@ fn cmd_doctor(name: Option<&str>) -> Result<()> {
|
|||
let state = state::State::load()?;
|
||||
|
||||
let targets: Vec<String> = 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
|
|
@ -36,8 +36,6 @@ pub struct Package {
|
|||
#[serde(default)]
|
||||
pub system_deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub optional_system_deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub bread_deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub services: Vec<Service>,
|
||||
|
|
@ -46,21 +44,6 @@ pub struct Package {
|
|||
pub post_install: Vec<String>,
|
||||
}
|
||||
|
||||
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<Index> {
|
|||
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<Vec<u8>> {
|
|||
let mut buf = Vec::new();
|
||||
resp.into_reader()
|
||||
.read_to_end(&mut buf)
|
||||
.context("reading response")?;
|
||||
.context("reading binary")?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
113
scripts/gen-index.sh
Executable file → Normal file
113
scripts/gen-index.sh
Executable file → Normal file
|
|
@ -1,28 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# Generate dl.breadway.dev/index.json from:
|
||||
# - registry/bread-ecosystem.toml (product list)
|
||||
# - <DL_DIR>/<name>/bakery.toml (per-product metadata, uploaded by release.yml)
|
||||
# - <DL_DIR>/ (built binaries + sha256 files)
|
||||
# - <repo>/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
|
||||
}'
|
||||
}
|
||||
|
|
|
|||
33
scripts/get.sh
Executable file → Normal file
33
scripts/get.sh
Executable file → Normal file
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}/<name>/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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue