diff --git a/.forgejo/workflows/rc-bakery.yml b/.forgejo/workflows/beta-bakery.yml similarity index 71% rename from .forgejo/workflows/rc-bakery.yml rename to .forgejo/workflows/beta-bakery.yml index d4e7853..05c1774 100644 --- a/.forgejo/workflows/rc-bakery.yml +++ b/.forgejo/workflows/beta-bakery.yml @@ -1,19 +1,14 @@ -name: beta (rc) bakery +name: beta bakery -# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag pushed -# to `main` — there is no separate `beta` branch; "freezing" is just -# pausing pushes to main while an RC gets tested. See -# docs/release-channels.md for the release-track policy. +# Publishes a beta-track build when a `beta-v*` tag is pushed — a deliberate +# promotion step (you pick the version string and the commit), distinct from +# dev-bakery.yml's automatic build-on-every-push. See docs/release-channels.md. on: push: - tags: ['v*'] - # No paths: filter. Tag pushes compare against an unrelated commit and - # would skip the RC publish if bakery/** wasn't in that diff; the job - # `if: contains -rc.` is the real gate. + tags: ['beta-v*'] jobs: build: - if: ${{ contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -32,8 +27,7 @@ jobs: - name: prepare artifacts run: | set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + VERSION="${GITHUB_REF_NAME#beta-v}" PKG_DIR="/srv/breadway-dl/beta/bakery/${VERSION}" mkdir -p "${PKG_DIR}" cp "src/target/release/bakery" "${PKG_DIR}/bakery-x86_64" @@ -48,6 +42,7 @@ jobs: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} run: | set -euo pipefail + VERSION="${GITHUB_REF_NAME#beta-v}" PKG_DIR="/srv/breadway-dl/beta/bakery/${VERSION}" if [ -n "${MINISIGN_SEC_KEY:-}" ]; then minisign -W -S -s "${MINISIGN_SEC_KEY}" -m "${PKG_DIR}/bakery-x86_64" \ @@ -57,9 +52,8 @@ jobs: echo "::warning::BAKERY_MINISIGN_SEC_KEY_PATH not set — shipping bakery-x86_64 UNSIGNED" fi - # No GitHub Release upload step here, unlike release-bakery.yml — beta - # builds happen on every push while the branch is frozen for testing, - # so dl.breadway.dev/beta/ is the only distribution point for this track. + # No GitHub Release upload — beta, like dev, is only distributed via + # dl.breadway.dev/beta/. - name: regenerate beta index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} diff --git a/.forgejo/workflows/rc-bread-theme.yml b/.forgejo/workflows/beta-bread-theme.yml similarity index 76% rename from .forgejo/workflows/rc-bread-theme.yml rename to .forgejo/workflows/beta-bread-theme.yml index bd53ba1..24bde62 100644 --- a/.forgejo/workflows/rc-bread-theme.yml +++ b/.forgejo/workflows/beta-bread-theme.yml @@ -1,19 +1,14 @@ -name: beta (rc) bread-theme +name: beta bread-theme -# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag pushed -# to `main` — there is no separate `beta` branch; "freezing" is just -# pausing pushes to main while an RC gets tested. See -# docs/release-channels.md for the release-track policy. +# Publishes a beta-track build when a `beta-v*` tag is pushed — a deliberate +# promotion step, distinct from dev-bread-theme.yml's build-on-every-push. +# See docs/release-channels.md. on: push: - tags: ['v*'] - # No paths: filter. Tag pushes compare against an unrelated commit and - # would skip the RC publish if bread-theme/** wasn't in that diff; the - # job `if: contains -rc.` is the real gate. + tags: ['beta-v*'] jobs: build: - if: ${{ contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -29,8 +24,7 @@ jobs: - name: prepare artifacts run: | set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - echo "VERSION=${VERSION}" >> "$GITHUB_ENV" + VERSION="${GITHUB_REF_NAME#beta-v}" PKG_DIR="/srv/breadway-dl/beta/bread-theme/${VERSION}" mkdir -p "${PKG_DIR}" cp "src/target/release/bread-theme" "${PKG_DIR}/bread-theme-x86_64" @@ -45,6 +39,7 @@ jobs: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} run: | set -euo pipefail + VERSION="${GITHUB_REF_NAME#beta-v}" PKG_DIR="/srv/breadway-dl/beta/bread-theme/${VERSION}" if [ -n "${MINISIGN_SEC_KEY:-}" ]; then minisign -W -S -s "${MINISIGN_SEC_KEY}" -m "${PKG_DIR}/bread-theme-x86_64" \ diff --git a/.forgejo/workflows/dev-bakery.yml b/.forgejo/workflows/dev-bakery.yml index a7c4bde..ae2e9ef 100644 --- a/.forgejo/workflows/dev-bakery.yml +++ b/.forgejo/workflows/dev-bakery.yml @@ -1,14 +1,13 @@ name: dev bakery -# Publishes a dev-track build on every push to `main` (the trunk branch — -# there is no separate `dev` branch). See docs/release-channels.md for the -# release-track policy this is part of. +# Publishes a dev-track build on every push to `dev` — separate from +# release-bakery.yml's tag-triggered stable releases. See docs/release-channels.md +# for the three-track policy (stable/beta/dev) this is part of. on: push: - branches: ['main'] + branches: ['dev'] paths: - 'bakery/**' - - 'bread-utils/**' - 'Cargo.toml' - 'Cargo.lock' - '.forgejo/workflows/dev-bakery.yml' @@ -21,7 +20,7 @@ jobs: run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch main --depth 1 \ + git clone --branch dev --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build @@ -46,7 +45,7 @@ jobs: # what's already installed and bakery would correctly refuse it. LATEST_TAG="$(git ls-remote --tags --refs \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ - | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" + | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" if [ -n "${LATEST_TAG}" ]; then CUR="${LATEST_TAG}" else diff --git a/.forgejo/workflows/dev-bread-theme.yml b/.forgejo/workflows/dev-bread-theme.yml index 1cff78e..3d645d2 100644 --- a/.forgejo/workflows/dev-bread-theme.yml +++ b/.forgejo/workflows/dev-bread-theme.yml @@ -1,11 +1,11 @@ name: dev bread-theme -# Publishes a dev-track build on every push to `main` (the trunk branch — -# there is no separate `dev` branch). See docs/release-channels.md for the -# release-track policy this is part of. +# Publishes a dev-track build on every push to `dev` — separate from +# release-bread-theme.yml's tag-triggered stable releases. See +# docs/release-channels.md for the three-track policy this is part of. on: push: - branches: ['main'] + branches: ['dev'] paths: - 'bread-theme/**' - 'Cargo.toml' @@ -20,7 +20,7 @@ jobs: run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch main --depth 1 \ + git clone --branch dev --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build @@ -37,7 +37,7 @@ jobs: # what's already installed and bakery would correctly refuse it. LATEST_TAG="$(git ls-remote --tags --refs \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ - | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" + | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" if [ -n "${LATEST_TAG}" ]; then CUR="${LATEST_TAG}" else diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml index ca941f2..6725e22 100644 --- a/.forgejo/workflows/package.yml +++ b/.forgejo/workflows/package.yml @@ -6,9 +6,6 @@ on: jobs: package: - # PKGBUILD pkgver cannot contain `-`; skip RC tags the same way - # release-bakery.yml does. - if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] container: image: archlinux:latest diff --git a/.forgejo/workflows/release-bakery.yml b/.forgejo/workflows/release-bakery.yml index 2ac03bf..283153b 100644 --- a/.forgejo/workflows/release-bakery.yml +++ b/.forgejo/workflows/release-bakery.yml @@ -6,7 +6,6 @@ on: jobs: build: - if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -59,13 +58,7 @@ jobs: - name: regenerate index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} - run: | - set -euo pipefail - if [ -z "${MINISIGN_SEC_KEY:-}" ]; then - echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate stable index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the stable track)" - exit 1 - fi - cd src && bash scripts/gen-index.sh + run: cd src && bash scripts/gen-index.sh - name: upload to GitHub Release env: diff --git a/.forgejo/workflows/release-bread-theme.yml b/.forgejo/workflows/release-bread-theme.yml index 2892e14..335f982 100644 --- a/.forgejo/workflows/release-bread-theme.yml +++ b/.forgejo/workflows/release-bread-theme.yml @@ -6,7 +6,6 @@ on: jobs: build: - if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -57,13 +56,7 @@ jobs: - name: regenerate index.json env: MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} - run: | - set -euo pipefail - if [ -z "${MINISIGN_SEC_KEY:-}" ]; then - echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate stable index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the stable track)" - exit 1 - fi - cd src && bash scripts/gen-index.sh + run: cd src && bash scripts/gen-index.sh - name: upload to GitHub Release env: diff --git a/.gitignore b/.gitignore index e96063c..4c046ac 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,3 @@ # scripts/get.sh for how it's consumed via MINISIGN_SEC_KEY). *.minisign-sec minisign.key - -# Local tool caches — not build output, never belongs in the repo. -# (breadbar already excludes graphify-out; this repo did not, and 111k lines -# of it were swept in by a `git add -A`.) -graphify-out/ -.grok/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c8819c5..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,24 +0,0 @@ -# AGENTS.md — Repo hygiene - -Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. - -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`. Bakery's package version **must** match `[workspace.package] version` in the root `Cargo.toml` at tag time (`bakery --version` is compiled from that field; `bakery list` reports the git tag) — never push a `v*` tag without bumping Cargo.toml to the same `X.Y.Z`. - -## Remotes -- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative. -- `github` — GitHub mirror. Push both when publishing. - -## CI -- `.forgejo/workflows/package.yml`, `release-bakery.yml`, `release-bread-theme.yml` all trigger on `push: tags: ['v*']`, gated to skip any tag containing `-rc.` — pushing to `main` doesn't run these. Tag a release to trigger packaging. -- `dev-bakery.yml` / `dev-bread-theme.yml` trigger on `push: branches: ['main']`; `rc-bakery.yml` / `rc-bread-theme.yml` trigger on `push: tags: ['v*']` gated to *only* run for `-rc.` tags — both auto-publish a signed, auto-versioned build to `dl.breadway.dev/{dev,beta}/`. See `docs/release-channels.md` for the full track (stable/beta/dev) policy. -- No build/lint/test CI runs on ordinary commits or PRs to `main` beyond the dev-track workflow above — there's no separate lint/PR-check pipeline. - -## Cleanup -- Delete feature/fix branches (local + remote) once merged. Check with `git branch --merged main`. -- A `fix/audit-findings` branch and a merged `copilot/create-readme-md` branch (both local and on `origin`/`github`) were found stale and fully merged here on 2026-07-21 and removed. - -## Don't -- Don't embed credentials in remote URLs — SSH or a credential helper only. -- Don't flip bakery's default install prefix. System prefix (`/usr/local` via - `/etc/bakery/config.toml` or `BAKERY_PREFIX`) is for BOS; hermes and - `get.sh` stay on `~/.local`. See [`bakery/README.md`](bakery/README.md). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..261735d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,23 @@ +# CLAUDE.md — Repo hygiene (local only, not committed) + +Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. + +## Branch model +- `main` — release branch, always tag-ready. Don't commit directly to it. +- `dev` — integration branch. Land day-to-day work here first. +- Feature/fix work goes on short-lived branches off `dev` (`feature/x`, `fix/x`), merged back into `dev`, then `dev` → `main` when ready to release. + +## Remotes +- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative. +- `github` — GitHub mirror. Push both when publishing. + +## CI +- `.forgejo/workflows/package.yml`, `release-bakery.yml`, `release-bread-theme.yml` all trigger only on `push: tags: ['v*']` — pushing to `dev` or `main` runs nothing. Tag a release to trigger packaging. +- No build/lint/test CI runs on ordinary commits or PRs — test locally before merging to `dev`/`main`. + +## Cleanup +- Delete feature/fix branches (local + remote) once merged. Check with `git branch --merged dev` / `git branch --merged main`. +- A `fix/audit-findings` branch and a merged `copilot/create-readme-md` branch (both local and on `origin`/`github`) were found stale and fully merged here on 2026-07-21 and removed. + +## Don't +- Don't embed credentials in remote URLs — SSH or a credential helper only. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 349d06b..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,120 +0,0 @@ -# Contributing - -This repo is a Cargo workspace. Bakery-channel products shipped from here -are `bakery` (the ecosystem package manager) and `bread-theme` (the shared -theming crate). Shared crates that sibling apps pin — not bakery packages -of their own — are `bread-utils`, `bread-app`, `bread-onnx`, -`bread-screenshots`, and `bread-capture`. `bread-polkit` is an in-tree -session agent: it has `bread-polkit/bakery.toml` so it *can* be published, -but it is not in `registry/bread-ecosystem.toml` (unpublished — not on -the bakery index, not on the BOS ISO). 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`. New GTK tools should depend on -`bread-app` instead of copying another app's bootstrap. - -## Branches - -There is one long-lived branch: **`main`**. All day-to-day work lands here. -Every push to `main` automatically builds and publishes a **dev-track** -build for both products (see Tracks below) — use this to test your change -in a real install before cutting anything more formal. - -New work — features and bug fixes alike — goes on a short-lived branch: - -``` -feature/ -fix/ -``` - -Branch off `main`, open a PR/push back into `main` when ready. Short-lived -branches get deleted on merge — they never accumulate the kind of drift a -second long-lived branch does. - -## The release cycle - -There's no separate `beta` or release branch — "stable" and "beta" are both -just **tags** on `main`, not branches that need to be kept in sync: - -1. Work accumulates on `main` via `feature/x` / `fix/x` branches. Each push - auto-publishes a dev build for both `bakery` and `bread-theme` — install - with `bakery track set dev` and `bakery update --all`, then fix anything - broken with another push. -2. When you want to stabilize before a real release, tag a release - candidate: `git tag vX.Y.Z-rc.1 && git push origin vX.Y.Z-rc.1` (push to - both remotes). That tag alone triggers a beta-track build — - "freezing" is just pausing pushes to `main` while you test it, not a - branch operation. Cut `-rc.2`, `-rc.3`, etc. for further fixes. -3. Once an RC has gone without issues, tag the real release: - `git tag vX.Y.Z && git push origin vX.Y.Z` — that's what triggers the - signed stable release build. - -**Version honesty**: bakery's compiled `--version` is -`[workspace.package] version` in the root `Cargo.toml`. The bakery -package version in the index (what `bakery list` shows) is the git tag. -Those must match at tag time — bump `workspace.package.version` to -`X.Y.Z` *before* pushing `vX.Y.Z` or `vX.Y.Z-rc.N`. Never jump a tag -(e.g. `v0.3.1` → `v0.7.1`) without that Cargo.toml bump; the resulting -binary will report the old workspace version while the index claims the -new tag. - -**Note**: `bakery` and `bread-theme` share the same `v*` tag pattern -(both `release-bakery.yml` and `release-bread-theme.yml` trigger on -`tags: ['v*']`, pre-existing behavior this doc isn't changing) — a single -tag push builds and publishes a release for *both* products at once. If -you ever need to release one independently of the other, that's a real gap -worth fixing in the workflow files themselves, not something to work around -by hand. - -## Tracks, from a user's perspective - -``` -bakery track show # what you're currently on (defaults to stable) -bakery track set dev # or beta, or stable -bakery update --all # pull the latest build on your current track -``` - -| Track | What it is | Published from | -|--------|-----------|-----------------| -| `stable` | The last tagged release | a `vX.Y.Z` tag | -| `beta` | Latest release candidate | a `vX.Y.Z-rc.N` tag | -| `dev` | Bleeding edge | `main`, on every push | - -Dev versions are auto-computed (`X.Y.Z-dev.+`) from the -latest published stable tag, so they always sort as newer than what you -have installed — no manual version bumping needed. Beta versions are just -the RC tag itself (already valid semver, already sorts below the real -release it's a candidate for). - -## Local development - -```sh -cargo build --release -p bakery -cargo test --release -p bakery -``` - -`bakery`, `bread-theme`, `bread-utils`, `bread-app`, `bread-polkit`, -`bread-onnx`, `bread-screenshots`, and `bread-capture` are all workspace -members. Run the same commands with `-p bread-theme --bin bread-theme` -for that crate, `-p bread-utils --features bread-client` for the IPC -client, or `-p bread-app --features bread-client` for the GTK bootstrap -helpers. - -## CI - -- `dev-bakery.yml` / `dev-bread-theme.yml` — triggered on push to `main`. -- `rc-bakery.yml` / `rc-bread-theme.yml` — triggered on any `vX.Y.Z-rc.N` - tag push. -- `release-bakery.yml` / `release-bread-theme.yml` — triggered on any other - `v*` tag push, cuts the actual stable release. -- `package.yml` — publishes `bakery` to the `[breadway]` pacman repo, also - tag-triggered. - -All CI runs on a self-hosted runner; nothing runs automatically on plain -commits or PRs beyond the track builds above. See -[`docs/release-channels.md`](docs/release-channels.md) for the full policy, -including how a new product gets wired onto these tracks. - -## Questions - -Open an issue on this repo's Forgejo tracker. diff --git a/Cargo.lock b/Cargo.lock index a9d191d..b1812e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,40 +96,6 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - [[package]] name = "autocfg" version = "1.5.1" @@ -138,15 +104,12 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bakery" -version = "0.7.5" +version = "0.3.1" dependencies = [ "anyhow", - "bread-utils", "chrono", "clap", - "clap_complete", - "dirs 5.0.1", - "fs4", + "dirs", "hex", "minisign-verify", "semver", @@ -185,35 +148,9 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bread-app" -version = "0.7.5" -dependencies = [ - "bread-utils", -] - -[[package]] -name = "bread-capture" -version = "0.7.5" -dependencies = [ - "anyhow", - "bread-utils", - "clap", - "image", -] - -[[package]] -name = "bread-launcher" -version = "0.7.5" -dependencies = [ - "bread-utils", - "gtk4", - "serde_json", -] - [[package]] name = "bread-onnx" -version = "0.7.5" +version = "0.3.1" dependencies = [ "anyhow", "bread-utils", @@ -226,62 +163,33 @@ dependencies = [ "ureq", ] -[[package]] -name = "bread-polkit" -version = "0.7.5" -dependencies = [ - "anyhow", - "bread-app", - "bread-theme", - "gtk4", - "serde", - "tokio", - "tracing", - "tracing-subscriber", - "zbus", -] - -[[package]] -name = "bread-screenshots" -version = "0.7.5" -dependencies = [ - "anyhow", - "bread-utils", - "tracing", -] - [[package]] name = "bread-shared" -version = "0.8.0" -source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.8.0#cdd5de8f58e437b3fc6d9b9087eb7b3d0fd09704" +version = "0.7.0" +source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b" dependencies = [ - "dirs 6.0.0", + "dirs", "serde", "serde_json", "toml 0.8.23", - "uuid", ] [[package]] name = "bread-theme" -version = "0.7.5" +version = "0.3.1" dependencies = [ - "anyhow", - "dirs 5.0.1", + "dirs", "gtk4", - "libadwaita", "serde", "serde_json", - "toml 0.8.23", - "tracing", ] [[package]] name = "bread-utils" -version = "0.7.5" +version = "0.3.1" dependencies = [ "bread-shared", - "dirs 5.0.1", + "dirs", "gtk4", "gtk4-layer-shell", "serde", @@ -296,24 +204,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - [[package]] name = "cairo-rs" version = "0.22.0" @@ -407,15 +297,6 @@ dependencies = [ "strsim", ] -[[package]] -name = "clap_complete" -version = "4.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f84a88507dbd05c695f2cb5e8558e747179134005e9893882dec964190ed89" -dependencies = [ - "clap", -] - [[package]] name = "clap_derive" version = "4.6.1" @@ -425,7 +306,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -553,7 +434,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn", ] [[package]] @@ -564,7 +445,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -594,7 +475,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -604,7 +485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.119", + "syn", ] [[package]] @@ -623,16 +504,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", + "dirs-sys", ] [[package]] @@ -643,22 +515,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.6", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - [[package]] name = "displaydoc" version = "0.2.6" @@ -667,7 +527,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -682,33 +542,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -734,41 +567,12 @@ dependencies = [ "cc", ] -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - [[package]] name = "field-offset" version = "0.3.6" @@ -810,16 +614,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs4" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" -dependencies = [ - "rustix 0.38.44", - "windows-sys 0.52.0", -] - [[package]] name = "futures-channel" version = "0.3.33" @@ -852,19 +646,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.33" @@ -873,7 +654,7 @@ checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -1077,7 +858,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -1211,7 +992,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -1384,19 +1165,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", -] - [[package]] name = "indexmap" version = "2.14.0" @@ -1458,58 +1226,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libadwaita" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b9900e67182a4b5b1f157b448d94f0715c8b9770cce21cf000801917f53bfa" -dependencies = [ - "gdk4", - "gio", - "glib", - "gtk4", - "libadwaita-sys", - "pango", -] - -[[package]] -name = "libadwaita-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3c27642b389852aa99341bd4a4c19ec6f8a2b63ebdd7f5ba1952198079ccd" -dependencies = [ - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk4-sys", - "libc", - "pango-sys", - "system-deps", -] - [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libloading" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" -dependencies = [ - "cfg-if", - "windows-link", -] - [[package]] name = "libredox" version = "0.1.18" @@ -1519,12 +1241,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1559,15 +1275,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "matrixmultiply" version = "0.3.11" @@ -1615,17 +1322,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - [[package]] name = "monostate" version = "0.1.18" @@ -1645,17 +1341,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", -] - -[[package]] -name = "moxcms" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" -dependencies = [ - "num-traits", - "pxfm", + "syn", ] [[package]] @@ -1750,23 +1436,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "ort" version = "2.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" dependencies = [ - "libloading", "ndarray", "ort-sys", "smallvec", @@ -1802,12 +1477,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "paste" version = "1.0.15" @@ -1832,19 +1501,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "portable-atomic" version = "1.14.0" @@ -1896,12 +1552,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pxfm" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" - [[package]] name = "quote" version = "1.0.46" @@ -2000,17 +1650,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - [[package]] name = "regex" version = "1.13.1" @@ -2063,19 +1702,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", -] - [[package]] name = "rustix" version = "1.1.4" @@ -2085,7 +1711,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -2169,7 +1795,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -2185,17 +1811,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - [[package]] name = "serde_spanned" version = "0.6.9" @@ -2225,31 +1840,12 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.10" @@ -2268,16 +1864,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "spm_precompiled" version = "0.1.4" @@ -2325,17 +1911,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -2344,7 +1919,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -2375,7 +1950,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -2405,7 +1980,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -2416,16 +1991,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", + "syn", ] [[package]] @@ -2472,34 +2038,6 @@ dependencies = [ "unicode_categories", ] -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - [[package]] name = "toml" version = "0.8.23" @@ -2611,7 +2149,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -2623,38 +2161,12 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "once_cell", - "regex-automata", - "sharded-slab", - "thread_local", - "tracing", - "tracing-core", -] - [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - [[package]] name = "unicode-ident" version = "1.0.24" @@ -2742,18 +2254,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - [[package]] name = "version-compare" version = "0.2.1" @@ -2813,7 +2313,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn", "wasm-bindgen-shared", ] @@ -2875,7 +2375,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -2886,7 +2386,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -3116,75 +2616,10 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", "synstructure", ] -[[package]] -name = "zbus" -version = "5.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" -dependencies = [ - "async-broadcast", - "async-recursion", - "async-trait", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.4", - "serde", - "serde_repr", - "tokio", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 1.0.4", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.3", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" -dependencies = [ - "serde", - "winnow 1.0.4", - "zvariant", -] - -[[package]] -name = "zcheapstr" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" -dependencies = [ - "serde", -] - [[package]] name = "zerocopy" version = "0.8.54" @@ -3202,7 +2637,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -3222,7 +2657,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", "synstructure", ] @@ -3262,7 +2697,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -3270,44 +2705,3 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zvariant" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e28c25bd8bb8da5a1f3e7065d0c156b9ee9a7973adf78b0e35eaefdf3b1b5c" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow 1.0.4", - "zcheapstr", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d496a145685283b67e232bd9e47377f6b60ad9d51e3601b23867f77c42477f96" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.3", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629d80ece222cad20fe0e8741be493c4ab166acf3b85341bdc2cdbcfd8f3c2d6" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 3.0.3", - "winnow 1.0.4", -] diff --git a/Cargo.toml b/Cargo.toml index 183d182..4957702 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [workspace] -members = ["bakery", "bread-theme", "bread-utils", "bread-onnx", "bread-screenshots", "bread-capture", "bread-app", "bread-polkit", "bread-launcher"] +members = ["bakery", "bread-theme", "bread-utils", "bread-onnx"] resolver = "2" [workspace.package] -version = "0.7.5" +version = "0.3.1" edition = "2021" license = "MIT" authors = ["Breadway "] diff --git a/README.md b/README.md index 927c629..fff6340 100644 --- a/README.md +++ b/README.md @@ -4,36 +4,20 @@ A collection of Rust tools for the Linux desktop (Hyprland / Wayland / Arch). Install any product with a single command — no Rust toolchain required. ```sh -curl -fsSL https://get.breadway.dev | sh +curl https://breadway.dev/get | sh bakery install breadbar ``` ## Products -The table below is generated from [`registry/bread-ecosystem.toml`](registry/bread-ecosystem.toml). Regenerate with `scripts/gen-readme-products.sh`. - - - | Package | Description | |---------|-------------| -| `bakery` | Bread ecosystem package manager | -| `bread-theme` | Shared pywal-accented, fixed-dark-base theming CLI for the bread ecosystem | -| `bread` | Reactive automation daemon and CLI for Linux desktops | -| `breadbar` | Minimal status bar and notification daemon for Hyprland | -| `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 | +| `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`) | | `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 | - - ## Recommended keybinds @@ -84,7 +68,9 @@ spacing, radii, colour roles) the stylesheet is built from. `bakery` is the package manager for the ecosystem. Install it with the bootstrap script: ```sh -curl -fsSL https://get.breadway.dev | sh +curl https://breadway.dev/get | 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. @@ -106,26 +92,6 @@ bakery remove # remove a package (data files are never deleted) `bakery install` runs `doctor` first and bails with a clear message if any system dependency is missing. Binaries land in `~/.local/bin` (override with `BAKERY_BIN_DIR`). -## System prefix (BOS) - -Default install root is `~/.local`. BOS sets a system prefix so bakery-managed -desktop apps live on the `@` root subvolume and ride along with -snapper/grub-btrfs snapshots: - -```toml -# /etc/bakery/config.toml -prefix = "/usr/local" -``` - -`BAKERY_PREFIX` overrides the config file. A non-home prefix installs bins to -`$prefix/bin`, share/data/desktop/licenses to `$prefix/share/...`, and systemd -user units to `/usr/lib/systemd/user`. Per-user state (`installed.json`, -update backups) stays in `~/.local/state/bakery`. Writes that need root use -`sudo -n`, then `pkexec`. `bakery doctor` prints the active prefix. - -Hermes and `get.sh` are unchanged — they keep the user-local default. See -[`bakery/README.md`](bakery/README.md). - ## System dependencies by product `bakery doctor` checks these automatically before any install. Required deps block installation; optional deps generate a warning but never block. @@ -143,66 +109,22 @@ Install all required deps with `sudo pacman -S `. Use `pacman -Q ## 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, or in-tree tools that are not bakery packages of their own. +This repo is a Cargo workspace: ``` bread-ecosystem/ -├── bakery/ # package manager binary -├── bread-theme/ # shared pywal + fixed-dark-base theming crate -├── bread-utils/ # shared plumbing (Hyprland IPC, singleton, XDG, BreadClient, …) -├── bread-app/ # GTK bootstrap new tools should use (app id, singleton, overlay, command listen) -├── bread-polkit/ # themed PolicyKit agent (bakery.toml present; unpublished) -├── 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 +├── bakery/ # package manager binary +├── bread-theme/ # shared pywal + fixed-dark-base theming crate +├── registry/ # bread-ecosystem.toml — product registry └── scripts/ - ├── get.sh # curl | sh bootstrap - ├── gen-index.sh # generates dl.breadway.dev/index.json from release artifacts - └── gen-readme-products.sh # rewrites the Products table from the registry -``` - -### New GTK tools - -Do not copy another app's `main.rs`. Depend on `bread-app`: - -- `bread_app::application_id` / `try_acquire` / `toggle_or_kill` for the - `com.breadway.*` application id and single-instance lock -- feature `gtk` re-exports `bread_utils::gtk_popup` (layer-shell overlay) -- feature `bread-client` for `listen_commands` on `bread.command..**` - -See the `bread-app` crate docs. Existing apps are not migrated in this -tree; `bread-polkit` is the first in-tree consumer. - -### bread-polkit - -A session PolicyKit authentication agent (password prompt, cancel, -identity). Not a wrapper around `polkit-gnome`. `bread-polkit/bakery.toml` -exists so it can be published via bakery; it is not in -`registry/bread-ecosystem.toml` and is therefore unpublished — not on the -bakery index and not on the BOS ISO lockfile. - -```sh -cargo run -p bread-polkit -``` - -Autostart — pick one: - -```sh -cp bread-polkit/contrib/bread-polkit.desktop ~/.config/autostart/ -``` - -``` -# hyprland.conf -exec-once = bread-polkit + ├── get.sh # curl | sh bootstrap + └── gen-index.sh # generates dl.breadway.dev/index.json from release artifacts ``` ## Release pipeline -Each product repo (`Breadway/bread`, `Breadway/breadbar`, …) has -`.forgejo/workflows/release-*.yml` that triggers on `v*` tags. The workflow +Each product repo (`Breadway/bread`, `Breadway/breadbar`, …) has a +`.github/workflows/release.yml` that triggers on `v*` tags. The workflow runs on a self-hosted runner on hestia, builds a stripped x86_64 binary, deposits it at `dl.breadway.dev///`, updates `index.json`, and mirrors the binary to GitHub Releases as a fallback. @@ -210,12 +132,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. -Beyond stable releases, most products also publish **dev** and **beta** -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 -[`docs/release-channels.md`](docs/release-channels.md) for the full track -policy. Switch tracks with `bakery track set `. - ### Release artifact contract Each product's `release.yml` **must** upload the following files alongside diff --git a/bakery/Cargo.toml b/bakery/Cargo.toml index 06ca297..f65aedd 100644 --- a/bakery/Cargo.toml +++ b/bakery/Cargo.toml @@ -17,10 +17,9 @@ ureq = { workspace = true } sha2 = { workspace = true } hex = { workspace = true } clap = { workspace = true } -clap_complete = "4" chrono = { workspace = true } minisign-verify = { workspace = true } semver = { workspace = true } -bread-utils = { path = "../bread-utils" } -fs4 = { version = "0.8", features = ["sync"] } + +[dev-dependencies] tempfile = "3" diff --git a/bakery/README.md b/bakery/README.md deleted file mode 100644 index 21cb2cd..0000000 --- a/bakery/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# bakery - -Package manager for the bread ecosystem. Usage lives in the -[repo README](../README.md). - -## Install prefix - -Default root is `~/.local` (bins in `~/.local/bin`, data in -`~/.local/share`). That is the hermes / `get.sh` path and must stay the -default. - -BOS sets a system prefix so bakery-managed desktop apps live on the `@` -root subvolume and are included in snapper/grub-btrfs snapshots: - -```toml -# /etc/bakery/config.toml -prefix = "/usr/local" -``` - -`BAKERY_PREFIX` overrides the config file. A non-home prefix installs: - -| Thing | Path | -|-------|------| -| bins | `$prefix/bin` | -| share / desktop / licenses / data | `$prefix/share/...` | -| systemd user units | `/usr/lib/systemd/user` | - -Per-user state (`installed.json` and pre-update backups) stays in -`~/.local/state/bakery`. Writes that need root use `sudo -n`, then -`pkexec`. `bakery doctor` prints the active prefix. diff --git a/bakery/src/doctor.rs b/bakery/src/doctor.rs index f4ef71b..e261e1d 100644 --- a/bakery/src/doctor.rs +++ b/bakery/src/doctor.rs @@ -11,48 +11,18 @@ pub struct DepReport { pub fn check_deps(required: &[String], optional: &[String]) -> Result { Ok(DepReport { - missing: required - .iter() - .filter(|d| !dep_present(d)) - .cloned() - .collect(), - warnings: optional - .iter() - .filter(|d| !dep_present(d)) - .cloned() - .collect(), + missing: required.iter().filter(|d| !dep_present(d)).cloned().collect(), + warnings: optional.iter().filter(|d| !dep_present(d)).cloned().collect(), }) } -/// Arch package name -> Debian/Ubuntu package name, for the few cases where -/// they differ *and* the Debian package's own binaries don't share a name -/// with either package (so `path_has` can't bridge the gap the way it -/// already does for e.g. `ffmpeg`/`openssl`, whose package name matches -/// their own binary name on both distros). `system_deps` in `bakery.toml` -/// is always written as the Arch name — this is what makes that same -/// declaration also resolve correctly on a Debian-family bakery host like -/// hestia. -const ARCH_TO_DEBIAN_PKG: &[(&str, &str)] = &[("mkvtoolnix-cli", "mkvtoolnix")]; - -fn debian_name(pkg: &str) -> &str { - ARCH_TO_DEBIAN_PKG - .iter() - .find(|(arch, _)| *arch == pkg) - .map(|(_, debian)| *debian) - .unwrap_or(pkg) -} - fn dep_present(pkg: &str) -> bool { // Primary: `pacman -Q` uses the exact Arch package name — no name mapping needed. if pacman_installed(pkg) { return true; } // Fallback for environments without pacman: native PATH search then pkg-config. - if path_has(pkg) || pkg_config_exists(pkg) { - return true; - } - // Further fallback for Debian/Ubuntu hosts: dpkg, via the name map above. - dpkg_installed(debian_name(pkg)) + path_has(pkg) || pkg_config_exists(pkg) } fn pacman_installed(pkg: &str) -> bool { @@ -63,17 +33,6 @@ fn pacman_installed(pkg: &str) -> bool { .unwrap_or(false) } -fn dpkg_installed(pkg: &str) -> bool { - Command::new("dpkg-query") - .args(["-W", "-f=${Status}", pkg]) - .output() - .map(|o| { - o.status.success() - && String::from_utf8_lossy(&o.stdout).contains("install ok installed") - }) - .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") @@ -90,41 +49,16 @@ fn pkg_config_exists(lib: &str) -> bool { .unwrap_or(false) } -/// Builds the "install with: ..." hint for a list of missing Arch package -/// names, picking the command for whichever package manager is actually on -/// this host — `sudo pacman -S ...` is meaningless advice on a Debian-family -/// bakery host like hestia, which has neither `pacman` nor the Arch names. -pub fn install_hint(missing: &[String]) -> String { - if path_has("pacman") { - format!("sudo pacman -S {}", missing.join(" ")) - } else if path_has("apt") { - let names: Vec<&str> = missing.iter().map(|p| debian_name(p)).collect(); - format!("sudo apt install {}", names.join(" ")) - } else { - format!("install: {}", missing.join(", ")) - } -} - /// 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], - name_width: usize, -) -> bool { +pub fn report(package_name: &str, required: &[String], optional: &[String]) -> bool { if required.is_empty() && optional.is_empty() { - ui::check_row(true, package_name, name_width, "no system deps required"); + println!(" {}", ui::ok(&format!("{package_name}: no system deps required"))); return true; } match check_deps(required, optional) { Err(e) => { - ui::check_row( - false, - package_name, - name_width, - &format!("error running doctor: {e}"), - ); + eprintln!(" {}", ui::fail(&format!("error running doctor for {package_name}: {e}"))); false } Ok(rep) => { @@ -141,24 +75,17 @@ pub fn report( ); } if rep.missing.is_empty() { - ui::check_row( - true, - package_name, - name_width, - "all required system deps satisfied", - ); + println!(" {}", ui::ok(&format!("{package_name}: all required system deps satisfied"))); true } else { - ui::check_row( - false, - package_name, - name_width, - &format!("missing: {}", rep.missing.join(", ")), - ); eprintln!( " {}", - ui::dim(&format!("install with: {}", install_hint(&rep.missing))) + ui::fail(&format!( + "{package_name}: missing system deps: {}", + rep.missing.join(", ") + )) ); + eprintln!(" install with: sudo pacman -S {}", rep.missing.join(" ")); false } } @@ -188,38 +115,24 @@ mod tests { assert!(path_has("sh")); } - #[test] - fn debian_name_maps_known_alias() { - assert_eq!(debian_name("mkvtoolnix-cli"), "mkvtoolnix"); - } - - #[test] - fn debian_name_passes_through_unmapped() { - assert_eq!(debian_name("ffmpeg"), "ffmpeg"); - } - - // This test only runs on systems with dpkg (Debian/Ubuntu). - #[test] - #[ignore] - fn dpkg_finds_dpkg_itself() { - assert!(dpkg_installed("dpkg")); - } - - #[test] - fn dpkg_missing_package_not_present() { - assert!(!dpkg_installed("this-package-does-not-exist-xyzzy42")); - } - #[test] fn missing_required_dep_detected() { - let rep = check_deps(&["this-package-does-not-exist-xyzzy42".to_string()], &[]).unwrap(); + 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(); + let rep = check_deps( + &[], + &["this-package-does-not-exist-xyzzy42".to_string()], + ) + .unwrap(); assert!(rep.missing.is_empty()); assert_eq!(rep.warnings.len(), 1); } diff --git a/bakery/src/download.rs b/bakery/src/download.rs index 4d1c1c7..3362bd0 100644 --- a/bakery/src/download.rs +++ b/bakery/src/download.rs @@ -3,27 +3,33 @@ use sha2::{Digest, Sha256}; use std::path::Path; use crate::manifest::{fetch_binary, Binary}; -use crate::ui; -/// Download a binary, verify its SHA-256, then atomically write it into -/// place (fsynced, temp-in-same-dir-with-unique-name then rename — see -/// `bread_utils::atomic`). Bails before touching `dest` if the checksum -/// fails. Returns the verified hex sha256 so callers (`install:: -/// install_package`) can record it for `bakery verify` without hashing the -/// bytes a second time — `verify_sha256` already confirmed `bytes` matches -/// `binary.sha256`, so that's the value to return. -pub fn fetch_and_place(binary: &Binary, dest: &Path) -> Result { - ui::step("downloading", &binary.name); +/// Download a binary to a temp path, verify its SHA-256, then atomically move +/// it into place. Bails before touching `dest` if the checksum fails. +pub fn fetch_and_place(binary: &Binary, dest: &Path) -> Result<()> { + println!(" downloading {}…", binary.name); let bytes = fetch_binary(&binary.dl_url, &binary.github_url) .with_context(|| format!("downloading {}", binary.name))?; verify_sha256(&bytes, &binary.sha256) .with_context(|| format!("checksum mismatch for {}", binary.name))?; - crate::prefix::write_bytes(dest, &bytes, 0o755) - .with_context(|| format!("placing binary at {}", dest.display()))?; - ui::step("placed", &dest.display().to_string()); - Ok(binary.sha256.clone()) + if let Some(dir) = dest.parent() { + std::fs::create_dir_all(dir)?; + } + + let tmp = dest.with_extension("tmp"); + std::fs::write(&tmp, &bytes).context("writing binary to tmp")?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755))?; + } + + std::fs::rename(&tmp, dest).context("placing binary")?; + println!(" installed {}", dest.display()); + Ok(()) } /// Verify that `bytes` hashes to `expected_hex` under SHA-256. @@ -32,9 +38,6 @@ pub fn fetch_and_place(binary: &Binary, dest: &Path) -> Result { /// [`fetch_and_place`]), and config-example / systemd-unit downloads in /// `install.rs` — so all downloaded artifacts get the same integrity check. pub fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<()> { - if expected_hex.is_empty() { - bail!("index entry has no sha256 recorded — refusing to trust an unverifiable download"); - } let mut hasher = Sha256::new(); hasher.update(bytes); let actual = hex::encode(hasher.finalize()); @@ -77,14 +80,4 @@ mod tests { let hash = sha256_hex(bytes); assert!(verify_sha256(bytes, &hash).is_ok()); } - - #[test] - fn verify_missing_sha256_gives_a_clear_error() { - // gen-index.sh emits an empty sha256 string when a .sha256 sidecar - // is missing — must not fall through to the generic mismatch - // message ("expected: \n actual: "), which is confusing about - // what actually went wrong. - let err = verify_sha256(b"anything", "").unwrap_err(); - assert!(err.to_string().contains("no sha256 recorded")); - } } diff --git a/bakery/src/install.rs b/bakery/src/install.rs index dd31145..3b4c925 100644 --- a/bakery/src/install.rs +++ b/bakery/src/install.rs @@ -1,363 +1,120 @@ -use anyhow::{bail, Context, Result}; -use std::collections::HashMap; -#[cfg(not(test))] -use std::io::IsTerminal; +use anyhow::{Context, Result}; use std::path::{Path, PathBuf}; use std::process::Command; use crate::download::{fetch_and_place, verify_sha256}; use crate::manifest::{fetch_binary, Package, Service}; -use crate::prefix::{self, Layout}; use crate::state::{InstalledPackage, State}; -use crate::track::Track; -use crate::ui; -/// Rejects a filename that isn't a safe single path component — no `/`, -/// `\`, empty, `.`, or `..`. `bin.name`/`svc.unit`/`cfg.example`/`pkg.name`/ -/// `license_file`/`desktop_file`/`data_archive` all come from the -/// minisign-verified index, so this is defense in depth (not exploitable -/// without a compromised signing key) rather than the primary guard — but -/// closing the path-traversal class is cheap enough to do anyway before any -/// of these are joined onto a fixed base directory. -fn ensure_safe_component(name: &str, what: &str) -> Result<()> { - if name.is_empty() || name == "." || name == ".." || name.contains('/') || name.contains('\\') { - bail!("refusing to install: {what} '{name}' is not a safe filename"); - } - Ok(()) -} +pub fn install_package(pkg: &Package, bin_dir: &Path) -> Result<()> { + println!("installing {}@{}…", pkg.name, pkg.version); -/// Whether stdin should be treated as an interactive terminal. Always -/// `false` in test builds regardless of the real process stdin — running -/// `cargo test` from an actual interactive shell (not CI, not piped) gives -/// the test binary a real tty, which previously made `confirm` block on a -/// `read_line` nobody was there to answer. -fn stdin_is_terminal() -> bool { - #[cfg(test)] - { - false - } - #[cfg(not(test))] - { - std::io::stdin().is_terminal() - } -} - -/// Prompts `prompt [y/N] ` and returns the answer. `assume_yes` (the global -/// `--yes` flag) skips the prompt entirely; otherwise, a non-tty stdin -/// (CI, piped input) answers "no" rather than blocking on a read that will -/// never resolve. -fn confirm(prompt: &str, assume_yes: bool) -> bool { - if assume_yes { - return true; - } - if !stdin_is_terminal() { - return false; - } - use std::io::Write; - print!("{prompt} {} ", ui::dim("[y/N]")); - std::io::stdout().flush().ok(); - let mut buf = String::new(); - std::io::stdin().read_line(&mut buf).ok(); - matches!(buf.trim().to_lowercase().as_str(), "y" | "yes") -} - -/// Installs `pkg`. `previous` is the package's current `InstalledPackage` -/// record when this is an update (looked up by the caller before starting, -/// since it's already loaded elsewhere in the call chain) — `None` for a -/// fresh first-time install. Threading it in rather than reloading `State` -/// here avoids a second load, and lets step 1 tell "update" from "fresh -/// install" for the pre-overwrite backup below. -pub fn install_package( - pkg: &Package, - layout: &Layout, - track: Track, - previous: Option<&InstalledPackage>, - no_hooks: bool, - assume_yes: bool, -) -> Result<()> { - ensure_safe_component(&pkg.name, "package name")?; - - // 1. Download and verify all binaries. On an update (not a fresh - // install), back up the current binary first — best-effort, feeding - // `bakery rollback` — before it's overwritten below. Backups stay - // per-user under ~/.local/state even when the live binary is in - // /usr/local/bin, so a snapper snapshot of `@` plus this local copy - // is enough to roll back; no second snapshot system. - let backup_dir = previous.map(|prev| crate::state::backup_dir(&pkg.name, &prev.version)); + // 1. Download and verify all binaries. let mut binary_names = Vec::new(); - let mut binary_sha256 = HashMap::new(); for bin in &pkg.binaries { - ensure_safe_component(&bin.name, "binary name")?; let install_name = strip_arch_suffix(&bin.name); - let dest = layout.bin_dir.join(install_name); - if let Some(dir) = &backup_dir { - backup_current_binary(dir, install_name, &dest); - } - let sha256 = fetch_and_place(bin, &dest)?; + let dest = bin_dir.join(&install_name); + fetch_and_place(bin, &dest)?; binary_names.push(install_name.to_string()); - binary_sha256.insert(install_name.to_string(), sha256); } - // 2. Scaffold config dir + download example file. Config stays - // per-user (~/.config) regardless of prefix — it's authored content, - // not bakery-placed bits. + // 2. Scaffold config dir + download example file. if let Some(cfg) = &pkg.config { scaffold_config(cfg, pkg)?; } - // 3. Install license file, if declared. - if let Some(license) = &pkg.license_file { - install_license(pkg, license, layout)?; - } - - // 4. Install desktop entry, if declared. - if let Some(desktop) = &pkg.desktop_file { - install_desktop_file(pkg, desktop, layout)?; - } - - // 5. Download + extract data archive, if declared. - if let Some(archive) = &pkg.data_archive { - install_data_archive(pkg, archive, layout)?; - } - - // 6. Install systemd user units. + // 3. Install systemd user units. let mut service_names = Vec::new(); for svc in &pkg.services { - install_service(svc, layout, pkg)?; + install_service(svc, bin_dir, pkg)?; service_names.push(svc.unit.clone()); } - // 7. Run post_install hooks — arbitrary `sh -c` on index-controlled - // strings, so this is gated behind --no-hooks and an interactive - // confirmation rather than running unconditionally. - if !pkg.post_install.is_empty() { - if no_hooks { - eprintln!( - " {}", - ui::note(&format!( - "skipped {} post_install hook(s) for {} (--no-hooks)", - pkg.post_install.len(), - pkg.name - )) - ); - } else if confirm( - &format!( - " run {} post_install hook(s) for {}?", - pkg.post_install.len(), - pkg.name - ), - assume_yes, - ) { - for cmd in &pkg.post_install { - run_hook(cmd, &pkg.name)?; - } - } else { - eprintln!( - " {}", - ui::note(&format!( - "skipped post_install hooks for {} (declined)", - pkg.name - )) - ); - } + // 4. Run post_install hooks. + for cmd in &pkg.post_install { + run_hook(cmd, &pkg.name)?; } - // 8. Record in state, under an exclusive lock so a concurrent `bakery` - // invocation can't clobber this install's record with its own. - State::with_lock(|state| { - state.record(InstalledPackage { - name: pkg.name.clone(), - version: pkg.version.clone(), - binaries: binary_names, - services: service_names, - installed_at: chrono::Utc::now().to_rfc3339(), - track, - previous_version: previous.map(|p| p.version.clone()), - binary_sha256, - }); - Ok(()) - })?; + // 5. Record in state. + let mut state = State::load()?; + state.record(InstalledPackage { + name: pkg.name.clone(), + version: pkg.version.clone(), + binaries: binary_names, + services: service_names, + installed_at: chrono::Utc::now().to_rfc3339(), + }); + state.save()?; - println!(" {}", ui::ok(&format!("{} installed", pkg.name))); - warn_path_if_needed(&layout.bin_dir); + println!(" {} installed successfully", pkg.name); + warn_path_if_needed(bin_dir); Ok(()) } -/// Best-effort copy of the on-disk binary into `backup_dir` before it's -/// overwritten by an update — feeds `bakery rollback`. `backup_dir` is -/// deliberately a local path (ultimately under `~/.local/state/bakery/ -/// backups///`, see `state::backup_dir`) rather than -/// `bakery rollback` re-fetching the old version from `dl.breadway.dev`: -/// `index.json`'s minisign signature only covers the *current* published -/// version's checksums, so verifying an old version pulled fresh from the -/// server would only be checkable against its unsigned per-version -/// `.sha256` sidecar — a materially weaker guarantee than bakery's normal -/// trust model. A local pre-update snapshot sidesteps that gap entirely. -fn backup_current_binary(backup_dir: &Path, binary_name: &str, current_path: &Path) { - if !current_path.exists() { - return; - } - if let Err(e) = std::fs::create_dir_all(backup_dir) { - eprintln!( - " {}", - ui::warn(&format!( - "could not create backup dir {} ({e}) — rollback won't be available for this update", - backup_dir.display() - )) - ); - return; - } - if let Err(e) = std::fs::copy(current_path, backup_dir.join(binary_name)) { - eprintln!( - " {}", - ui::warn(&format!( - "could not back up {binary_name} before update ({e}) — rollback won't be available for this update" - )) - ); - } -} - -pub fn remove_package( - pkg_name: &str, - layout: &Layout, - assume_yes: bool, - purge: bool, -) -> Result<()> { - let installed = State::with_lock(|state| Ok(state.remove(pkg_name)))?; - let installed = match installed { +pub fn remove_package(pkg_name: &str, bin_dir: &Path) -> Result<()> { + let mut state = State::load()?; + let installed = match state.remove(pkg_name) { Some(p) => p, None => { - eprintln!(" {}", ui::fail(&format!("{pkg_name} is not installed"))); + eprintln!("{pkg_name} is not installed"); return Ok(()); } }; - ui::action("Removing", pkg_name, Some(&installed.version)); - // State is already committed by with_lock above — everything from here - // is best-effort file cleanup, and must all run even if part of it fails. + // Commit removal immediately — file cleanup below is best-effort. + state.save()?; - // Remove binaries. Collect failures instead of aborting on the first one - // so a stuck/permission-denied binary doesn't skip service removal and - // the config/data-preserved messages below. - let mut failures = Vec::new(); + // Remove binaries. for bin in &installed.binaries { - let path = layout.bin_dir.join(bin); + let path = bin_dir.join(bin); if path.exists() { - match prefix::remove_file(&path) { - Ok(()) => ui::step("removed", &path.display().to_string()), - Err(e) => failures.push(format!("{}: {e}", path.display())), - } + std::fs::remove_file(&path) + .with_context(|| format!("removing {}", path.display()))?; + println!(" removed {}", path.display()); } } // Prompt for unit removal. if !installed.services.is_empty() { - let service_dir = &layout.systemd_user_dir; + let service_dir = systemd_user_dir(); for unit in &installed.services { let unit_path = service_dir.join(unit); - if confirm_remove_unit(unit, assume_yes) { + if confirm_remove_unit(unit) { let _ = Command::new("systemctl") .args(["--user", "disable", "--now", unit]) .status(); if unit_path.exists() { - let _ = prefix::remove_file(&unit_path); + std::fs::remove_file(&unit_path).ok(); } let _ = Command::new("systemctl") .args(["--user", "daemon-reload"]) .status(); - ui::step("removed", &format!("unit {unit}")); + println!(" removed unit {unit}"); } } } - // Config is never touched, even with --purge: unlike the license/desktop - // /data paths below (all bakery-downloaded or -extracted content, - // reproducible from source), the config dir holds user-authored/edited - // content bakery never wrote — silently destroying it would be a bad - // surprise no flag should cause. + // Never touch config or data dirs. if let Some(cfg_dir) = guess_config_dir(pkg_name) { if cfg_dir.exists() { - ui::step("preserved", &format!("config {}", cfg_dir.display())); + println!(" config preserved at {}", cfg_dir.display()); } } - - let share_dir = &layout.share_dir; - let data_dir = share_dir.join(pkg_name); - - if purge { - let license_dir = share_dir.join("licenses").join(pkg_name); - remove_purged_path(&license_dir, "license dir", true, assume_yes, &mut failures); - - let desktop_file = share_dir - .join("applications") - .join(format!("{pkg_name}.desktop")); - remove_purged_path( - &desktop_file, - "desktop entry", - false, - assume_yes, - &mut failures, - ); - - remove_purged_path(&data_dir, "data dir", true, assume_yes, &mut failures); - } else if data_dir.exists() { - ui::step("preserved", &format!("data {}", data_dir.display())); + let data_dir = dirs::data_dir() + .unwrap_or_else(|| PathBuf::from("~/.local/share")) + .join(pkg_name); + if data_dir.exists() { + println!(" data preserved at {}", data_dir.display()); } - if !failures.is_empty() { - eprintln!( - " {}", - ui::fail(&format!("failed to remove {} item(s):", failures.len())) - ); - for f in &failures { - eprintln!(" {f}"); - } - bail!("{pkg_name} removed from state, but some files could not be deleted"); - } - - println!(" {}", ui::ok(&format!("{pkg_name} removed"))); + println!(" {pkg_name} removed"); Ok(()) } -/// Confirms (via `confirm`, so `--yes` and non-tty stdin behave the same as -/// every other destructive prompt in this file) and removes `path` — a -/// directory when `recursive`, otherwise a single file. Declining leaves it -/// in place and prints the same "preserved at" wording the non-purge path -/// already uses. Shared by `remove_package`'s three `--purge` targets -/// (license dir, desktop entry, data dir). -fn remove_purged_path( - path: &Path, - label: &str, - recursive: bool, - assume_yes: bool, - failures: &mut Vec, -) { - if !path.exists() { - return; - } - if !confirm( - &format!(" remove {label} at {}?", path.display()), - assume_yes, - ) { - ui::step("preserved", &format!("{label} {}", path.display())); - return; - } - let result = if recursive { - prefix::remove_dir_all(path) - } else { - prefix::remove_file(path) - }; - match result { - Ok(()) => ui::step("removed", &path.display().to_string()), - Err(e) => failures.push(format!("{}: {e}", path.display())), - } -} - fn scaffold_config(cfg: &crate::manifest::ConfigScaffold, pkg: &Package) -> Result<()> { let dir = expand_tilde(&cfg.dir); std::fs::create_dir_all(&dir)?; if let Some(example) = &cfg.example { - ensure_safe_component(example, "config.example")?; let dest = dir.join(example); if !dest.exists() { if let Some((primary, fallback)) = pkg.artifact_urls(example) { @@ -367,307 +124,89 @@ fn scaffold_config(cfg: &crate::manifest::ConfigScaffold, pkg: &Package) -> Resu Ok(()) => { std::fs::write(&dest, &bytes) .with_context(|| format!("writing {}", dest.display()))?; - ui::step("config", &dest.display().to_string()); + println!(" installed example config at {}", dest.display()); } Err(e) => { eprintln!( - " {}", - ui::warn(&format!( - "checksum mismatch for example config {example}: {e} — not installed" - )) + " warning: checksum mismatch for example config {example}: {e} — not installed" ); - ui::step("config", &dir.display().to_string()); + println!(" config dir created at {}", dir.display()); } }, None => { eprintln!( - " {}", - ui::warn(&format!( - "index.json has no sha256 for example config \ - {example} — refusing to install an unverified download" - )) + " warning: index.json has no sha256 for example config \ + {example} — refusing to install an unverified download" ); - ui::step("config", &dir.display().to_string()); + println!(" config dir created at {}", dir.display()); } }, Err(e) => { - eprintln!( - " {}", - ui::warn(&format!("could not download example config {example}: {e}")) - ); - ui::step("config", &dir.display().to_string()); + eprintln!(" warning: could not download example config {example}: {e}"); + println!(" config dir created at {}", dir.display()); } } } else { - ui::step("config", &dir.display().to_string()); + println!(" config dir created at {}", dir.display()); } } else { - ui::step( - "config", - &format!("{} already exists, skipping", dest.display()), - ); + println!(" config at {} already exists, skipping", dest.display()); } } else { - ui::step("config", &dir.display().to_string()); + println!(" config dir created at {}", dir.display()); } Ok(()) } -/// Download `filename` from `pkg`'s release dir, verify it against `sha256` -/// (refusing an unverified download the same way `scaffold_config` does), -/// and write it to `dest`. Shared by `install_license`/`install_desktop_file` -/// since both are "fetch one small artifact, verify, place" — unlike a config -/// example, these aren't user-editable, so they're always refreshed rather -/// than skipped when already present. -fn fetch_verify_write( - pkg: &Package, - filename: &str, - sha256: &Option, - dest: &Path, - label: &str, -) -> Result<()> { - let Some((primary, fallback)) = pkg.artifact_urls(filename) else { - eprintln!( - " {}", - ui::warn(&format!("no artifact URL to download {label} ({filename})")) - ); - return Ok(()); - }; - let bytes = match fetch_binary(&primary, &fallback) { - Ok(b) => b, - Err(e) => { - eprintln!( - " {}", - ui::warn(&format!("could not download {label} {filename}: {e}")) - ); - return Ok(()); - } - }; - let Some(expected) = sha256 else { - eprintln!( - " {}", - ui::warn(&format!( - "index.json has no sha256 for {label} {filename} — \ - refusing to install an unverified download" - )) - ); - return Ok(()); - }; - if let Err(e) = verify_sha256(&bytes, expected) { - eprintln!( - " {}", - ui::warn(&format!( - "checksum mismatch for {label} {filename}: {e} — not installed" - )) - ); - return Ok(()); - } - prefix::write_bytes(dest, &bytes, 0o644) - .with_context(|| format!("writing {}", dest.display()))?; - ui::step("installed", &format!("{label} {}", dest.display())); - Ok(()) -} - -fn install_license(pkg: &Package, filename: &str, layout: &Layout) -> Result<()> { - ensure_safe_component(filename, "license_file")?; - let dest = layout - .share_dir - .join("licenses") - .join(&pkg.name) - .join("LICENSE"); - fetch_verify_write(pkg, filename, &pkg.license_file_sha256, &dest, "license") -} - -fn install_desktop_file(pkg: &Package, filename: &str, layout: &Layout) -> Result<()> { - ensure_safe_component(filename, "desktop_file")?; - let dest = layout - .share_dir - .join("applications") - .join(format!("{}.desktop", pkg.name)); - fetch_verify_write( - pkg, - filename, - &pkg.desktop_file_sha256, - &dest, - "desktop entry", - ) -} - -fn install_data_archive(pkg: &Package, filename: &str, layout: &Layout) -> Result<()> { - ensure_safe_component(filename, "data_archive")?; - let data_dir = layout.share_dir.join(&pkg.name); - fetch_extract_archive(pkg, filename, &pkg.data_archive_sha256, &data_dir) -} - -/// Downloads + verifies a `.tar.gz` artifact, then extracts it into -/// `dest_dir`. Shells out to `tar` rather than adding an archive-extraction -/// crate dependency — `tar` is universally present on Linux and this file -/// already shells out to `systemctl` for the same "trust the base system -/// has this" reason. Split from `install_data_archive` (which just supplies -/// the real `$prefix/share/` destination) so tests can extract into -/// a tempdir instead. -fn fetch_extract_archive( - pkg: &Package, - filename: &str, - sha256: &Option, - dest_dir: &Path, -) -> Result<()> { - // A securely-named, process-unique temp file — the old - // `std::env::temp_dir().join(format!("bakery-{name}-{filename}"))` was a - // predictable path on a shared /tmp, so another local user could - // pre-plant a symlink there for `fetch_verify_write`'s write to follow. - let tmp_archive = tempfile::Builder::new() - .prefix(&format!("bakery-{}-", pkg.name)) - .tempfile() - .context("creating temp file for archive download")? - .into_temp_path(); - - fetch_verify_write(pkg, filename, sha256, &tmp_archive, "data archive")?; - // `fetch_verify_write` treats a download/checksum/missing-sha failure - // as a soft warning and returns `Ok` *without writing* — but the - // `NamedTempFile` above is already created (0 bytes), so gating on - // mere `exists()` would let an empty file through to `tar tvf`, which - // then bails with a confusing "not in gzip format" error that masks - // the real cause and aborts the whole install. Gate on *non-empty*. - if std::fs::metadata(&tmp_archive) - .map(|m| m.len()) - .unwrap_or(0) - == 0 - { - return Ok(()); - } - - verify_archive_paths(&tmp_archive)?; - - match prefix::extract_tar_gz(&tmp_archive, dest_dir) { - Ok(()) => ui::step( - "extracted", - &format!("{filename} → {}", dest_dir.display()), - ), - Err(e) => { - eprintln!( - " {}", - ui::warn(&format!("could not extract {filename}: {e}")) - ); - } - } - // `tmp_archive` (a `TempPath` guard) deletes the file when it drops here. - Ok(()) -} - -/// Lists `archive_path`'s contents via `tar tvf` and rejects the archive -/// outright (no extraction) if any entry is a symlink or has an unsafe path -/// (`..` component, or absolute). `--no-same-owner --no-same-permissions` on -/// the actual extraction covers ownership/permission escalation, but not a -/// symlink or `../` entry walking the extraction outside `dest_dir` — this -/// closes that gap before `tar` ever touches disk. -fn verify_archive_paths(archive_path: &Path) -> Result<()> { - let output = Command::new("tar") - .arg("tvf") - .arg(archive_path) - .output() - .context("listing archive contents")?; - if !output.status.success() { - bail!( - "tar tvf exited with {} listing archive contents — refusing to extract", - output.status - ); - } - - let listing = String::from_utf8_lossy(&output.stdout); - for line in listing.lines() { - if line.trim().is_empty() { - continue; - } - // tar -tvf: ` /