platform: BreadClient command/health, fail-closed get.sh, registry README
Add BreadClient::command (unsourced bread.command.<app>.<verb> emit) plus health/api_version, and a clap-free screenshot_cli helper for the next pin. get.sh now dies if minisign or .minisig is missing — checksum-only is not enough to install. Generate the README products table from the registry, and refresh release-channels/CONTRIBUTING/CLAUDE.md to match.
This commit is contained in:
parent
69ce2d67a8
commit
08b71262da
10 changed files with 494 additions and 106 deletions
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -2,21 +2,7 @@
|
||||||
|
|
||||||
Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation.
|
Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation.
|
||||||
|
|
||||||
This repo follows the branch/release workflow documented in `CONTRIBUTING.md`
|
Follow [`CONTRIBUTING.md`](CONTRIBUTING.md) for any git, branch, or release work. Channel/track policy lives in [`docs/release-channels.md`](docs/release-channels.md). The product list is [`registry/bread-ecosystem.toml`](registry/bread-ecosystem.toml) — regenerate the README table with `scripts/gen-readme-products.sh` after editing it. Don't invent a second long-lived branch; there is only `main`.
|
||||||
— read and follow it for any git, branch, or release work here (the
|
|
||||||
single-trunk model, `feature/x`/`fix/x` branch naming, how RC tags work,
|
|
||||||
etc). Don't improvise a different workflow. The short version: there is one
|
|
||||||
long-lived branch, `main` — no `dev` or `beta` branch exists. `main`
|
|
||||||
auto-publishes a dev-track build on every push. "Beta" and "stable" are both
|
|
||||||
just tags, not branches: push a `vX.Y.Z-rc.N` tag to publish a beta-track
|
|
||||||
build, push a plain `vX.Y.Z` tag to cut the signed stable release.
|
|
||||||
"Freezing" for stabilization means pausing pushes to `main`, not moving a
|
|
||||||
branch. This replaced an earlier three-branch (`dev`/`beta`/`main`) model
|
|
||||||
after `main` was found to have silently rotted out of sync with `dev`/`beta`
|
|
||||||
across most repos in this ecosystem — a manual "merge beta into main
|
|
||||||
monthly" step nobody reliably did across a dozen-plus repos. Collapsing to
|
|
||||||
one branch removes the class of bug; there's nothing left that can fall out
|
|
||||||
of sync.
|
|
||||||
|
|
||||||
## Remotes
|
## Remotes
|
||||||
- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative.
|
- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
This repo hosts `bakery` (the ecosystem package manager) and `bread-theme`
|
This repo is a Cargo workspace. Bakery-channel products shipped from here
|
||||||
(the shared theming crate). Other ecosystem products (`bread`, `breadbar`,
|
are `bakery` (the ecosystem package manager) and `bread-theme` (the shared
|
||||||
`breadbox`, …) live in their own repos under `Breadway/` but follow the same
|
theming crate). Shared crates that sibling apps pin — not bakery packages
|
||||||
workflow described here.
|
of their own — are `bread-utils`, `bread-onnx`, `bread-screenshots`, and
|
||||||
|
`bread-capture`. Other ecosystem products (`bread`, `breadbar`, `breadbox`,
|
||||||
|
…) live in their own repos under `Breadway/` but follow the same workflow
|
||||||
|
described here. The product list is `registry/bread-ecosystem.toml`.
|
||||||
|
|
||||||
## Branches
|
## Branches
|
||||||
|
|
||||||
|
|
@ -76,8 +79,10 @@ cargo build --release -p bakery
|
||||||
cargo test --release -p bakery
|
cargo test --release -p bakery
|
||||||
```
|
```
|
||||||
|
|
||||||
Both `bakery` and `bread-theme` are members of this workspace's Cargo.toml.
|
`bakery`, `bread-theme`, `bread-utils`, `bread-onnx`, `bread-screenshots`,
|
||||||
Run the same commands with `-p bread-theme --bin bread-theme` for that crate.
|
and `bread-capture` are all workspace members. Run the same commands with
|
||||||
|
`-p bread-theme --bin bread-theme` for that crate, or `-p bread-utils
|
||||||
|
--features bread-client` for the IPC client.
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
|
|
|
||||||
53
README.md
53
README.md
|
|
@ -4,20 +4,36 @@ A collection of Rust tools for the Linux desktop (Hyprland / Wayland / Arch).
|
||||||
Install any product with a single command — no Rust toolchain required.
|
Install any product with a single command — no Rust toolchain required.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl https://breadway.dev/get | sh
|
curl -fsSL https://get.breadway.dev | sh
|
||||||
bakery install breadbar
|
bakery install breadbar
|
||||||
```
|
```
|
||||||
|
|
||||||
## Products
|
## Products
|
||||||
|
|
||||||
|
The table below is generated from [`registry/bread-ecosystem.toml`](registry/bread-ecosystem.toml). Regenerate with `scripts/gen-readme-products.sh`.
|
||||||
|
|
||||||
|
<!-- gen-readme-products:start -->
|
||||||
|
|
||||||
| Package | Description |
|
| Package | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `bread` | Reactive automation daemon (`breadd`) + CLI — Lua scripting over Hyprland, udev, power, network, and Bluetooth events |
|
| `bakery` | Bread ecosystem package manager |
|
||||||
| `breadbar` | GTK4 status bar (workspaces, clock, CPU/RAM/battery/WiFi/Bluetooth) and D-Bus notification daemon for Hyprland |
|
| `bread-theme` | Shared pywal-accented, fixed-dark-base theming CLI for the bread ecosystem |
|
||||||
| `breadbox` | GTK4 fuzzy app launcher for Hyprland with context-aware sorting; ships an icon-sync daemon (`breadbox-sync`) |
|
| `bread` | Reactive automation daemon and CLI for Linux desktops |
|
||||||
| `breadcrumbs` | Profile-aware Wi-Fi state machine with Tailscale exit-node management and a self-healing watch daemon |
|
| `breadbar` | Minimal status bar and notification daemon for Hyprland |
|
||||||
| `breadpad` | Quick-capture scratchpad popup with AI-powered note classification, reminders, recurrence, and a full note viewer (`breadman`) |
|
| `breadbox` | App launcher for Hyprland / Wayland |
|
||||||
|
| `breadcrumbs` | Profile-aware Wi-Fi state machine with Tailscale integration |
|
||||||
|
| `breadpad` | Quick-capture scratchpad and note viewer with AI classification |
|
||||||
| `breadpaper` | Wallpaper manager for the bread desktop |
|
| `breadpaper` | Wallpaper manager for the bread desktop |
|
||||||
|
| `breadmon` | Terminal UI monitor manager for Hyprland |
|
||||||
|
| `breadsearch` | Semantic system-wide search for BOS |
|
||||||
|
| `breadclip` | Wayland clipboard history manager for Hyprland |
|
||||||
|
| `breadshot` | Screenshot utility for the bread ecosystem |
|
||||||
|
| `bos-settings` | System settings app for Bread OS |
|
||||||
|
| `breadhelp` | Onboarding and help center for Bread OS |
|
||||||
|
| `breadcast` | Cast your screen to any Chromecast/Google TV or DLNA renderer — daemon + GTK4 popup — Bakery product; not included in the BOS ISO |
|
||||||
|
| `breadarr` | Single-daemon Sonarr+Radarr+Prowlarr replacement — release watching, matching, grabbing, importing, and a terminal UI, no web UI — Homelab, not shipped on BOS |
|
||||||
|
|
||||||
|
<!-- gen-readme-products:end -->
|
||||||
|
|
||||||
## Recommended keybinds
|
## Recommended keybinds
|
||||||
|
|
||||||
|
|
@ -68,9 +84,7 @@ spacing, radii, colour roles) the stylesheet is built from.
|
||||||
`bakery` is the package manager for the ecosystem. Install it with the bootstrap script:
|
`bakery` is the package manager for the ecosystem. Install it with the bootstrap script:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl https://breadway.dev/get | sh
|
curl -fsSL https://get.breadway.dev | sh
|
||||||
# or
|
|
||||||
curl -sSfL https://get.breadway.dev | sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The script downloads the prebuilt `bakery` binary to `~/.local/bin/bakery` and prints a note if that directory isn't on your `PATH` yet.
|
The script downloads the prebuilt `bakery` binary to `~/.local/bin/bakery` and prints a note if that directory isn't on your `PATH` yet.
|
||||||
|
|
@ -109,16 +123,23 @@ Install all required deps with `sudo pacman -S <packages>`. Use `pacman -Q <pkg>
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
|
|
||||||
This repo is a Cargo workspace:
|
This repo is a Cargo workspace. Bakery-channel products shipped from here
|
||||||
|
are `bakery` and `bread-theme`; the other members are shared crates sibling
|
||||||
|
apps pin, not bakery packages of their own.
|
||||||
|
|
||||||
```
|
```
|
||||||
bread-ecosystem/
|
bread-ecosystem/
|
||||||
├── bakery/ # package manager binary
|
├── bakery/ # package manager binary
|
||||||
├── bread-theme/ # shared pywal + fixed-dark-base theming crate
|
├── bread-theme/ # shared pywal + fixed-dark-base theming crate
|
||||||
├── registry/ # bread-ecosystem.toml — product registry
|
├── bread-utils/ # shared plumbing (Hyprland IPC, singleton, XDG, BreadClient, …)
|
||||||
|
├── bread-onnx/ # shared ONNX runtime helpers
|
||||||
|
├── bread-screenshots/ # grim capture primitive used by app `--screenshot` modes
|
||||||
|
├── bread-capture/ # orchestrator that drives those `--screenshot` modes
|
||||||
|
├── registry/ # bread-ecosystem.toml — product registry
|
||||||
└── scripts/
|
└── scripts/
|
||||||
├── get.sh # curl | sh bootstrap
|
├── get.sh # curl | sh bootstrap
|
||||||
└── gen-index.sh # generates dl.breadway.dev/index.json from release artifacts
|
├── gen-index.sh # generates dl.breadway.dev/index.json from release artifacts
|
||||||
|
└── gen-readme-products.sh # rewrites the Products table from the registry
|
||||||
```
|
```
|
||||||
|
|
||||||
## Release pipeline
|
## Release pipeline
|
||||||
|
|
@ -133,7 +154,7 @@ and mirrors the binary to GitHub Releases as a fallback.
|
||||||
to the GitHub Release URL recorded in the manifest.
|
to the GitHub Release URL recorded in the manifest.
|
||||||
|
|
||||||
Beyond stable releases, most products also publish **dev** and **beta**
|
Beyond stable releases, most products also publish **dev** and **beta**
|
||||||
tracks — continuous builds off the `dev` and `beta` branches, respectively.
|
tracks — continuous builds off `main` (dev) and `vX.Y.Z-rc.N` tags (beta).
|
||||||
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the branch/release workflow and
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the branch/release workflow and
|
||||||
[`docs/release-channels.md`](docs/release-channels.md) for the full track
|
[`docs/release-channels.md`](docs/release-channels.md) for the full track
|
||||||
policy. Switch tracks with `bakery track set <stable|beta|dev>`.
|
policy. Switch tracks with `bakery track set <stable|beta|dev>`.
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,15 @@
|
||||||
//!
|
//!
|
||||||
//! A sibling app must never crash or block because breadd is down,
|
//! A sibling app must never crash or block because breadd is down,
|
||||||
//! restarting, or was never installed. Concretely:
|
//! restarting, or was never installed. Concretely:
|
||||||
//! - [`BreadClient::emit`] is a best-effort, fire-and-forget single-shot
|
//! - [`BreadClient::emit`] and [`BreadClient::command`] are best-effort,
|
||||||
//! connection (mirroring `bread-emit`'s own stance) — if breadd is
|
//! fire-and-forget single-shot connections (mirroring `bread-emit`'s
|
||||||
//! unreachable, the event is silently dropped, not an error the caller
|
//! own stance) — if breadd is unreachable, the event is silently
|
||||||
//! has to handle.
|
//! dropped, not an error the caller has to handle.
|
||||||
|
//! - [`BreadClient::health`] / [`BreadClient::api_version`] return `None`
|
||||||
|
//! when breadd is unreachable or the response is missing fields.
|
||||||
|
//! Long-running daemons SHOULD log a warning in that case and MUST NOT
|
||||||
|
//! crash. [`BreadClient::connect`] never fails just because breadd is
|
||||||
|
//! down — do not change that.
|
||||||
//! - [`BreadClient::subscribe`] runs its read loop on a background thread
|
//! - [`BreadClient::subscribe`] runs its read loop on a background thread
|
||||||
//! that reconnects with exponential backoff on any disconnect. The
|
//! that reconnects with exponential backoff on any disconnect. The
|
||||||
//! caller's callback simply stops being invoked while disconnected; it
|
//! caller's callback simply stops being invoked while disconnected; it
|
||||||
|
|
@ -29,6 +34,15 @@
|
||||||
//! outside the app's own `bread.<app_id>.*` segment, so a misconfigured
|
//! outside the app's own `bread.<app_id>.*` segment, so a misconfigured
|
||||||
//! caller fails fast instead of discovering the mistake from the daemon's
|
//! caller fails fast instead of discovering the mistake from the daemon's
|
||||||
//! rejection. The daemon enforces the same rule server-side regardless.
|
//! rejection. The daemon enforces the same rule server-side regardless.
|
||||||
|
//!
|
||||||
|
//! `command` is the outbound half of the same story: it publishes
|
||||||
|
//! `bread.command.<target_app>.<verb>` as an **unsourced** IPC emit
|
||||||
|
//! (`params` is `{ event, data }` only — no `source`/`kind`). The
|
||||||
|
//! daemon treats unsourced `bread.command.<known_app>.*` as legal so a
|
||||||
|
//! sibling can address another app without impersonating that app's
|
||||||
|
//! own namespace. Local refusal (eprint + return, same stance as
|
||||||
|
//! `emit`) if `target_app` or `verb` is empty, or if `verb` contains
|
||||||
|
//! `.` (a command verb is a single segment).
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::net::Shutdown;
|
use std::net::Shutdown;
|
||||||
|
|
@ -94,25 +108,60 @@ impl BreadClient {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let request = json!({
|
fire_and_forget_emit(json!({
|
||||||
"id": "0",
|
"event": event,
|
||||||
"method": "emit",
|
"source": self.app_id,
|
||||||
"params": {
|
"kind": event,
|
||||||
"event": event,
|
"data": data,
|
||||||
"source": self.app_id,
|
}));
|
||||||
"kind": event,
|
}
|
||||||
"data": data,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let Ok(line) = serde_json::to_string(&request) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(mut stream) = UnixStream::connect(bread_shared::resolve_socket_path()) else {
|
/// Publish `bread.command.<target_app>.<verb>` as an unsourced IPC
|
||||||
|
/// emit so another bread app (or a Lua workflow) can act on it.
|
||||||
|
///
|
||||||
|
/// Fire-and-forget: same silent-if-down stance as [`emit`]. Locally
|
||||||
|
/// refuses (eprint + return, no socket) if `target_app` or `verb` is
|
||||||
|
/// empty, or if `verb` contains `.` — a verb is one segment
|
||||||
|
/// (`clear`, not `history.clear`).
|
||||||
|
///
|
||||||
|
/// The wire payload is `{ method: "emit", params: { event, data } }`
|
||||||
|
/// with **no** `source`/`kind`. Do not add those: a sourced emit
|
||||||
|
/// would have to claim the *target's* namespace (or ours), and the
|
||||||
|
/// daemon half of this integration is specifically making unsourced
|
||||||
|
/// `bread.command.<known_app>.*` legal.
|
||||||
|
pub fn command(&self, target_app: &str, verb: &str, data: Value) {
|
||||||
|
if target_app.is_empty() || verb.is_empty() || verb.contains('.') {
|
||||||
|
eprintln!(
|
||||||
|
"bread-client: refusing to send command to '{target_app}' with verb '{verb}' \
|
||||||
|
(target and verb must be non-empty; verb must be a single segment)"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
let _ = stream.set_write_timeout(Some(Duration::from_millis(200)));
|
|
||||||
let _ = writeln!(stream, "{line}");
|
let event = format!("bread.command.{target_app}.{verb}");
|
||||||
|
fire_and_forget_emit(json!({
|
||||||
|
"event": event,
|
||||||
|
"data": data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-shot `health` IPC request. `None` if breadd is unreachable or
|
||||||
|
/// the response is malformed / an error.
|
||||||
|
///
|
||||||
|
/// Long-running daemons SHOULD log a warning when this returns
|
||||||
|
/// `None` (or when [`api_version`] is missing) and MUST NOT crash.
|
||||||
|
pub fn health(&self) -> Option<Value> {
|
||||||
|
self.request("health", json!({}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `api_version` string from [`health`], or `None` if health failed
|
||||||
|
/// or the field is absent. Same SHOULD-warn / MUST-NOT-crash rule
|
||||||
|
/// as [`health`].
|
||||||
|
pub fn api_version(&self) -> Option<String> {
|
||||||
|
self.health()?
|
||||||
|
.get("api_version")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(str::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a one-shot IPC request and return its `result`, or `None` on any
|
/// Send a one-shot IPC request and return its `result`, or `None` on any
|
||||||
|
|
@ -205,6 +254,27 @@ impl BreadClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fire-and-forget a single `emit` request. Shared by sourced [`BreadClient::emit`]
|
||||||
|
/// and unsourced [`BreadClient::command`] so the write/timeout path cannot
|
||||||
|
/// drift. Silent if the socket is missing, the write fails, or the body
|
||||||
|
/// cannot be serialized.
|
||||||
|
fn fire_and_forget_emit(params: Value) {
|
||||||
|
let request = json!({
|
||||||
|
"id": "0",
|
||||||
|
"method": "emit",
|
||||||
|
"params": params,
|
||||||
|
});
|
||||||
|
let Ok(line) = serde_json::to_string(&request) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(mut stream) = UnixStream::connect(bread_shared::resolve_socket_path()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = stream.set_write_timeout(Some(Duration::from_millis(200)));
|
||||||
|
let _ = writeln!(stream, "{line}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Connects once, sends `events.subscribe`, and invokes `on_event` for every
|
/// Connects once, sends `events.subscribe`, and invokes `on_event` for every
|
||||||
/// matching line until the connection ends (cleanly or with an error).
|
/// matching line until the connection ends (cleanly or with an error).
|
||||||
/// Stores the live stream in `current_stream` so [`Subscription::stop`] can
|
/// Stores the live stream in `current_stream` so [`Subscription::stop`] can
|
||||||
|
|
@ -334,12 +404,81 @@ mod tests {
|
||||||
// integration tests for the IPC-side of namespace validation.
|
// integration tests for the IPC-side of namespace validation.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Point `HOME` + `XDG_RUNTIME_DIR` at an empty temp dir so
|
||||||
|
/// `resolve_socket_path` cannot find a live breadd (either via
|
||||||
|
/// `~/.config/bread/breadd.toml` or `$XDG_RUNTIME_DIR/bread/breadd.sock`).
|
||||||
|
/// Serialized with the other env-mutating tests via `env_test_lock`.
|
||||||
|
fn with_unreachable_daemon<T>(f: impl FnOnce() -> T) -> T {
|
||||||
|
let _lock = crate::env_test_lock()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let old_home = std::env::var("HOME").ok();
|
||||||
|
let old_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("HOME", tmp.path());
|
||||||
|
std::env::set_var("XDG_RUNTIME_DIR", tmp.path());
|
||||||
|
}
|
||||||
|
struct Restore(Option<String>, Option<String>);
|
||||||
|
impl Drop for Restore {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
match &self.0 {
|
||||||
|
Some(v) => std::env::set_var("HOME", v),
|
||||||
|
None => std::env::remove_var("HOME"),
|
||||||
|
}
|
||||||
|
match &self.1 {
|
||||||
|
Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v),
|
||||||
|
None => std::env::remove_var("XDG_RUNTIME_DIR"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _restore = Restore(old_home, old_xdg);
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_returns_none_when_daemon_is_unreachable() {
|
fn request_returns_none_when_daemon_is_unreachable() {
|
||||||
// No daemon present in the test environment; must return None
|
with_unreachable_daemon(|| {
|
||||||
// promptly rather than blocking or panicking.
|
let client = BreadClient::connect("clip");
|
||||||
|
assert!(client.request("widgets.list", json!(null)).is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_refuses_empty_target_without_connecting() {
|
||||||
let client = BreadClient::connect("clip");
|
let client = BreadClient::connect("clip");
|
||||||
assert!(client.request("widgets.list", json!(null)).is_none());
|
client.command("", "clear", json!({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_refuses_empty_verb_without_connecting() {
|
||||||
|
let client = BreadClient::connect("clip");
|
||||||
|
client.command("clip", "", json!({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_refuses_dotted_verb_without_connecting() {
|
||||||
|
// A verb is a single segment — `history.clear` would produce
|
||||||
|
// `bread.command.clip.history.clear`, which is two verb segments.
|
||||||
|
let client = BreadClient::connect("clip");
|
||||||
|
client.command("clip", "history.clear", json!({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_is_a_silent_no_op_when_daemon_is_unreachable() {
|
||||||
|
let client = BreadClient::connect("clip");
|
||||||
|
client.command("clip", "clear", json!({ "n": 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn health_and_api_version_return_none_when_daemon_is_unreachable() {
|
||||||
|
with_unreachable_daemon(|| {
|
||||||
|
let client = BreadClient::connect("clip");
|
||||||
|
assert!(client.health().is_none());
|
||||||
|
assert!(client.api_version().is_none());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,18 @@
|
||||||
//! - [`gtk_popup`] (feature `gtk`) — shared layer-shell popup window setup,
|
//! - [`gtk_popup`] (feature `gtk`) — shared layer-shell popup window setup,
|
||||||
//! list navigation, and click-outside-to-close.
|
//! list navigation, and click-outside-to-close.
|
||||||
//! - [`bread_client`] (feature `bread-client`) — a persistent-connection
|
//! - [`bread_client`] (feature `bread-client`) — a persistent-connection
|
||||||
//! client for breadd's IPC socket (emit + subscribe), for sibling
|
//! client for breadd's IPC socket (`emit`, unsourced `command`,
|
||||||
//! `bread*` app daemons integrating with the bread automation fabric.
|
//! `health`/`api_version`, `subscribe`), for sibling `bread*` app
|
||||||
|
//! daemons integrating with the bread automation fabric.
|
||||||
|
//! - [`screenshot_cli`] — shared `--screenshot` / `--output` /
|
||||||
|
//! `--width` / `--height` values, `SETTLE_DELAY` (300ms), and the
|
||||||
|
//! "both flags or neither" validator. Next-pin helper; no clap/GTK
|
||||||
|
//! dependency. Does not replace `bread-screenshots` or `bread-capture`.
|
||||||
|
|
||||||
pub mod atomic;
|
pub mod atomic;
|
||||||
pub mod hypr;
|
pub mod hypr;
|
||||||
pub mod proc;
|
pub mod proc;
|
||||||
|
pub mod screenshot_cli;
|
||||||
pub mod singleton;
|
pub mod singleton;
|
||||||
pub mod xdg;
|
pub mod xdg;
|
||||||
|
|
||||||
|
|
|
||||||
156
bread-utils/src/screenshot_cli.rs
Normal file
156
bread-utils/src/screenshot_cli.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
//! Shared `--screenshot` CLI flags and the post-`map` settle delay used by
|
||||||
|
//! bread-capture-driven GTK apps.
|
||||||
|
//!
|
||||||
|
//! The same clap block (`--screenshot`, `--output`, `--width`, `--height`)
|
||||||
|
//! plus a 300ms settle after GTK `map` is cloned across breadbar, breadbox,
|
||||||
|
//! breadclip, breadpad, breadman, breadsearch, and breadhelp. This module
|
||||||
|
//! is the next-pin target for that duplication — consumers this cycle still
|
||||||
|
//! pin an older bread-utils tag and will not see it until a future release.
|
||||||
|
//!
|
||||||
|
//! Zero extra deps: no clap, no GTK. Apps keep (or flatten) the four `#[arg]`
|
||||||
|
//! fields themselves and call [`validate_pair`] / [`SETTLE_DELAY`].
|
||||||
|
//!
|
||||||
|
//! Confirmed present in:
|
||||||
|
//! - `breadbar/src/screenshot.rs`
|
||||||
|
//! - `breadbox/breadbox/src/screenshot.rs`
|
||||||
|
//! - `breadclip/breadclip/src/screenshot.rs`
|
||||||
|
//! - `breadsearch/breadsearch/src/screenshot.rs`
|
||||||
|
//! - `breadpad/breadpad/src/screenshot.rs`
|
||||||
|
//! - `breadpad/breadman/src/screenshot.rs`
|
||||||
|
//! - `breadhelp/src/screenshot.rs`
|
||||||
|
//!
|
||||||
|
//! Do not fold `bread-screenshots` or `bread-capture`'s `TARGETS` table into
|
||||||
|
//! this module — those are the capture primitive and the orchestrator, not
|
||||||
|
//! the per-app CLI flags.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// Extra settle time after GTK `map` for the first frame to actually paint
|
||||||
|
/// before grim runs. `map` fires once the surface exists, not once anything
|
||||||
|
/// has been drawn into it.
|
||||||
|
pub const SETTLE_DELAY: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
/// Default `--width`, matching `bread-capture --isolate-width`.
|
||||||
|
pub const DEFAULT_WIDTH: u32 = 1920;
|
||||||
|
|
||||||
|
/// Default `--height`, matching `bread-capture --isolate-height`.
|
||||||
|
pub const DEFAULT_HEIGHT: u32 = 1080;
|
||||||
|
|
||||||
|
/// The four `--screenshot` / `--output` / `--width` / `--height` values
|
||||||
|
/// parsed from an app's CLI.
|
||||||
|
///
|
||||||
|
/// `screenshot` and `output` must both be present (a capture run) or both
|
||||||
|
/// absent (a normal run) — see [`ScreenshotCli::validate`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ScreenshotCli {
|
||||||
|
/// Named view to capture (`--screenshot`). `None` for a normal run.
|
||||||
|
pub screenshot: Option<String>,
|
||||||
|
/// PNG path to write (`--output`). Required together with `screenshot`.
|
||||||
|
pub output: Option<PathBuf>,
|
||||||
|
/// Capture canvas width (`--width`).
|
||||||
|
pub width: u32,
|
||||||
|
/// Capture canvas height (`--height`).
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ScreenshotCli {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
screenshot: None,
|
||||||
|
output: None,
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why a screenshot-flag pair is invalid.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ScreenshotCliError {
|
||||||
|
/// `--screenshot` was given without `--output`.
|
||||||
|
ScreenshotWithoutOutput,
|
||||||
|
/// `--output` was given without `--screenshot`.
|
||||||
|
OutputWithoutScreenshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ScreenshotCliError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ScreenshotWithoutOutput => write!(f, "--screenshot requires --output"),
|
||||||
|
Self::OutputWithoutScreenshot => write!(f, "--output requires --screenshot"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ScreenshotCliError {}
|
||||||
|
|
||||||
|
/// Both `--screenshot` and `--output` must be present, or neither.
|
||||||
|
pub fn validate_pair(
|
||||||
|
screenshot: Option<&str>,
|
||||||
|
output: Option<&Path>,
|
||||||
|
) -> Result<(), ScreenshotCliError> {
|
||||||
|
match (screenshot, output) {
|
||||||
|
(Some(_), Some(_)) | (None, None) => Ok(()),
|
||||||
|
(Some(_), None) => Err(ScreenshotCliError::ScreenshotWithoutOutput),
|
||||||
|
(None, Some(_)) => Err(ScreenshotCliError::OutputWithoutScreenshot),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenshotCli {
|
||||||
|
/// Both `screenshot` and `output` present, or neither.
|
||||||
|
pub fn validate(&self) -> Result<(), ScreenshotCliError> {
|
||||||
|
validate_pair(self.screenshot.as_deref(), self.output.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` when this is a capture run (both flags present).
|
||||||
|
pub fn is_screenshot_run(&self) -> bool {
|
||||||
|
self.screenshot.is_some() && self.output.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settle_delay_is_300ms() {
|
||||||
|
assert_eq!(SETTLE_DELAY, Duration::from_millis(300));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn neither_flag_is_ok() {
|
||||||
|
assert!(validate_pair(None, None).is_ok());
|
||||||
|
assert!(ScreenshotCli::default().validate().is_ok());
|
||||||
|
assert!(!ScreenshotCli::default().is_screenshot_run());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn both_flags_are_ok() {
|
||||||
|
let cli = ScreenshotCli {
|
||||||
|
screenshot: Some("search".into()),
|
||||||
|
output: Some(PathBuf::from("/tmp/out.png")),
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
};
|
||||||
|
assert!(cli.validate().is_ok());
|
||||||
|
assert!(cli.is_screenshot_run());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn screenshot_without_output_is_an_error() {
|
||||||
|
assert_eq!(
|
||||||
|
validate_pair(Some("search"), None),
|
||||||
|
Err(ScreenshotCliError::ScreenshotWithoutOutput)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_without_screenshot_is_an_error() {
|
||||||
|
let path = PathBuf::from("/tmp/out.png");
|
||||||
|
assert_eq!(
|
||||||
|
validate_pair(None, Some(path.as_path())),
|
||||||
|
Err(ScreenshotCliError::OutputWithoutScreenshot)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,13 +38,16 @@ only if:
|
||||||
`archlinux:latest` container and `curl -X PUT`s the resulting
|
`archlinux:latest` container and `curl -X PUT`s the resulting
|
||||||
`.pkg.tar.zst` to `https://git.breadway.dev/api/packages/Breadway/arch/os`.
|
`.pkg.tar.zst` to `https://git.breadway.dev/api/packages/Breadway/arch/os`.
|
||||||
|
|
||||||
A repo can be on **both** channels (most GUI/daemon apps are — see
|
A repo can be on **both** channels, **bakery only** (bread, breadbar,
|
||||||
breadbar, breadbox, breadcrumbs, bread, breadpad, breadpaper), **bakery
|
breadbox, breadcrumbs, breadpad, breadpaper, breadclip, breadmon,
|
||||||
only** (breadclip, breadmon, breadsearch, breadshot, bread-theme, bakery
|
breadsearch, breadshot, breadhelp, bos-settings, bread-theme, breadcast,
|
||||||
itself), **pacman only** (breadlock, breadhelp — both are OS-integration
|
breadarr, bakery itself), **pacman only** (breadlock — installs a
|
||||||
pieces where package-manager rigor matters more than a curl-script), or
|
root-owned `/etc/pam.d/breadlock` PAM service file with no per-user
|
||||||
**neither** (dev-only / not yet released; no bakery.toml, no PKGBUILD, no
|
equivalent, so it can never move to bakery), or **neither** (dev-only /
|
||||||
release or package workflow — just the repo itself, e.g. breadarr today).
|
not yet released). Desktop apps dropped pacman packaging; bakery-channel
|
||||||
|
install is the supported path. `bakery` still carries a leftover
|
||||||
|
`package.yml` / `packaging/arch/PKGBUILD` from when it was also published
|
||||||
|
to the `[breadway]` pacman repo.
|
||||||
|
|
||||||
`bos` is a fourth, deliberately special case: it ships as an ISO, not a
|
`bos` is a fourth, deliberately special case: it ships as an ISO, not a
|
||||||
binary, via its own `release-iso.yml`. It is never on either channel and
|
binary, via its own `release-iso.yml`. It is never on either channel and
|
||||||
|
|
@ -159,14 +162,15 @@ missing it; that gap is intentional and about to be moot everywhere.
|
||||||
|
|
||||||
| Repo | bakery | pacman | tracks | notes |
|
| Repo | bakery | pacman | tracks | notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| bread-ecosystem (bakery product) | yes | yes | stable, beta, dev | single-trunk model; `release-bakery.yml` recovered from a dead `.github/workflows/release.yml` that referenced a `hestia` self-hosted runner GitHub never had registered |
|
| bread-ecosystem (bakery product) | yes | leftover `package.yml` | stable, beta, dev | bakery-channel (`curl -fsSL https://get.breadway.dev \| sh`) is the supported install; `package.yml` + `packaging/arch/PKGBUILD` remain from when bakery was also published to `[breadway]` |
|
||||||
| bread-ecosystem (bread-theme product) | yes | no | stable, beta, dev | single-trunk model |
|
| bread-ecosystem (bread-theme product) | yes | no | stable, beta, dev | single-trunk model |
|
||||||
| bread, breadbar, breadbox, breadcrumbs, breadpad, breadpaper | yes | no | stable, beta, dev | pacman packaging (PKGBUILD + `package.yml`) dropped — bakery-only, single-trunk model |
|
| bread, breadbar, breadbox, breadcrumbs, breadpad, breadpaper | yes | no | stable, beta, dev | pacman packaging (PKGBUILD + `package.yml`) dropped — bakery-only, single-trunk model |
|
||||||
| breadclip, breadmon, breadsearch, breadshot | yes | no | stable, beta, dev | single-trunk model |
|
| breadclip, breadmon, breadsearch, breadshot | yes | no | stable, beta, dev | single-trunk model |
|
||||||
| breadhelp, bos-settings | yes | no | stable, beta, dev | both moved onto bakery this cycle (previously pacman-only or partially wired); single-trunk model |
|
| breadhelp, bos-settings | yes | no | stable, beta, dev | bakery-channel desktop/settings apps; not pacman |
|
||||||
| breadlock | no | yes | n/a | deliberate, permanent exception — installs a root-owned `/etc/pam.d/breadlock` PAM service file with no per-user equivalent, so it can never move to bakery |
|
| breadlock | no | yes | n/a | deliberate, permanent exception — installs a root-owned `/etc/pam.d/breadlock` PAM service file with no per-user equivalent, so it can never move to bakery |
|
||||||
| bos | no | no | n/a | ISO-only via `release-iso.yml`; ships via a manual local build (`build-local.sh`), not a CI track — see its own branch note below |
|
| bos | no | no | n/a | ISO-only via `release-iso.yml`; ships via a manual local build (`build-local.sh`), not a CI track — see its own branch note below |
|
||||||
| breadarr | no | no | n/a | had an orphaned `bakery.toml` with no registry entry and zero workflows; removed. Not yet assigned a channel — do that deliberately when it's ready to ship, don't infer it from a stray config file |
|
| breadcast | yes | no | stable, beta, dev | bakery product; not included in the BOS ISO |
|
||||||
|
| breadarr | yes | no | stable, beta, dev | bakery product; homelab, not shipped on BOS |
|
||||||
|
|
||||||
`bos` doesn't follow the tracks table above (it has no `dev`/`beta`/`stable`
|
`bos` doesn't follow the tracks table above (it has no `dev`/`beta`/`stable`
|
||||||
publish cadence — ISO builds are deliberate and manual) but does share the
|
publish cadence — ISO builds are deliberate and manual) but does share the
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,10 @@ description = "Onboarding and help center for Bread OS"
|
||||||
name = "breadcast"
|
name = "breadcast"
|
||||||
repo = "Breadway/breadcast"
|
repo = "Breadway/breadcast"
|
||||||
description = "Cast your screen to any Chromecast/Google TV or DLNA renderer — daemon + GTK4 popup"
|
description = "Cast your screen to any Chromecast/Google TV or DLNA renderer — daemon + GTK4 popup"
|
||||||
|
notes = "Bakery product; not included in the BOS ISO"
|
||||||
|
|
||||||
[[products]]
|
[[products]]
|
||||||
name = "breadarr"
|
name = "breadarr"
|
||||||
repo = "Breadway/breadarr"
|
repo = "Breadway/breadarr"
|
||||||
description = "Single-daemon Sonarr+Radarr+Prowlarr replacement — release watching, matching, grabbing, importing, and a terminal UI, no web UI"
|
description = "Single-daemon Sonarr+Radarr+Prowlarr replacement — release watching, matching, grabbing, importing, and a terminal UI, no web UI"
|
||||||
|
notes = "Homelab, not shipped on BOS"
|
||||||
|
|
|
||||||
77
scripts/gen-readme-products.sh
Executable file
77
scripts/gen-readme-products.sh
Executable file
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rewrite the marked Products table in README.md from
|
||||||
|
# registry/bread-ecosystem.toml (the source of truth).
|
||||||
|
#
|
||||||
|
# Markers (must exist in README.md):
|
||||||
|
# <!-- gen-readme-products:start -->
|
||||||
|
# ...generated markdown...
|
||||||
|
# <!-- gen-readme-products:end -->
|
||||||
|
#
|
||||||
|
# Optional per-product `notes` in the registry is appended to the
|
||||||
|
# description after an em-dash (used for "homelab, not BOS" / "not in ISO").
|
||||||
|
#
|
||||||
|
# Usage: scripts/gen-readme-products.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
REGISTRY="${SCRIPT_DIR}/registry/bread-ecosystem.toml"
|
||||||
|
README="${SCRIPT_DIR}/README.md"
|
||||||
|
START="<!-- gen-readme-products:start -->"
|
||||||
|
END="<!-- gen-readme-products:end -->"
|
||||||
|
|
||||||
|
if [[ ! -f "${REGISTRY}" ]]; then
|
||||||
|
echo "error: registry not found at ${REGISTRY}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -f "${README}" ]]; then
|
||||||
|
echo "error: README not found at ${README}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "${REGISTRY}" "${README}" "${START}" "${END}" <<'PY'
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError: # pragma: no cover — 3.11+ is required
|
||||||
|
import tomli as tomllib # type: ignore
|
||||||
|
|
||||||
|
registry_path, readme_path, start, end = sys.argv[1:]
|
||||||
|
|
||||||
|
with open(registry_path, "rb") as f:
|
||||||
|
registry = tomllib.load(f)
|
||||||
|
|
||||||
|
products = registry.get("products") or []
|
||||||
|
if not products:
|
||||||
|
print("error: registry has no [[products]]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
lines = ["| Package | Description |", "|---------|-------------|"]
|
||||||
|
for product in products:
|
||||||
|
name = product["name"]
|
||||||
|
desc = str(product.get("description") or "").replace("|", "\\|")
|
||||||
|
notes = str(product.get("notes") or "").replace("|", "\\|")
|
||||||
|
if notes:
|
||||||
|
desc = f"{desc} — {notes}"
|
||||||
|
lines.append(f"| `{name}` | {desc} |")
|
||||||
|
table = "\n".join(lines)
|
||||||
|
|
||||||
|
readme = Path(readme_path)
|
||||||
|
text = readme.read_text()
|
||||||
|
start_at = text.find(start)
|
||||||
|
end_at = text.find(end)
|
||||||
|
if start_at < 0 or end_at < 0 or end_at < start_at:
|
||||||
|
print(
|
||||||
|
f"error: README.md is missing markers {start!r} / {end!r}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
rewritten = text[:start_at] + start + "\n\n" + table + "\n\n" + end + text[end_at + len(end):]
|
||||||
|
if rewritten != text:
|
||||||
|
readme.write_text(rewritten)
|
||||||
|
print(f"updated {readme_path} ({len(products)} products)")
|
||||||
|
else:
|
||||||
|
print(f"{readme_path} already matches the registry ({len(products)} products)")
|
||||||
|
PY
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Bootstrap script: downloads and installs the `bakery` binary.
|
# Bootstrap script: downloads and installs the `bakery` binary.
|
||||||
# Usage: curl https://breadway.dev/get | sh
|
# Usage: curl -fsSL https://get.breadway.dev | sh
|
||||||
# Or: curl -sSfL https://breadway.dev/get | sh
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# Pinned minisign public key for the bakery release binary. Matches the
|
# Pinned minisign public key for the bakery release binary. Matches the
|
||||||
|
|
@ -20,6 +19,14 @@ die() { echo "error: $*" >&2; exit 1; }
|
||||||
uname -m | grep -q x86_64 || die "bakery only supports x86_64 (got $(uname -m))"
|
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))"
|
uname -s | grep -q Linux || die "bakery only supports Linux (got $(uname -s))"
|
||||||
|
|
||||||
|
# Signature verification is mandatory. Checksum-only is not sufficient —
|
||||||
|
# the binary and its .sha256 typically come from the same server, so a
|
||||||
|
# compromised host can serve a matching pair. Fail closed if minisign
|
||||||
|
# isn't here rather than downloading something we refuse to trust.
|
||||||
|
if ! command -v minisign >/dev/null 2>&1; then
|
||||||
|
die "minisign is required to verify bakery. Install it: pacman -S minisign / apt install minisign"
|
||||||
|
fi
|
||||||
|
|
||||||
# Build download URLs. GitHub's "latest" redirect lives at a different path from
|
# 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'.
|
# versioned releases, so we handle them separately and always prefix tags with 'v'.
|
||||||
if [ "${BAKERY_VERSION}" = "latest" ]; then
|
if [ "${BAKERY_VERSION}" = "latest" ]; then
|
||||||
|
|
@ -55,42 +62,33 @@ echo "downloading bakery…"
|
||||||
if fetch "${DL_PRIMARY}" "${TMP}" 2>/dev/null; then
|
if fetch "${DL_PRIMARY}" "${TMP}" 2>/dev/null; then
|
||||||
echo " from dl.breadway.dev"
|
echo " from dl.breadway.dev"
|
||||||
sig_url="${SIG_URL}"
|
sig_url="${SIG_URL}"
|
||||||
checksum_only_fallback_note=" warning: could not fetch checksum — skipping verification"
|
sig_url_alt="${SIG_FALLBACK}"
|
||||||
elif fetch "${DL_FALLBACK}" "${TMP}" 2>/dev/null; then
|
elif fetch "${DL_FALLBACK}" "${TMP}" 2>/dev/null; then
|
||||||
echo " from GitHub (fallback)"
|
echo " from GitHub (fallback)"
|
||||||
sig_url="${SIG_FALLBACK}"
|
sig_url="${SIG_FALLBACK}"
|
||||||
checksum_only_fallback_note=" warning: no checksum available for GitHub fallback download"
|
sig_url_alt="${SIG_URL}"
|
||||||
else
|
else
|
||||||
die "failed to download bakery from both primary and fallback URLs"
|
die "failed to download bakery from both primary and fallback URLs"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Signature verification is the authoritative check: it proves the binary
|
# Signature is required. A missing .minisig is a refuse-to-install, not a
|
||||||
# was produced by whoever holds the bakery signing key, not just that bytes
|
# warning — checksum-only is not a substitute.
|
||||||
# match whatever the same (possibly compromised) server also reports as the
|
|
||||||
# checksum. Prefer it whenever both a .minisig is published and a minisign
|
|
||||||
# verifier is available on this machine.
|
|
||||||
sig_verified=0
|
|
||||||
if fetch "${sig_url}" "${TMP}.minisig" 2>/dev/null; then
|
if fetch "${sig_url}" "${TMP}.minisig" 2>/dev/null; then
|
||||||
if command -v minisign >/dev/null 2>&1; then
|
:
|
||||||
if minisign -V -q -m "${TMP}" -x "${TMP}.minisig" -P "${BAKERY_MINISIGN_PUBKEY}"; then
|
elif [ "${sig_url_alt}" != "${sig_url}" ] && fetch "${sig_url_alt}" "${TMP}.minisig" 2>/dev/null; then
|
||||||
echo " signature verified (minisign)"
|
echo " signature fetched from fallback URL"
|
||||||
sig_verified=1
|
|
||||||
else
|
|
||||||
die "minisign signature verification FAILED — refusing to install a binary that doesn't match the pinned bakery key"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " warning: 'minisign' is not installed — cannot verify the binary's" >&2
|
|
||||||
echo " warning: signature, only its checksum. Install minisign for the" >&2
|
|
||||||
echo " warning: strongest guarantee: pacman -S minisign / apt install minisign" >&2
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo " warning: no .minisig published for this release yet — signature not verified" >&2
|
die "could not fetch bakery-x86_64.minisig — refusing to install an unsigned binary"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Checksum is a secondary, best-effort check (kept for defense in depth and
|
if minisign -V -q -m "${TMP}" -x "${TMP}.minisig" -P "${BAKERY_MINISIGN_PUBKEY}"; then
|
||||||
# for the case where minisign isn't installed). It is not a substitute for
|
echo " signature verified (minisign)"
|
||||||
# signature verification: both the binary and its checksum typically come
|
else
|
||||||
# from the same server, so a compromised server can serve a matching pair.
|
die "minisign signature verification FAILED — refusing to install a binary that doesn't match the pinned bakery key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Checksum is defense-in-depth only, and never enough on its own. A
|
||||||
|
# mismatch still dies; a missing .sha256 is fine once the signature passed.
|
||||||
if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
||||||
expected="$(awk '{print $1}' "${TMP}.sha256")"
|
expected="$(awk '{print $1}' "${TMP}.sha256")"
|
||||||
actual="$(sha256sum "${TMP}" | awk '{print $1}')"
|
actual="$(sha256sum "${TMP}" | awk '{print $1}')"
|
||||||
|
|
@ -98,12 +96,6 @@ if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
||||||
die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})"
|
die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})"
|
||||||
fi
|
fi
|
||||||
echo " checksum verified"
|
echo " checksum verified"
|
||||||
else
|
|
||||||
echo "${checksum_only_fallback_note}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${sig_verified}" -ne 1 ]; then
|
|
||||||
echo " warning: proceeding WITHOUT a verified signature on the bakery binary" >&2
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chmod +x "${TMP}"
|
chmod +x "${TMP}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue