diff --git a/.forgejo/workflows/release-iso.yml b/.forgejo/workflows/release-iso.yml index 7191c22..241197d 100644 --- a/.forgejo/workflows/release-iso.yml +++ b/.forgejo/workflows/release-iso.yml @@ -1,21 +1,19 @@ name: Build and release ISO -# Builds the BOS ISO on the hestia self-hosted runner (native Arch container). -# Stages bakery desktop apps from the *minisign-verified* stable index at -# https://dl.breadway.dev/index.json (see iso/bread-lockfile.toml), then runs -# build-local.sh and uploads the ISO to a Forgejo release. A matching GitHub -# release is created best-effort and points at Forgejo for the download +# Builds the BOS ISO on the hestia self-hosted runner (native Arch container), +# downloads all bakery ecosystem binaries from their GitHub releases, compiles +# bread-theme from source, and uploads the resulting ISO to a Forgejo pre-release. +# A matching GitHub release is created that points to Forgejo for the download # (GitHub releases cannot host files larger than 2 GB). # # Required secrets: # RELEASE_TOKEN — Forgejo API token with write:repository scope -# MIRROR_TOKEN — GitHub personal access token with repo scope +# MIRROR_TOKEN — GitHub personal access token with repo scope (already used by mirror.yml) # GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing" # identity (releases@breadway.dev); public half is committed -# at KEYS.asc for verifying ISO SHA256SUMS only. That key -# does not sign the [breadway] pacman repo. No passphrase -# (CI-only key, access controlled via the Forgejo secret -# store). +# at KEYS.asc for verification. No passphrase (CI-only key, +# access controlled via the Forgejo secret store, not a +# passphrase nobody could type non-interactively anyway). on: push: @@ -30,8 +28,6 @@ jobs: release-iso: runs-on: [self-hosted, hestia] container: - # Floating tag: this environment cannot pin a reproducible digest of - # archlinux:latest. Do not invent one. image: archlinux:latest # --privileged: mkarchiso needs CAP_SYS_ADMIN for loop mounts + mknod # --network=host: gives localhost:3002 access to Forgejo (avoids the @@ -41,7 +37,7 @@ jobs: steps: - name: Install build dependencies run: | - pacman -Syu --noconfirm archiso curl python git minisign + pacman -Syu --noconfirm archiso curl python git rust - name: Determine tag and version id: vars @@ -59,17 +55,85 @@ jobs: git clone --branch "${{ steps.vars.outputs.tag }}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /bos - - name: Stage bakery ecosystem from signed stable index + - name: Download bakery ecosystem binaries run: | set -euo pipefail - cd /bos - LAPTOP_HOME=/build-home python3 scripts/ci-stage-bakery.py + mkdir -p /build-home/.local/bin \ + /build-home/.local/state/bakery \ + /build-home/.cache/bakery - - name: Verify staged bakery bake inputs + # Fetch the canonical bakery index + curl -fsSL "https://dl.breadway.dev/index.json" \ + -o /build-home/.cache/bakery/index.json + + # Download each binary from dl.breadway.dev (canonical source; github_url + # is not always published for dev/patch releases) and generate the + # installed.json that bakery expects in ~/.local/state. + python3 << 'PYEOF' + import json, urllib.request, os + + with open('/build-home/.cache/bakery/index.json') as f: + idx = json.load(f) + + BIN_DIR = '/build-home/.local/bin' + installed = {} + + for pkg_name, pkg in idx['packages'].items(): + bins = [] + for b in pkg['binaries']: + dest_name = b['name'].removesuffix('-x86_64') + dest = os.path.join(BIN_DIR, dest_name) + url = b['dl_url'] + print(f' {dest_name} <- {url}', flush=True) + urllib.request.urlretrieve(url, dest) + os.chmod(dest, 0o755) + bins.append(dest_name) + + # installed.json services field is a flat list of unit-name strings + services = [ + (s['unit'] if isinstance(s, dict) else s) + for s in pkg.get('services', []) + ] + installed[pkg_name] = { + 'name': pkg_name, + 'version': pkg['version'], + 'binaries': bins, + 'services': services, + 'installed_at': '2024-01-01T00:00:00+00:00', + } + + with open('/build-home/.local/state/bakery/installed.json', 'w') as f: + json.dump({'packages': installed}, f, indent=2) + print('installed.json written', flush=True) + PYEOF + + - name: Build bread-theme from source run: | set -euo pipefail - cd /bos - LAPTOP_HOME=/build-home bash scripts/ci-verify-bake.sh + # bread-theme is not in the bakery index; build it at the ref pinned + # in bos-settings' Cargo.toml so the CLI matches the library version + # the bos-settings package (and breadbar/breadbox/breadpad) were + # built against. bos-settings used to live at bos-settings/Cargo.toml + # inside this repo; it's since been split into its own repo + # (git.breadway.dev/Breadway/bos-settings) and migrated to Tauri, + # which moved the Rust manifest to bos-settings/src/Cargo.toml, so + # fetch it from there. Uses bos-settings' default branch (dev) — the + # branch its own CI actually publishes the `bos-settings` pacman + # package from. + REPO_OWNER="${GITHUB_REPOSITORY%%/*}" + curl -fsSL "https://git.breadway.dev/${REPO_OWNER}/bos-settings/raw/branch/dev/src/Cargo.toml" \ + -o /tmp/bos-settings-Cargo.toml + # bread-theme is pinned by tag once a release ships the functions + # bos-settings needs, or by branch in the meantime — handle either. + THEME_REF=$(grep '^bread-theme' /tmp/bos-settings-Cargo.toml \ + | grep -oP '(tag|branch)\s*=\s*"\K[^"]+') + echo "Building bread-theme @ $THEME_REF" + git clone --branch "$THEME_REF" --depth 1 \ + https://github.com/Breadway/bread-ecosystem /bread-ecosystem + cd /bread-ecosystem + cargo build --release -p bread-theme + install -m 755 target/release/bread-theme /build-home/.local/bin/bread-theme + echo "bread-theme built OK" - name: Build ISO run: | @@ -183,8 +247,9 @@ jobs: gh release create "${TAG}" \ --repo "Breadway/bos" \ --title "BOS ${TAG}" \ + \ --notes-file /tmp/gh-release-notes.md \ - || echo "skip: GitHub release failed (MIRROR_TOKEN historically broken)" + 2>/dev/null || echo "GitHub release already exists — skipping" # `stable` is a marker branch only — CI fast-forwards it to whatever # commit the latest real (non-RC) release tag points at. Never merged diff --git a/.gitignore b/.gitignore index 6e38d88..5032061 100644 --- a/.gitignore +++ b/.gitignore @@ -45,7 +45,3 @@ logs/ # Local hygiene notes (not for commit) CLAUDE.md - -# Python -__pycache__/ -*.pyc diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 3a9f4bf..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,55 +0,0 @@ -# AGENTS.md — Repo hygiene - -Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. Product shape is [README.md](README.md). - -## What this repo is - -ISO + Calamares + skel. **No Cargo workspace. No `bos-settings/` member.** -bos-settings (Tauri 2 + Svelte) and breadhelp are standalone bakery -products. breadlock is the only bread\* pacman package. - -Live skel is `iso/airootfs/etc/skel`. `dotfiles/` is stale. - -## Branch model - -Single-trunk: - -- `main` — integration + release trunk. Land work via short-lived - `feature/*` / `fix/*` branches, then merge. -- `stable` — marker only. CI fast-forwards it to the latest non-RC release - tag. Never merge into it by hand. - -There is no `dev` integration branch. - -## Remotes - -- `origin` — Forgejo (`ssh://git@100.66.238.26:2222/Breadway/bos.git`) — authoritative. -- `github` — GitHub (`https://github.com/Breadway/bos.git`) mirror. Push - origin (and github when mirroring). - -`origin` is **not** GitHub. - -## CI - -- `.forgejo/workflows/*.yml` trigger on `push: tags: ['v*']` (or - path-scoped packaging triggers) — ordinary pushes to `main` run nothing - except those path filters. Tag a release to build the ISO. -- `stable` is moved by the release-iso workflow, not by humans. -- No build/lint/test CI runs on ordinary commits or PRs — test locally - before merging to `main`. - -## Cleanup - -- Delete feature/fix branches (local + remote) once merged. Check with - `git branch --merged main`. -- Don't let merged branches accumulate. - -## Don't - -- Don't embed credentials in remote URLs — SSH or a credential helper only. -- Don't leave the default branch pointed at a feature branch on - GitHub/Forgejo. -- Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs - work does not require it. -- Don't tell users to `snapper rollback` blindly; GRUB pins - `rootflags=subvol=@`. Recovery is grub-btrfs reboot. diff --git a/DESIGN.md b/DESIGN.md index 31b22f8..d6d6585 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,26 +1,4 @@ -# BOS — historical design plan - -## Current architecture - -**Read [README.md](README.md) for how this repo actually ships.** This file -is the original plan. Several sections below are historical and must not be -taken as current: - -| Plan said | What the tree does now | -|-----------|------------------------| -| Cargo workspace with a `bos-settings/` member | This repo is ISO + Calamares + skel only. No Cargo workspace. | -| `bos-settings` as an in-tree GTK4 app | Standalone bakery product, **Tauri 2 + Svelte**. | -| bakery install in Calamares post-install | bakery binaries + breadhelp content are **baked into `/etc/skel` at ISO build time** from `iso/bread-lockfile.toml`. Missing bins fail the bake. | -| `dotfiles/` is the live skel | Live defaults are `iso/airootfs/etc/skel`. `dotfiles/` is stale. | -| A/B root swapping | **Future.** Today: btrfs + snapper + **grub-btrfs**. GRUB pins `rootflags=subvol=@`, so `snapper rollback` is not the user-facing recovery path. | -| Work on `dev`; origin = GitHub | Single-trunk `main`; `stable` is a CI marker. `origin` = Forgejo, `github` = GitHub. | -| `[breadway]` provides bakery/breadbar/bos-settings | `[breadway]` is breadlock + AUR republishes. Desktop apps are bakery. **Not shipped:** breadcast, breadarr. | -| NVIDIA / A/B / Secure Boot / LUKS2 | NVIDIA proprietary is **unsupported**. A/B root swapping is **not implemented**. Secure Boot is **Setup Mode only** (self-signed `sbctl`). Disk encryption is **LUKS1** because GRUB cannot unlock LUKS2+Argon2id. | -| `SigLevel = Required` on `[breadway]` | **No.** Forgejo's Arch registry has no pacman-compatible db signatures. `SigLevel = Never` is TLS only; flipping Required without a signed db breaks installs. `KEYS.asc` signs ISO SHA256SUMS, not the pacman repo. | - ---- - -# Original plan (kept for history) +# BOS — Bread Operating System Plan ## Context @@ -29,25 +7,51 @@ The bread ecosystem (bread, breadbar, breadbox, breadcrumbs, breadpad/breadman, Goals: - **Install and be done**: Calamares GUI installer → reboot → working Hyprland + full bread stack - **Rollback safety**: Btrfs subvolumes + snapper + snap-pac; every pacman transaction is snapshotted -- **Unified config**: `bos-settings` surfaces all app configs + snapshot management + bakery updates +- **Unified config**: `bos-settings` GTK4 app surfaces all app configs + snapshot management + bakery updates - **Future-compatible**: Btrfs layout is designed to allow A/B partition migration later (SteamOS model) --- ## Repo Structure -Single new repo: `Breadway/bos` — *planned as* a Cargo workspace. **That is -not what landed**; see Current architecture. +Single new repo: `Breadway/bos` — a Cargo workspace. ``` bos/ -├── Cargo.toml # Workspace (members: [bos-settings]) — NOT in tree -├── bos-settings/ # planned GTK4 app — now its own bakery repo -├── iso/ # archiso profile (this is the repo) +├── Cargo.toml # Workspace (members: [bos-settings]) +├── bos-settings/ # GTK4 unified settings app +│ ├── Cargo.toml +│ └── src/ +│ ├── main.rs +│ ├── state.rs +│ ├── theme.rs +│ ├── ui/ +│ │ ├── window.rs # Sidebar + content shell (port breadman pattern) +│ │ ├── sidebar.rs +│ │ └── views/ +│ │ ├── bread.rs +│ │ ├── breadbar.rs +│ │ ├── breadbox.rs +│ │ ├── breadcrumbs.rs +│ │ ├── breadpad.rs +│ │ ├── snapshots.rs +│ │ ├── packages.rs +│ │ └── hyprland.rs +│ └── config/ +│ └── mod.rs # Per-app config loaders +├── iso/ # archiso profile │ ├── profiledef.sh -│ ├── packages.x86_64 -│ └── airootfs/ -└── dotfiles/ # planned install-time configs — NOT the live skel +│ ├── packages.x86_64 # Live ISO + installed system package list +│ ├── airootfs/ # Files overlaid onto live ISO root +│ │ └── etc/ +│ │ ├── calamares/ # Calamares YAML configuration +│ │ └── skel/ # Default user dotfiles +└── dotfiles/ # Default configs deployed at install time + ├── hyprland/ # hyprland.conf, keybinds, autostart + ├── bread/ # breadd.toml, init.lua, devices.lua + ├── breadbar/ # (no config needed; zero-config by default) + ├── breadbox/ # config.toml with default context priorities + └── breadcrumbs/ # breadcrumbs.toml with default home profile ``` --- @@ -66,11 +70,7 @@ bos/ Mount options: `noatime,compress=zstd,space_cache=v2` on all subvolumes. -**A/B compatibility note (future):** The `@` subvolume is self-contained and -could be swapped atomically. This is a design property for a later upgrade -path. It is **not** implemented. Recovery today is reboot into a grub-btrfs -snapshot; GRUB's `rootflags=subvol=@` means a raw `snapper rollback` is the -wrong instruction to give users. +**A/B compatibility note:** The `@` subvolume is self-contained and can be swapped atomically — this is the design property needed for a future A/B upgrade path. The layout does not need to change to adopt it. ### Snapshot tooling (installed + configured during post-install) @@ -96,79 +96,100 @@ No user-facing CLI needed for this component — `bos-settings` is the interface ### archiso profile (`iso/`) - Derives from `/usr/share/archiso/configs/releng/` (the standard baseline) -- `packages.x86_64` is the live + installed pacman set (Hyprland, Calamares, - breadlock, WebKitGTK 4.1 for Tauri bos-settings, …). bakery apps are not - listed here. -- `airootfs/etc/skel/` contains the default user configs (this is the live - skel — not `dotfiles/`). +- `packages.x86_64` includes: base, linux, grub, btrfs-progs, snapper, snap-pac, grub-btrfs, hyprland, pipewire, wireplumber, networkmanager, gtk4, gtk4-layer-shell, iw, librsvg, libpulse, bluez, bluez-utils, calamares, calamares-qt6 +- `airootfs/etc/skel/` contains the default dotfiles (symlinked from `dotfiles/`) - Live session autologs into a `liveuser` and launches Calamares automatically ### Calamares modules (in order) -The historical list below included a post-install `bakery install` and -Calamares `bootloader`/`grubcfg` installing GRUB. What shipped instead: -binaries are already in skel; `post-install.sh` runs `grub-install` + -`grub-mkconfig` (Calamares' bootloader modules leave the ESP empty here). - 1. **welcome** — system checks (RAM ≥ 2GB, internet, disk space) 2. **locale** — timezone + locale selection 3. **keyboard** — layout selection 4. **partition** — custom `btrfs` mode: creates EFI partition + single btrfs pool with the subvolume layout above 5. **users** — create main user, set password 6. **packages** — install package list (reuses `packages.x86_64`) -7. **bootloader** — *planned*; actual GRUB install is in `post-install.sh` -8. **shellprocess (post-install)** — snapper, services, copy skel; does **not** run bakery +7. **bootloader** — install GRUB to EFI, `grub-mkconfig` with grub-btrfs hook +8. **shellprocess (post-install)** — runs `iso/post-install.sh`: + - Configures snapper root config + - Enables services: `NetworkManager`, `bluetooth`, `breadd` (user), `breadbox-sync` (user) + - Runs `bakery install bread breadbar breadbox breadcrumbs breadpad` (or `bakery install --all`) + - Copies `dotfiles/` into `/home/$USER/.config/` (skips any file that already exists) 9. **finished** — reboot prompt --- -## Component 3: `bos-settings` (planned as GTK4) - -### Tech choices (original) +## Component 3: `bos-settings` GTK4 App +### Tech choices - **gtk4-rs** (v0.11, v4_12 feature), no relm4 — plain GTK4 following breadman's pattern - -**What shipped:** Tauri 2 + Svelte in its own repo -(`git.breadway.dev/Breadway/bos-settings`), distributed by bakery. This -repo does not build it. +- **bread-theme** for palette + CSS (git dep: `github.com/Breadway/bread-ecosystem`) +- Reads/writes each tool's own config file directly (no unified intermediate config) +- Window: 960×640, sidebar 190px, `gtk4::Stack` for view switching — identical structure to breadman ### Sidebar sections + views -The panel list is still roughly accurate; see README. Snapshots recovery -should send users through **grub-btrfs reboot**, not `snapper rollback N`. +| Section | View | What it does | +|---------|------|--------------| +| **Apps** | bread | Edit `~/.config/bread/breadd.toml` | +| | breadbar | Edit `~/.config/breadbar/` (style.css override, no TOML needed) | +| | breadbox | Edit `~/.config/breadbox/config.toml` (context priority lists) | +| | breadcrumbs | Edit `~/.config/breadcrumbs/breadcrumbs.toml` (profiles, networks) | +| | breadpad | Edit `~/.config/breadpad/breadpad.toml` (model, reminders, calendar) | +| **System** | Snapshots | `snapper list` output; rollback button calls `snapper rollback N` | +| | Packages | `bakery list --installed`; update buttons call `bakery update ` | +| | Hyprland | "Open config in editor" + monitor list from `bread.state.monitors()` | + +### Config loading pattern + +Each view has a dedicated `load_config(path) -> Result` and `save_config(path, T) -> Result<()>` using `toml` crate. Config structs mirror each app's existing types (no duplication — import the `*-shared` crate where it exists, e.g. `breadpad-shared`). For apps without a shared crate (breadbox, breadcrumbs), define minimal local structs. + +### Snapshots view specifics + +- On open: runs `snapper list --output-cols number,date,description,pre-post` via `std::process::Command`, parses into table rows +- Rollback: confirmation dialog → `snapper rollback ` → notify user to reboot +- Delete: `snapper delete ` +- No write access to `/` needed for list/rollback since snapper is configured with `ALLOW_USERS` for the main user + +### Packages view specifics + +- On open: reads `~/.local/state/bakery/installed.json` directly (no network) +- "Check for updates": runs `bakery list` (triggers index refresh), compares versions +- "Update all": runs `bakery update --all` in a subprocess, streams stdout to a log TextView ### Distribution -`bos-settings` has its own `bakery.toml` and is installable via -`bakery install bos-settings` on any Arch/Hyprland system, not only as part -of a BOS install. +`bos-settings` gets a `bakery.toml` and is added to the `bread-ecosystem` registry — installable standalone on any Arch/Hyprland system via `bakery install bos-settings`, not only as part of a BOS install. --- ## Component 4: Default Dotfiles -Minimal but functional defaults. These live in `iso/airootfs/etc/skel` -(`hyprland.lua` + JSON binds, not `dotfiles/hyprland/*.conf`). +Minimal but functional defaults deployed at install time. These are opinionated starting points, not locked configs — users edit freely after install. -Zero-config bakery apps survive with no extra skel files. breadcrumbs -networks are user-filled after install — do not invent a full -`breadcrumbs.toml` in-tree. +| File | Key content | +|------|-------------| +| `dotfiles/hyprland/hyprland.conf` | Monitor auto-detect, default keybinds, `exec-once` for breadd/breadbar/breadbox-sync | +| `dotfiles/hyprland/keybinds.conf` | `$mod+Space` → breadbox, `$mod+N` → breadpad, `$mod+M` → breadman, `$mod+S` → bos-settings | +| `dotfiles/bread/breadd.toml` | All adapters enabled, log_level=info | +| `dotfiles/bread/init.lua` | Minimal: activates "default" profile on startup | +| `dotfiles/breadbox/config.toml` | Single default context with common apps | +| `dotfiles/breadcrumbs/breadcrumbs.toml` | Placeholder home profile (user fills in SSIDs) | --- ## Build Order -Historical. The ISO profile + skel + Calamares path is what this repo -iterates on. bos-settings is developed in its own repo. +1. **Dotfiles** — write default configs; these unblock installer testing immediately +2. **Btrfs + snapper config** — write `post-install.sh`; test in a VM with `archiso` livecdbase +3. **ISO profile** — archiso profiledef + package list + Calamares YAML; iterate in a VM +4. **bos-settings** — start with Snapshots and Packages views (highest value, no app-specific config parsing needed), then add per-app views one at a time --- ## Verification -- **ISO**: `sudo ./build-local.sh` (not a raw `mkarchiso iso/` — the bake - step is required). Boot in QEMU; complete install; confirm bakery bins and - `~/.local/share/breadhelp/content`. +- **ISO**: Build with `mkarchiso -v -w /tmp/bos-work -o /tmp/bos-out iso/`; boot in QEMU (`qemu-system-x86_64 -cdrom bos.iso -m 4G -enable-kvm`); complete install; reboot into installed system; confirm all services running and bakery packages present - **btrfs layout**: `btrfs subvolume list /` after install; confirm `@`, `@home`, `@snapshots`, `@log`, `@cache` exist - **snapper**: `snapper list`; run `pacman -Syu` and confirm two new snapshots appear - **grub-btrfs**: Reboot and confirm snapshot submenu in GRUB -- **bos-settings**: built and tested in the bos-settings repo, not here +- **bos-settings**: `cargo build --release`; launch; confirm each view loads its config file; edit a value, save, re-open and confirm persistence; test rollback button in Snapshots view diff --git a/README.md b/README.md index 6ad797b..7eb401b 100644 --- a/README.md +++ b/README.md @@ -5,121 +5,74 @@ ecosystem](https://git.breadway.dev/Breadway) preconfigured. One Calamares insta produces a themed, bootable Wayland desktop — no manual Arch bootstrap, no wiring up dotfiles, no per-tool bakery installs. -> This file is the product as the tree ships it. [DESIGN.md](DESIGN.md) is the -> original plan, kept as history — several of its sections (in-tree GTK -> bos-settings, bakery-at-post-install, A/B as if it were current) are not -> how the ISO works today. +> Design rationale and the btrfs/A-B roadmap live in [DESIGN.md](DESIGN.md). +> This file is the practical overview: what's in the image, how to build it, +> and how to test it. ## What you get - **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated keybinds, snappy animations, blur, and pywal-driven colours on a black base. - **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries - (no network needed at install time): the `bread`/`breadd` automation daemon - (`bread-emit` / `bread-module-host` when the stable bread release publishes - them), `breadbar` (status bar + notifications), `breadbox` (launcher), - `breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles), - `breadpad`/`breadman` (notes), `breadpaper` (wallpaper + theme), - `breadsearch` (system search), `breadmon` (monitor layout TUI), - `breadshot` (screenshots), `bread-theme` (the shared palette engine), - `breadhelp` (onboarding + cheatsheet), `bos-settings` (control panel), - and the `bakery` package manager. Most of those apps are zero-config on - first boot; breadcrumbs networks are user-filled after install. See - [below](#the-bread-ecosystem). -- **breadlock** (lock screen + greeter) is the one bread\* app that ships as - **pacman**, not bakery — it needs a root-owned PAM service. -- **bos-settings**: a **Tauri 2 + Svelte** control panel (standalone bakery - product, not a member of this repo). Configures every bread\* app - non-destructively, plus snapshots, bakery/pacman updates, and day-to-day - machine administration. -- **Login**: greetd + breadgreet (under `cage`) → Hyprland session. + (no network needed at install time): the `bread`/`breadd` automation daemon, + `breadbar` (status bar + notifications), `breadbox` (launcher), `breadclip` + (clipboard history), `breadcrumbs` (Wi-Fi profiles), `breadpad`/`breadman` + (notes), `breadpaper` (wallpaper + theme), `breadsearch` (system search), + `breadmon` (monitor layout TUI), `breadshot` (screenshots), `bread-theme` + (the shared palette engine), and the `bakery` package manager. `breadlock` + (lock screen + greeter) ships as its own pacman package alongside + `bos-settings`, not through bakery. See [below](#the-bread-ecosystem) for + what each one actually does. +- **bos-settings**: a GTK4 control panel that configures every bread\* app's + config from a GUI (non-destructively), plus snapshot rollback and bakery + updates. See below. +- **Login**: greetd + breadgreet (bread-ecosystem's own greeter, under `cage`) + → Hyprland session. - **Boot splash**: Plymouth `bos` theme (logo + spinner, black background). - **Theming**: global dark across GTK3 (Adwaita-dark), GTK4/libadwaita (`color-scheme: prefer-dark`), and Qt (qt5ct/qt6ct Fusion dark); Papirus-Dark icons; Bibata cursor. - **Apps**: kitty, nautilus (+ gvfs), Zen browser, VLC, loupe, gnome-text-editor, gnome-calculator, file-roller, with file associations wired in `mimeapps.list`. - `yay` ships for AUR access beyond bakery + `[breadway]`. + `yay` ships for AUR access beyond bakery's bread ecosystem + `[breadway]`. - **Hardware**: pipewire audio, NetworkManager, BlueZ + blueman, CUPS printing with avahi mDNS discovery, TLP power management, fwupd firmware updates. - Mesa only — **NVIDIA proprietary drivers are not included** and NVIDIA is - unsupported out of the box (see Known limitations). - **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every pacman transaction; zram swap; ufw firewall (deny-incoming, mDNS allowed). - A/B root swapping is **not** implemented. Recovery is a grub-btrfs reboot, - not `snapper rollback` (GRUB pins `rootflags=subvol=@`). -- **Security**: optional full-disk encryption is **LUKS1** (GRUB cannot unlock - LUKS2 + Argon2id). Secure Boot is **self-signed Setup Mode only** via - `sbctl` — not a Microsoft-signed shim; enrollment is skipped unless the - firmware is already in Setup Mode. - -## What ships vs what does not - -| Channel | What | -|---------|------| -| **Bakery, required** | `bakery`, `bread` / `breadd`, `breadbar`, `breadbox` / `breadbox-sync`, `breadcrumbs`, `breadpad` / `breadman`, `breadpaper`, `bread-theme`, `breadmon`, `breadsearch` / `breadmill`, `breadclip` / `breadclipd`, `breadshot`, `bos-settings`, `breadhelp` (+ breadhelp content under `~/.local/share/breadhelp/`) | -| **Bakery, optional** | `bread-emit`, `bread-module-host` — baked when the verified stable index publishes them; skipped (not a failed bake) until bread ships them | -| **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) | -| **Not shipped** | `breadcast`, `breadarr` | - -The baked name list is [`iso/bread-lockfile.toml`](iso/bread-lockfile.toml). -`build-local.sh` fails if any **required** binary is missing on the builder. +- **Security**: full-disk encryption (LUKS, via Calamares' built-in support — + cryptsetup + the matching mkinitcpio/GRUB wiring ship so an encrypted + install actually boots) and self-signed Secure Boot (via `sbctl`, enrolled + automatically at install time when the firmware is in Setup Mode). ## Repo layout -This is an **ISO + Calamares + skel** repo. There is no Cargo workspace and -no `bos-settings/` member — bos-settings and breadhelp live in their own -repos and arrive via bakery. - ``` bos/ +├── Cargo.toml # workspace (members: bos-settings) +├── bos-settings/ # GTK4 unified settings app (Rust) +│ └── src/ +│ ├── config/mod.rs # non-destructive toml_edit config layer +│ └── ui/{widgets,window,sidebar}.rs, ui/views/*.rs ├── iso/ # archiso profile -│ ├── bread-lockfile.toml # bakery bins (required + optional) │ ├── profiledef.sh -│ ├── packages.x86_64 # live + installed pacman set +│ ├── packages.x86_64 # live + installed package set │ └── airootfs/ # files overlaid onto the image │ └── etc/ -│ ├── skel/ # live user defaults (hypr, kitty, gtk, …) +│ ├── skel/ # default user dotfiles (hypr, kitty, gtk, …) │ └── calamares/ # installer config + post-install.sh ├── packaging/ # in-house PKGBUILDs for AUR-only deps +│ ├── arch/ # bos-settings │ ├── calamares/ -│ ├── bibata/ -│ ├── powerlevel10k/ -│ └── yay-bin/ -├── dotfiles/ # STALE — not the live skel; see its README -├── scripts/ -│ ├── ci-stage-bakery.py # CI: minisign-verified index → $LAPTOP_HOME -│ ├── ci-verify-bake.sh # CI: read-only checks before mkarchiso -│ └── smoke-test.sh -├── .forgejo/workflows/ # CI: AUR republish + tagged ISO release +│ └── bibata/ +├── .forgejo/workflows/ # CI: build + publish packages to [breadway] ├── build-local.sh # native ISO build for this machine -├── README.md -└── DESIGN.md # historical plan +└── DESIGN.md ``` -Live binds are `iso/airootfs/etc/skel/.config/hypr/binds.json` (`Super+L` → -`loginctl lock-session`, breadshot on `Super+Shift+S/C/P`, `Super+U` -breadpad). Do not treat `dotfiles/hypr/keybinds.conf` as current. - -## Branches and remotes - -Single-trunk: work on **`main`** via short-lived `feature/*` / `fix/*` -branches. **`stable`** is a marker branch CI fast-forwards to the latest -non-RC release tag — do not land work there by hand. - -Dual remotes: - -- **`origin`** — Forgejo (`ssh://git@100.66.238.26:2222/Breadway/bos.git`), - authoritative -- **`github`** — GitHub (`https://github.com/Breadway/bos.git`) mirror - -Push `origin` (and `github` when mirroring). Do not treat origin as GitHub. - ## Building the ISO `build-local.sh` builds the image natively (no container) and bakes this -machine's bakery-installed bread binaries + breadhelp content into -`/etc/skel`: +machine's bakery-installed bread binaries into `/etc/skel`: ```sh sudo ./build-local.sh # release-quality (xz squashfs) @@ -127,22 +80,16 @@ sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration (zstd squashfs) ``` The ISO lands in `out/bos--x86_64.iso`. The script pins -`SOURCE_DATE_EPOCH` (reproducible UUIDs), rewrites the `[breadway]` repo URL -to the Tailscale-reachable Forgejo registry for the build, and **exits -non-zero** if any **required** lockfile binary (or breadhelp content) is -missing. Optional bins are skipped with a warning. - -CI stages the builder from the **minisign-verified** stable bakery index -(`index.json` + `index.json.minisig`); local builds still snapshot the -builder. The lockfile is names only. +`SOURCE_DATE_EPOCH` (reproducible UUIDs) and rewrites the `[breadway]` repo URL +to the Tailscale-reachable Forgejo registry for the build. ### Why some packages are in-house `calamares`, `zen-browser-bin`, `bibata-cursor-theme`, and `yay-bin` are -AUR-only. BOS keeps a PKGBUILD for each under `packaging/` and republishes -the built package to the `[breadway]` repo via a Forgejo Actions workflow -(built on the hestia self-hosted runner, published with a scoped registry -token). `[breadway]` is **not** where bakery/breadbar/bos-settings live. +AUR-only. BOS keeps a PKGBUILD for each under `packaging/` and republishes the +built package to the `[breadway]` repo via a Forgejo Actions workflow (built +on the hestia self-hosted runner, published with a scoped registry token). +`bos-settings` itself publishes the same way on a `v*` tag. ### Verifying a release @@ -155,10 +102,7 @@ dedicated release-signing key (not reused from anything else): 5620 3B86 A110 695A E7F3 1093 4AF3 323D 678E B5E2 ``` -The public half is committed at [`KEYS.asc`](KEYS.asc). That key signs -**ISO checksums only** — it does not sign the `[breadway]` pacman repo -(Forgejo's Arch registry has no pacman-compatible db signatures; that -section stays `SigLevel = Never`). To verify a download: +The public half is committed at [`KEYS.asc`](KEYS.asc). To verify a download: ```sh gpg --import KEYS.asc @@ -180,21 +124,19 @@ It uses KVM + `-cpu host`, 8 GiB / 8 vCPU, and `virtio-vga-gl` with Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to avoid memory pressure. -Post-install, `scripts/smoke-test.sh` (run as the installed user) checks -subvolumes, services, bakery bins, and breadhelp content under -`~/.local/share/breadhelp/content`. - ## bos-settings -Standalone bakery product: **Tauri 2 + Svelte**, not GTK4, and not built -from this repo. Install/update with `bakery`; the ISO just bakes whatever -binary the builder has. +A GTK4 settings app aiming for GNOME-Settings-style parity: not just editing +config files, but live system state and control, so day-to-day machine +administration doesn't require a terminal. -It aims for GNOME-Settings-style parity: live system state and control, so -day-to-day administration doesn't require a terminal. Bread-ecosystem -configs are edited **non-destructively** (comments and unmodeled keys stay). -Panels with a daemon (bread, breadbox, breadcrumbs, breadsearch, breadclip) -also get live systemd status + Start/Stop/Restart/Logs. +Bread-ecosystem TOML configs are edited **non-destructively**: `toml_edit` +parses the file, changes only the keys a view exposes, and writes it back — +preserving comments and any keys the UI doesn't model (calendar passwords, +saved-network passwords, model paths). Panels with a daemon behind them +(bread, breadbox, breadcrumbs, breadsearch, breadclip) also get live +systemd status + Start/Stop/Restart/Logs via a shared `service_control` +widget, not just the config file. | Panel | What it does | |-------|--------------| @@ -219,8 +161,12 @@ also get live systemd status + Start/Stop/Restart/Logs. | Firmware | `fwupd` device list + updates | | Snapshots | `snapper` list / boot-into (grub-btrfs) / delete | -Source and build live in the [bos-settings](https://git.breadway.dev/Breadway/bos-settings) -repo, not here. +Build standalone: + +```sh +cargo build --release -p bos-settings +cargo test -p bos-settings # includes config round-trip tests +``` ## The bread ecosystem @@ -228,17 +174,17 @@ Everything below is a separate bakery-distributed project with its own repo and release cadence, baked into `/etc/skel` at ISO build time so a fresh install has them all with no network round-trip. Some ship more than one binary from a single package — that's noted where it applies. Most have a -corresponding **bos-settings** panel; this table is about *using* the app -directly. +corresponding **bos-settings** panel for configuration; this table is about +*using* the app directly. **Desktop shell** | Tool | Role | Launch | |------|------|--------| -| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor/power/network signals into events dispatched to Lua modules (`~/.config/bread/`). `bread-emit` is the fire-and-forget helper hooks/CLIs use; `bread-module-host` is the sandboxed out-of-process module runtime breadd spawns. Both extra bins are **optional** on the ISO until a stable bread release publishes them. | runs at login (`breadd.service`) | +| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor/power/network signals into events dispatched to Lua modules (`~/.config/bread/`). Everything else in the ecosystem can subscribe to its events. | runs at login (`breadd.service`) | | `breadbar` | Top status bar: workspaces, clock, system stats, tray, **and** the notification daemon — one process, not two | runs at login | | `breadbox` | Application launcher (fuzzy search, per-context results via `breadbox-sync`) | `SUPER+Space` | -| `breadlock` | Idle lock screen. Also provides `breadgreet`, the login greeter hosted under `cage` via greetd — same project, two binaries, one visual identity from login to lock. **pacman**, not bakery. | `SUPER+L` (via `loginctl lock-session`, picked up by `hypridle`); `breadgreet` runs automatically at boot | +| `breadlock` | Idle lock screen. Also provides `breadgreet`, the login greeter hosted under `cage` via greetd — same project, two binaries, one visual identity from login to lock | `SUPER+L` (via `loginctl lock-session`, picked up by `hypridle`); `breadgreet` runs automatically at boot | | `bread-theme` | The shared palette engine every bread app renders through: fixed dark base colors, with only the accent slots following the current wallpaper's pywal palette. `bread-theme generate` regenerates the stylesheet; hyprland.lua calls it automatically on wallpaper change. | invoked automatically, rarely run by hand | **Productivity** @@ -249,7 +195,6 @@ directly. | `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` | | `breadclip` | Clipboard history. `breadclipd` is the background daemon that actually records history; `breadclip` is the GTK4 popup that browses it | `SUPER+V` / `SUPER+Shift+V` | | `breadsearch` | Semantic system-wide search (indexes files/notes, embeds locally — CPU/ROCm/CUDA backend configurable). `breadmill` is its indexing daemon. | via breadbox, or BOS Settings → File Search | -| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `~/.local/share/breadhelp/content` (bakery `content.tar.gz`, baked into skel). | `SUPER+/` | **System** @@ -265,7 +210,7 @@ directly. | Tool | Role | Launch | |------|------|--------| | `bakery` | CLI package manager for the whole ecosystem — install/update/list, tracks installed binaries + versions independently of pacman | `bakery` | -| `bos-settings` | Unified Tauri 2 + Svelte control panel: live system state + control (network, power, firewall, users, packages, firmware, AUR, snapshots) plus non-destructive config editing for every app above | `SUPER+,` | +| `bos-settings` | Unified GTK4 control panel: live system state + control (network, power, firewall, users, packages, firmware, AUR, snapshots) plus non-destructive config editing for every app above | `SUPER+,` | ## Keyboard shortcuts @@ -329,16 +274,9 @@ cheatsheet in-session; first boot shows a short welcome (once). ## Recovery **An update broke something (system still boots):** open BOS Settings → -Snapshots and pick a snapshot to boot, **or** choose one from the **GRUB -“snapshots” submenu** (grub-btrfs) at boot, then reboot into it. - -Do **not** run `snapper rollback` as the default recovery step. BOS GRUB -pins `rootflags=subvol=@`, so a snapper-swapped default subvolume is not -what the installed grub.cfg will boot next. Use the grub-btrfs entry so the -kernel command line matches the snapshot you want. - -A/B root swapping (SteamOS-style) is a **future** idea in DESIGN.md — it is -not shipped. +Snapshots and roll back, or pick a pre-update snapshot from the **GRUB +“snapshots” submenu** at boot, then run `snapper rollback` from the booted +snapshot. **The system won't boot (broken GRUB / lost EFI entry):** diff --git a/build-local.sh b/build-local.sh index b06f58d..ebaac0a 100755 --- a/build-local.sh +++ b/build-local.sh @@ -41,235 +41,66 @@ if [ "${FAST_BUILD:-0}" = "1" ]; then fi grep airootfs_image_tool_options "$STAGE/profiledef.sh" -# --- Bake this machine's bakery-installed bread ecosystem into /etc/skel ------ -# The bread desktop apps are bakery-managed (release binaries from -# dl.breadway.dev / GitHub), not pacman. bakery needs DNS at install time, -# which the live/installed image doesn't have — so instead of running bakery -# on the target, we copy the binaries + bakery manifest this builder already -# has into skel. Every user created from skel then gets those versions fully -# offline. Copied at build time so the binaries never bloat the git repo. -# -# CI should prefer the stable bakery index when populating the builder home. -# Local builds still snapshot the builder. required_bins fail the bake if -# missing; optional_bins are skipped with a warning (a hollow ISO is worse -# than a failed build). A flat `bins` list is treated as all-required. -LOCKFILE="$REPO/iso/bread-lockfile.toml" -if [[ ! -f "$LOCKFILE" ]]; then - echo "ERROR: bakery lockfile missing: $LOCKFILE" >&2 - exit 1 -fi -eval "$(python3 - "$LOCKFILE" <<'PY' -import sys, tomllib -path = sys.argv[1] -with open(path, "rb") as f: - data = tomllib.load(f) -required = data.get("required_bins") -optional = data.get("optional_bins") or [] -if required is None: - required = data.get("bins") or data.get("binaries") -if not isinstance(required, list) or not required: - sys.exit(f"{path}: missing non-empty required_bins (or bins) list") -if not isinstance(optional, list): - sys.exit(f"{path}: optional_bins must be a list") -blocked = {"breadcast", "breadarr"} -for label, names in (("required_bins", required), ("optional_bins", optional)): - for b in names: - if not isinstance(b, str) or not b or "/" in b or b in (".", ".."): - sys.exit(f"{path}: invalid {label} name {b!r}") - if b in blocked: - sys.exit(f"{path}: {b} is not shipped on the ISO") -def emit(name, values): - print(f"{name}=(") - for v in values: - print(f" {v!r}") - print(")") -emit("REQUIRED_BINS", required) -emit("OPTIONAL_BINS", optional) -PY -)" -if [[ ${#REQUIRED_BINS[@]} -eq 0 ]]; then - echo "ERROR: $LOCKFILE produced an empty required bins list" >&2 - exit 1 -fi - +# --- Bake this laptop's bakery-installed bread ecosystem into /etc/skel ------- +# The bread apps are managed by bakery (which fetches release binaries from +# GitHub), not pacman. bakery needs DNS at install time, which the live/installed +# image doesn't have — so instead of running bakery on the target, we copy the +# exact binaries + bakery manifest this laptop already has into skel. Every user +# created from skel (the live user and the installed user) then gets the same +# versions `bakery list` reports here, fully offline. Copied at build time so the +# binaries never bloat the git repo and always track the current bakery state. +BREAD_BINS=(bakery bread breadd breadman breadbar breadbox breadbox-sync breadcrumbs breadpad breadpaper bread-theme breadmon breadsearch breadmill breadclip breadclipd breadshot bos-settings breadhelp) LAPTOP_HOME="${LAPTOP_HOME:-$(getent passwd "${SUDO_USER:-$USER}" | cut -d: -f6)}" BAKERY_BIN="$LAPTOP_HOME/.local/bin" BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery" BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery" -BAKERY_SHARE="$LAPTOP_HOME/.local/share" SKEL="$STAGE/airootfs/etc/skel" echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ===" -echo "lockfile: $LOCKFILE (${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional)" - -missing=() -for b in "${REQUIRED_BINS[@]}"; do - if [[ ! -x "$BAKERY_BIN/$b" ]]; then - missing+=("$BAKERY_BIN/$b") - fi -done -if [[ ${#missing[@]} -gt 0 ]]; then - echo "ERROR: bakery lockfile requires binaries that are missing on the builder:" >&2 - printf ' %s\n' "${missing[@]}" >&2 - echo "Install them with bakery (or stage them under $BAKERY_BIN) before baking." >&2 - echo "A hollow ISO is worse than a failed build." >&2 - exit 1 -fi - -BREAD_BINS=("${REQUIRED_BINS[@]}") -for b in "${OPTIONAL_BINS[@]}"; do - if [[ -x "$BAKERY_BIN/$b" ]]; then - BREAD_BINS+=("$b") - else - echo "WARN: optional lockfile bin missing, skipping: $BAKERY_BIN/$b" >&2 - fi -done - install -d -m 0755 "$SKEL/.local/bin" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery" for b in "${BREAD_BINS[@]}"; do install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b" done - -# Drop packages that are not in the lockfile (breadcast/breadarr must not -# appear installed when their binaries were deliberately left out). -python3 - "$BAKERY_STATE/installed.json" "$SKEL/.local/state/bakery/installed.json" "${BREAD_BINS[@]}" <<'PY' -import json, sys -src, dest, *bins = sys.argv[1:] -wanted = set(bins) -with open(src) as f: - data = json.load(f) -pkgs = data.get("packages", data) -if not isinstance(pkgs, dict): - sys.exit(f"{src}: expected packages object") -kept = {} -for name, pkg in pkgs.items(): - pbins = pkg.get("binaries") or [] - if name in wanted or any(b in wanted for b in pbins): - kept[name] = pkg -out = {"packages": kept} -if "track" in data: - out["track"] = data["track"] -with open(dest, "w") as f: - json.dump(out, f, indent=2) - f.write("\n") -print("installed.json packages:", ", ".join(sorted(kept)) or "(none)") -PY - +install -m 0644 "$BAKERY_STATE/installed.json" "$SKEL/.local/state/bakery/installed.json" # bakery fetches its package index from dl.breadway.dev (then a GitHub fallback), # but falls back to a cached index when both are unreachable. With no network/DNS # in the live/installed image, even `bakery list` errors unless that cache exists, # so bake it in too — then bakery works fully offline (list/info from cache; # install/update still need network, as expected). -if [[ ! -f "$BAKERY_CACHE/index.json" ]]; then - echo "ERROR: bakery index cache missing: $BAKERY_CACHE/index.json" >&2 - exit 1 -fi install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json" -echo "baked bins: $(ls "$SKEL/.local/bin")" - -# --- Bake bakery data dirs the apps need offline ------------------------------ -# bakery extracts data_archive (breadhelp's content.tar.gz) to -# ~/.local/share// and writes desktop entries + licenses next to it. -# Copy those — never laptop-local state (clipboard history, WebKit cache, -# bread sync-repo, models). -echo "=== baking bakery share/data into skel ===" -BREADHELP_CONTENT="$BAKERY_SHARE/breadhelp/content" -if [[ ! -d "$BREADHELP_CONTENT" ]]; then - echo "ERROR: breadhelp content missing: $BREADHELP_CONTENT" >&2 - echo "bakery installs this from content.tar.gz into ~/.local/share/breadhelp/content" >&2 - echo "A breadhelp binary without content is a hollow ISO." >&2 - exit 1 -fi -install -d -m 0755 "$SKEL/.local/share" -cp -a "$BAKERY_SHARE/breadhelp" "$SKEL/.local/share/breadhelp" -echo " baked $SKEL/.local/share/breadhelp/content" - -python3 - "$BAKERY_CACHE/index.json" "$BAKERY_SHARE" "$SKEL/.local/share" "${BREAD_BINS[@]}" <<'PY' -import json, os, shutil, sys -index_path, src_share, dest_share, *bins = sys.argv[1:] -wanted = set(bins) -try: - with open(index_path) as f: - idx = json.load(f) - packages = idx.get("packages", {}) -except (OSError, json.JSONDecodeError): - packages = {} - -# Package names we ship: lockfile bin names plus index packages that -# publish at least one of those bins. -pkg_names = set(wanted) -for name, pkg in packages.items(): - pbins = [] - for b in pkg.get("binaries") or []: - n = b["name"] if isinstance(b, dict) else b - pbins.append(str(n).removesuffix("-x86_64")) - if name in wanted or any(b in wanted for b in pbins): - pkg_names.add(name) - -os.makedirs(os.path.join(dest_share, "applications"), exist_ok=True) -os.makedirs(os.path.join(dest_share, "licenses"), exist_ok=True) - -for name in sorted(pkg_names): - pkg = packages.get(name) or {} - if pkg.get("data_archive"): - src = os.path.join(src_share, name) - dest = os.path.join(dest_share, name) - if name == "breadhelp": - continue # already copied above, required - if os.path.isdir(src): - if os.path.exists(dest): - shutil.rmtree(dest) - shutil.copytree(src, dest, symlinks=True) - print(f" baked data dir {dest}") - else: - sys.exit(f"ERROR: bakery data_archive for {name} missing at {src}") - - desktop_src = os.path.join(src_share, "applications", f"{name}.desktop") - desktop_dest = os.path.join(dest_share, "applications", f"{name}.desktop") - if os.path.isfile(desktop_src) and not os.path.isfile(desktop_dest): - shutil.copy2(desktop_src, desktop_dest) - print(f" baked desktop {desktop_dest}") - - lic_src = os.path.join(src_share, "licenses", name) - lic_dest = os.path.join(dest_share, "licenses", name) - if os.path.isdir(lic_src) and not os.path.isdir(lic_dest): - shutil.copytree(lic_src, lic_dest, symlinks=True) - print(f" baked license {lic_dest}") -PY +echo "baked: $(ls "$SKEL/.local/bin")" # --- Bake systemd user services for bakery-managed bread packages ----------- # Historically only breadd.service was hand-committed to skel; every other # bakery package's service (breadbox-sync, breadmill, breadclipd, ...) was # silently left out, so those daemons never start on a fresh install/live # boot until the user re-runs `bakery install` (which needs network). -# Source of truth is the *filtered* installed.json we just wrote: only -# lockfile packages. Copy each unit with ExecStart rewritten from this -# laptop's literal home path to the portable `%h` specifier, and recreate -# whichever *.target.wants enable symlink bakery created locally. Units -# already committed by hand (breadd.service carries a -# RuntimeDirectoryPreserve=yes fix not yet upstreamed) are left alone. +# Generalize from the same source of truth as the binary bake above: read +# the services this laptop's bakery actually installed, copy each unit file +# into skel with ExecStart rewritten from this laptop's literal home path to +# the portable `%h` specifier, and recreate whichever *.target.wants enable +# symlink bakery created locally. Units already committed by hand (breadd.service +# carries a RuntimeDirectoryPreserve=yes fix not yet upstreamed — see bread-release-build +# notes) are left alone rather than overwritten. echo "=== baking bakery service units into skel ===" SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user" SKEL_SYSTEMD="$SKEL/.config/systemd/user" -mapfile -t SERVICE_UNITS < <(python3 - "$SKEL/.local/state/bakery/installed.json" <<'PY' -import json, sys -with open(sys.argv[1]) as f: +mapfile -t SERVICE_UNITS < <(python3 -c " +import json +with open('$BAKERY_STATE/installed.json') as f: d = json.load(f) -for pkg in d.get("packages", d).values(): - for s in pkg.get("services", []): - print(s["unit"] if isinstance(s, dict) else s) -PY -) +for pkg in d.get('packages', d).values(): + for s in pkg.get('services', []): + print(s) +") for unit in "${SERVICE_UNITS[@]}"; do - [[ -n "$unit" ]] || continue if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then echo " $unit already committed in skel, leaving as-is" continue fi src="$SYSTEMD_USER_DIR/$unit" if [[ ! -f "$src" ]]; then - echo "ERROR: $unit listed in bakery installed.json but not found at $src" >&2 - echo "Refusing to bake a skel whose daemons will never start." >&2 - exit 1 + echo " warning: $unit not found at $src, skipping" + continue fi install -d -m 0755 "$SKEL_SYSTEMD" sed "s#ExecStart=$LAPTOP_HOME/.local/bin/#ExecStart=%h/.local/bin/#" "$src" > "$SKEL_SYSTEMD/$unit" diff --git a/dotfiles/README.md b/dotfiles/README.md deleted file mode 100644 index 8e61cc6..0000000 --- a/dotfiles/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# `dotfiles/` is not the live skel - -These files are a leftover from an earlier design (Hyprland `.conf` binds). -They are **not** copied into the ISO or the installed system. - -`dotfiles/hypr/keybinds.conf` still mentions `grimblast`. That is not the -screenshot tool BOS ships — live binds use **breadshot** -(`iso/airootfs/etc/skel/.config/hypr/binds.json`). Do not treat grimblast -as current. - -Live user defaults live in [`iso/airootfs/etc/skel`](../iso/airootfs/etc/skel) -(`hyprland.lua` + `binds.json`, breadlock/`loginctl lock-session`, breadshot, -breadpad, …). Edit that tree. diff --git a/dotfiles/hypr/keybinds.conf b/dotfiles/hypr/keybinds.conf index a857962..7cf8cdd 100644 --- a/dotfiles/hypr/keybinds.conf +++ b/dotfiles/hypr/keybinds.conf @@ -1,7 +1,3 @@ -# STALE — not the live Hyprland binds. Not copied into the ISO. -# Screenshots are breadshot (see iso/airootfs/etc/skel/.config/hypr/binds.json), -# not grimblast. Do not copy from this file. - $mod = SUPER # App launchers diff --git a/iso/airootfs/etc/calamares/post-install.sh b/iso/airootfs/etc/calamares/post-install.sh index 66ba35b..c2e059f 100644 --- a/iso/airootfs/etc/calamares/post-install.sh +++ b/iso/airootfs/etc/calamares/post-install.sh @@ -41,11 +41,8 @@ passwd -l root || true # over to the target (unpackfs may skip it / perms differ), leaving the installed # system unable to verify package signatures — the first `pacman -Syu` then dies # with "keyring is not writable / required key missing". Initialise it here so a -# fresh install can update out of the box. archlinux-keyring is already present -# and is the only keyring populated — it verifies official Arch packages. -# [breadway] stays SigLevel=Never (Forgejo does not serve pacman-compatible -# db signatures). Do not import KEYS.asc here: that key signs ISO SHA256SUMS, -# not the pacman repo; treating it as a repo key would be a lie. +# fresh install can update out of the box. archlinux-keyring is already present; +# [breadway] is SigLevel=Never so it needs no key. # --------------------------------------------------------------------------- if command -v pacman-key &>/dev/null; then pacman-key --init || echo "WARN: pacman-key --init failed" @@ -377,8 +374,9 @@ fi # The whole bread ecosystem (bakery, bread, breadbar, breadbox, breadcrumbs, # breadpad, bos-settings, breadhelp, ...) is bakery-managed, not pacman: the # binaries and bakery manifest live in /etc/skel/.local (baked in at ISO -# build time from iso/bread-lockfile.toml) and are copied into the user's -# home below, so the install works fully offline with no DNS for bakery. +# build time via build-local.sh's BREAD_BINS array) and are copied into the +# user's home below, so the install works fully offline with no DNS for +# bakery/GitHub. # --------------------------------------------------------------------------- # Deploy dotfiles + the bakery bread ecosystem into the user's home (Calamares diff --git a/iso/airootfs/etc/pacman.conf b/iso/airootfs/etc/pacman.conf index 7e15c7a..20c5242 100644 --- a/iso/airootfs/etc/pacman.conf +++ b/iso/airootfs/etc/pacman.conf @@ -26,21 +26,17 @@ Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist # ----------------------------------------------------------------------- -# Breadway custom repo — breadlock plus AUR republishes the ISO needs -# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin, -# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp -# are NOT here; they are bakery-baked into /etc/skel at ISO build time. +# Breadway custom repo — provides: bakery and the bread ecosystem packages +# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings). +# (calamares comes from the official extra repo, not here.) # # Packages are published to the Forgejo Arch registry (group "os") by the -# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). +# .forgejo/workflows/package.yml workflow in each repo, on tag push. # -# Forgejo's Arch package registry does not serve pacman-compatible db -# signatures. SigLevel = Never is TLS-only integrity: the connection is -# HTTPS (or rewritten to hestia's localhost:3002 in CI). breadlock (PAM) -# rides this repo. Do NOT flip to SigLevel = Required unless a signed db -# has been verified to work — Required without signatures breaks the ISO -# and every install that uses [breadway]. KEYS.asc is the ISO SHA256SUMS -# signing key, not a pacman repo key. +# Forgejo signs the repo db with a key pacman can't look up, so TrustAll +# fails. SigLevel = Never skips verification (acceptable for this private +# repo over TLS). Future improvement: import Forgejo's signing key and +# switch to SigLevel = Required for full package verification. # ----------------------------------------------------------------------- # The section name must match Forgejo's served db filename # ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. diff --git a/iso/airootfs/usr/local/bin/bos-update b/iso/airootfs/usr/local/bin/bos-update index aeb6fbd..53ac3c1 100644 --- a/iso/airootfs/usr/local/bin/bos-update +++ b/iso/airootfs/usr/local/bin/bos-update @@ -2,17 +2,13 @@ # bos-update — update all of BOS in one go. # # BOS packages come from two channels, so a full update touches both: -# 1. pacman — Arch base/desktop + the [breadway] repo (breadlock + AUR -# republishes: calamares, zen-browser-bin, bibata, yay-bin, -# powerlevel10k). [breadway] does NOT provide bos-settings -# or other bakery desktop apps. Every transaction is -# snapshotted by snap-pac; recover via the GRUB "snapshots" -# submenu (grub-btrfs), not `snapper rollback`. +# 1. pacman — Arch base/desktop + the [breadway] repo (bos-settings, etc.). +# Every transaction is snapshotted by snap-pac, so you can roll +# back from the GRUB "snapshots" submenu or BOS Settings. # 2. bakery — the bread ecosystem apps in ~/.local/bin (whatever `bakery list` -# reports as installed — bakery, bread, breadbar, breadbox, -# breadcrumbs, breadpad, breadman, bread-theme, breadpaper, -# breadmon, breadsearch, breadclip, breadshot, bos-settings, -# breadhelp, ...). +# reports as installed — bread, breadbar, breadbox, breadcrumbs, +# breadpad, breadman, bread-theme, breadpaper, breadmon, +# breadsearch, breadclip, breadshot, ...). # # Best-effort: a failure in one channel doesn't abort the other. set -uo pipefail diff --git a/iso/bread-lockfile.toml b/iso/bread-lockfile.toml deleted file mode 100644 index d704c33..0000000 --- a/iso/bread-lockfile.toml +++ /dev/null @@ -1,44 +0,0 @@ -# Bakery binaries baked into the live/installed skel. -# -# build-local.sh and CI (scripts/ci-stage-bakery.py) read this file. A missing -# *required* binary fails the bake: a hollow ISO is worse than a failed build. -# optional_bins are baked when the verified stable index publishes them, and -# skipped with a warning when it does not (today: bread 0.7.0 has no -# bread-emit / bread-module-host). -# -# A flat `bins` list is still accepted and treated as required_bins. -# -# CI populates the builder from the minisign-verified stable bakery index -# (https://dl.breadway.dev/index.json). Local builds still snapshot whatever -# is installed on the builder; this file only names what must / may be present. -# -# Not shipped (even if they appear in the index): breadcast, breadarr. -# breadlock is pacman (see packages.x86_64), not bakery. - -required_bins = [ - "bakery", - "bread", - "breadd", - "breadman", - "breadbar", - "breadbox", - "breadbox-sync", - "breadcrumbs", - "breadpad", - "breadpaper", - "bread-theme", - "breadmon", - "breadsearch", - "breadmill", - "breadclip", - "breadclipd", - "breadshot", - "bos-settings", - "breadhelp", -] - -# Bake if the verified index publishes them; do not fail the ISO if absent. -optional_bins = [ - "bread-emit", - "bread-module-host", -] diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index ad76c71..c8cbb57 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -99,17 +99,12 @@ bluez-utils # blueman: GUI Bluetooth manager (pair/connect devices; breadbar shows status only). blueman -# GTK4 runtime (breadbar, breadbox, breadclip, breadhelp, and other bakery apps) +# GTK4 runtime gtk4 gtk4-layer-shell librsvg libpulse hicolor-icon-theme -# Tauri 2 runtime for bakery-baked bos-settings. Arch's WebKitGTK 4.1 package -# is webkit2gtk-4.1 (libwebkit2gtk-4.1.so); libsoup3 and JavaScriptCore 4.1 -# are pulled in as its dependencies. xdg-desktop-portal comes from the -# Hyprland/GTK portal packages listed above. -webkit2gtk-4.1 # GTK3 dark theme (Adwaita-dark); without this package the gtk-theme-name in # skel settings.ini silently falls back to the light theme for GTK3 apps. gnome-themes-extra @@ -184,16 +179,19 @@ yay-bin # Bread ecosystem. # -# breadlock is the only bread* pacman package here (it needs a root-owned -# /etc/pam.d/breadlock). Everything else — bakery, bread/breadd/bread-emit/ -# bread-module-host, breadbar, breadbox, breadcrumbs, breadpad, breadpaper, -# bread-theme, breadmon, breadsearch, breadclip, breadshot, bos-settings, -# breadhelp — is bakery-managed and baked into /etc/skel/.local at ISO build -# time from iso/bread-lockfile.toml (see build-local.sh). breadcast and -# breadarr are not shipped. bos-settings/breadhelp desktop entries are -# committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime -# deps stay listed even though no bread package depends on them via pacman -# (gtk4, gtk4-layer-shell, webkit2gtk-4.1, iw, libpulse, librsvg, …). +# None of the bread apps (bakery, bread, breadbar, breadbox, breadcrumbs, +# breadpad, bos-settings, breadhelp, ...) are pacman packages here anymore — +# they are all bakery-managed binaries baked into /etc/skel/.local/bin at +# build time (see build-local.sh's BREAD_BINS array), so every user gets the +# exact versions from this laptop's bakery install with no network/DNS +# needed at install or runtime. bos-settings/breadhelp's desktop entries are +# hand-committed static files at +# iso/airootfs/etc/skel/.local/share/applications/, matching the pattern +# already used for breadclip/breadman/breadmon/breadsearch. Their runtime +# system deps are pulled in elsewhere in this list (gtk4, gtk4-layer-shell, +# iw, libpulse, librsvg, networkmanager, openssl, zlib, systemd-libs, +# hicolor-icon-theme) — keep those even though no bread package depends on +# them via pacman. # Input / screen utilities brightnessctl @@ -326,3 +324,6 @@ qt6ct # hyprland.lua) needs these or Qt apps fall back to (blurry) XWayland. qt5-wayland qt6-wayland + +# Dev tools (for bos-settings standalone install) +rustup diff --git a/iso/pacman.conf b/iso/pacman.conf index 7e15c7a..20c5242 100644 --- a/iso/pacman.conf +++ b/iso/pacman.conf @@ -26,21 +26,17 @@ Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist # ----------------------------------------------------------------------- -# Breadway custom repo — breadlock plus AUR republishes the ISO needs -# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin, -# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp -# are NOT here; they are bakery-baked into /etc/skel at ISO build time. +# Breadway custom repo — provides: bakery and the bread ecosystem packages +# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings). +# (calamares comes from the official extra repo, not here.) # # Packages are published to the Forgejo Arch registry (group "os") by the -# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). +# .forgejo/workflows/package.yml workflow in each repo, on tag push. # -# Forgejo's Arch package registry does not serve pacman-compatible db -# signatures. SigLevel = Never is TLS-only integrity: the connection is -# HTTPS (or rewritten to hestia's localhost:3002 in CI). breadlock (PAM) -# rides this repo. Do NOT flip to SigLevel = Required unless a signed db -# has been verified to work — Required without signatures breaks the ISO -# and every install that uses [breadway]. KEYS.asc is the ISO SHA256SUMS -# signing key, not a pacman repo key. +# Forgejo signs the repo db with a key pacman can't look up, so TrustAll +# fails. SigLevel = Never skips verification (acceptable for this private +# repo over TLS). Future improvement: import Forgejo's signing key and +# switch to SigLevel = Required for full package verification. # ----------------------------------------------------------------------- # The section name must match Forgejo's served db filename # ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. diff --git a/packaging/arch/README.md b/packaging/arch/README.md index ce80f7e..4c3df28 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -10,9 +10,8 @@ one publishes on a push to `packaging//**`. Every bread-ecosystem app (bakery, bread, breadbar, breadbox, breadcrumbs, breadpad, breadpaper, breadmon, breadsearch, breadclip, breadshot, bos-settings, breadhelp, ...) is bakery-managed, not pacman-packaged — see -`iso/bread-lockfile.toml` (`required_bins` + `optional_bins`), which -`build-local.sh` uses as the name list when baking this machine's bakery -install into the ISO's `/etc/skel`. +`build-local.sh`'s `BREAD_BINS` array, which bakes this laptop's +bakery-installed binaries into the ISO's `/etc/skel` at build time. `breadlock` is the sole deliberate exception (it needs a root-owned `/etc/pam.d/breadlock` PAM service file, which bakery — by design — has no privileged-install path for) and stays on pacman only; see diff --git a/scripts/ci-stage-bakery.py b/scripts/ci-stage-bakery.py deleted file mode 100755 index 3545f6f..0000000 --- a/scripts/ci-stage-bakery.py +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env python3 -"""Stage bakery artifacts from the verified stable index into $LAPTOP_HOME. - -Used by .forgejo/workflows/release-iso.yml so the ISO bake does not invent -binaries, fake installed.json, or cargo-build bread-theme. Never downloads -breadcast or breadarr. -""" -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import shutil -import subprocess -import sys -import tarfile -import tempfile -import tomllib -import urllib.error -import urllib.request -from datetime import datetime, timezone -from pathlib import Path -from urllib.parse import urljoin, urlparse - -INDEX_URL = "https://dl.breadway.dev/index.json" -# Same key as bread-ecosystem/scripts/get.sh and bakery/src/manifest.rs. -MINISIGN_PUBKEY = "RWTBR8w/IJ+jaylOv80b52DzekKbSR2CvOVGvzB0ipGBaMhJPAOiEWq8" -BLOCKED = frozenset({"breadcast", "breadarr"}) -ARCH_SUFFIXES = ("-x86_64", "-aarch64", "-arm64", "-armv7") - - -def die(msg: str) -> None: - print(f"ERROR: {msg}", file=sys.stderr) - raise SystemExit(1) - - -def dest_name(name: str) -> str: - for suf in ARCH_SUFFIXES: - if name.endswith(suf): - return name[: -len(suf)] - return name - - -def valid_name(name: str) -> bool: - return bool(name) and "/" not in name and name not in (".", "..") - - -def load_lockfile(path: Path) -> tuple[list[str], list[str]]: - with path.open("rb") as f: - data = tomllib.load(f) - required = data.get("required_bins") - optional = data.get("optional_bins") or [] - if required is None: - required = data.get("bins") or data.get("binaries") - if not isinstance(required, list) or not required: - die(f"{path}: missing non-empty required_bins (or bins) list") - if not isinstance(optional, list): - die(f"{path}: optional_bins must be a list") - for label, names in (("required_bins", required), ("optional_bins", optional)): - for b in names: - if not isinstance(b, str) or not valid_name(b): - die(f"{path}: invalid {label} name {b!r}") - if b in BLOCKED: - die(f"{path}: {b} is not shipped on the ISO") - overlap = set(required) & set(optional) - if overlap: - die(f"{path}: bins in both required and optional: {sorted(overlap)}") - return list(required), list(optional) - - -def fetch(url: str, dest: Path) -> None: - dest.parent.mkdir(parents=True, exist_ok=True) - try: - urllib.request.urlretrieve(url, dest) - except (urllib.error.URLError, OSError) as e: - die(f"download failed: {url}: {e}") - - -def sha256_file(path: Path) -> str: - h = hashlib.sha256() - with path.open("rb") as f: - for chunk in iter(lambda: f.read(1024 * 1024), b""): - h.update(chunk) - return h.hexdigest() - - -def require_sha256(value: object, what: str) -> str: - if not isinstance(value, str) or not value.strip(): - die(f"{what}: index sha256 is required and must be non-empty") - return value.strip().lower() - - -def verify_sha256(path: Path, expected: str, what: str) -> None: - actual = sha256_file(path) - if actual != expected: - die(f"{what}: sha256 mismatch (expected {expected}, got {actual})") - - -def version_dir(first_dl_url: str) -> str: - parsed = urlparse(first_dl_url) - parent = parsed.path.rsplit("/", 1)[0] - return f"{parsed.scheme}://{parsed.netloc}{parent}/" - - -def verify_index(index_path: Path, sig_path: Path) -> None: - if shutil.which("minisign") is None: - die("minisign is not installed — refuse to trust an unsigned index") - cmd = [ - "minisign", - "-V", - "-q", - "-m", - str(index_path), - "-x", - str(sig_path), - "-P", - MINISIGN_PUBKEY, - ] - result = subprocess.run(cmd, check=False) - if result.returncode != 0: - die("index.json minisign verification FAILED — refusing to proceed") - print("index.json minisign OK") - - -def bin_index(packages: dict) -> dict[str, tuple[str, dict, dict]]: - out: dict[str, tuple[str, dict, dict]] = {} - for pkg_name, pkg in packages.items(): - if pkg_name in BLOCKED: - continue - for b in pkg.get("binaries") or []: - if not isinstance(b, dict): - continue - raw = b.get("name") - if not isinstance(raw, str): - continue - dest = dest_name(raw) - if dest in BLOCKED or pkg_name in BLOCKED: - continue - if dest in out and out[dest][0] != pkg_name: - die(f"index publishes {dest} from both {out[dest][0]} and {pkg_name}") - out[dest] = (pkg_name, pkg, b) - return out - - -def patch_exec_start(text: str, bin_dir: Path) -> str: - lines = [] - for line in text.splitlines(): - if line.lstrip().startswith("ExecStart="): - rest = line.split("=", 1)[1] - argv = rest.split() - if argv: - name = os.path.basename(argv[0]) - new_path = bin_dir / name - if len(argv) == 1: - line = f"ExecStart={new_path}" - else: - line = f"ExecStart={new_path} {' '.join(argv[1:])}" - lines.append(line) - out = "\n".join(lines) - if text.endswith("\n"): - out += "\n" - return out - - -def wanted_by(text: str) -> list[str]: - targets: list[str] = [] - for line in text.splitlines(): - if line.startswith("WantedBy="): - targets.extend(line.split("=", 1)[1].split()) - return targets or ["default.target"] - - -def assert_safe_archive(path: Path) -> None: - with tarfile.open(path, "r:gz") as tf: - for info in tf.getmembers(): - name = info.name - if info.issym() or info.islnk(): - die(f"refusing archive with symlink entry {name!r}") - if name.startswith("/") or any(p in ("..", "") for p in Path(name).parts if p == ".."): - die(f"refusing archive with unsafe path {name!r}") - if Path(name).is_absolute() or ".." in Path(name).parts: - die(f"refusing archive with unsafe path {name!r}") - - -def stage_file(url: str, dest: Path, sha256: str, what: str, mode: int | None = None) -> None: - fetch(url, dest) - verify_sha256(dest, sha256, what) - if mode is not None: - dest.chmod(mode) - - -def main() -> int: - # CI logs mix stdout/stderr; keep them in source order. - try: - sys.stdout.reconfigure(line_buffering=True) - sys.stderr.reconfigure(line_buffering=True) - except (AttributeError, OSError): - pass - repo = Path(__file__).resolve().parents[1] - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--home", - default=os.environ.get("LAPTOP_HOME", "/build-home"), - help="builder home to populate (default: $LAPTOP_HOME or /build-home)", - ) - parser.add_argument( - "--lockfile", - default=str(repo / "iso" / "bread-lockfile.toml"), - ) - parser.add_argument("--index-url", default=INDEX_URL) - args = parser.parse_args() - - home = Path(args.home) - lockfile = Path(args.lockfile) - if not lockfile.is_file(): - die(f"lockfile missing: {lockfile}") - - required, optional = load_lockfile(lockfile) - print(f"lockfile {lockfile}: {len(required)} required, {len(optional)} optional") - - bin_dir = home / ".local" / "bin" - state_dir = home / ".local" / "state" / "bakery" - cache_dir = home / ".cache" / "bakery" - share_dir = home / ".local" / "share" - unit_dir = home / ".config" / "systemd" / "user" - for d in (bin_dir, state_dir, cache_dir, share_dir, unit_dir): - d.mkdir(parents=True, exist_ok=True) - - index_path = cache_dir / "index.json" - sig_path = cache_dir / "index.json.minisig" - print(f"fetch {args.index_url}") - fetch(args.index_url, index_path) - print(f"fetch {args.index_url}.minisig") - fetch(args.index_url + ".minisig", sig_path) - verify_index(index_path, sig_path) - - with index_path.open() as f: - idx = json.load(f) - packages = idx.get("packages") - if not isinstance(packages, dict): - die("index.json: missing packages object") - - published = bin_index(packages) - selected: dict[str, dict] = {} - installed_bins: dict[str, list[str]] = {} - installed_sha: dict[str, dict[str, str]] = {} - - def take_bin(name: str, *, required_bin: bool) -> bool: - hit = published.get(name) - if hit is None: - if required_bin: - die(f"required bin {name!r} is not in the verified stable index") - print(f"WARN: optional bin {name} not in index — skipping", file=sys.stderr) - return False - pkg_name, pkg, binary = hit - if pkg_name in BLOCKED or name in BLOCKED: - die(f"refusing blocked package/bin {pkg_name}/{name}") - url = binary.get("dl_url") - if not isinstance(url, str) or not url: - die(f"{name}: missing dl_url") - digest = require_sha256(binary.get("sha256"), f"binary {name}") - dest = bin_dir / name - print(f" {name} <- {url}") - stage_file(url, dest, digest, f"binary {name}", mode=0o755) - selected[pkg_name] = pkg - installed_bins.setdefault(pkg_name, []).append(name) - installed_sha.setdefault(pkg_name, {})[name] = digest - return True - - for name in required: - take_bin(name, required_bin=True) - for name in optional: - take_bin(name, required_bin=False) - - if not selected: - die("no packages selected from lockfile ∩ index") - - now = datetime.now(timezone.utc).replace(microsecond=0).isoformat() - installed: dict[str, dict] = {} - - for pkg_name, pkg in sorted(selected.items()): - bins = pkg.get("binaries") or [] - first_url = None - for b in bins: - if isinstance(b, dict) and b.get("dl_url"): - first_url = b["dl_url"] - break - if not first_url: - die(f"{pkg_name}: no binary dl_url to derive version dir") - base = version_dir(first_url) - service_names: list[str] = [] - - for svc in pkg.get("services") or []: - if not isinstance(svc, dict): - die(f"{pkg_name}: service entry must be an object with unit + sha256") - unit = svc.get("unit") - if not isinstance(unit, str) or not valid_name(unit): - die(f"{pkg_name}: invalid service unit {unit!r}") - digest = require_sha256(svc.get("sha256"), f"{pkg_name} {unit}") - dest = unit_dir / unit - url = urljoin(base, unit) - print(f" {unit} <- {url}") - fetch(url, dest) - verify_sha256(dest, digest, f"unit {unit}") - dest.write_text(patch_exec_start(dest.read_text(), bin_dir)) - dest.chmod(0o644) - if svc.get("enable"): - for target in wanted_by(dest.read_text()): - if not valid_name(target): - die(f"{unit}: invalid WantedBy {target!r}") - wants = unit_dir / f"{target}.wants" - wants.mkdir(parents=True, exist_ok=True) - link = wants / unit - if link.exists() or link.is_symlink(): - link.unlink() - link.symlink_to(Path("..") / unit) - print(f" enabled {target}.wants/{unit}") - service_names.append(unit) - - archive = pkg.get("data_archive") - if archive: - if not isinstance(archive, str) or not valid_name(archive): - die(f"{pkg_name}: invalid data_archive {archive!r}") - digest = require_sha256(pkg.get("data_archive_sha256"), f"{pkg_name} {archive}") - url = urljoin(base, archive) - data_dir = share_dir / pkg_name - data_dir.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(prefix=f"bos-{pkg_name}-") as tmp: - tmp_path = Path(tmp) / archive - print(f" {archive} <- {url}") - stage_file(url, tmp_path, digest, f"{pkg_name} {archive}") - assert_safe_archive(tmp_path) - subprocess.run( - [ - "tar", - "xzf", - str(tmp_path), - "--no-same-owner", - "--no-same-permissions", - "-C", - str(data_dir), - ], - check=True, - ) - print(f" extracted to {data_dir}") - - desktop = pkg.get("desktop_file") - if desktop: - if not isinstance(desktop, str) or not valid_name(desktop): - die(f"{pkg_name}: invalid desktop_file {desktop!r}") - digest = require_sha256(pkg.get("desktop_file_sha256"), f"{pkg_name} {desktop}") - dest = share_dir / "applications" / f"{pkg_name}.desktop" - stage_file(urljoin(base, desktop), dest, digest, f"{pkg_name} {desktop}") - - license_file = pkg.get("license_file") - if license_file: - if not isinstance(license_file, str) or not valid_name(license_file): - die(f"{pkg_name}: invalid license_file {license_file!r}") - digest = require_sha256(pkg.get("license_file_sha256"), f"{pkg_name} {license_file}") - dest = share_dir / "licenses" / pkg_name / "LICENSE" - stage_file(urljoin(base, license_file), dest, digest, f"{pkg_name} {license_file}") - - installed[pkg_name] = { - "name": pkg_name, - "version": pkg.get("version"), - "binaries": installed_bins.get(pkg_name, []), - "services": service_names, - "installed_at": now, - "track": "stable", - "binary_sha256": installed_sha.get(pkg_name, {}), - } - - if "breadhelp" in installed: - content = share_dir / "breadhelp" / "content" - if not content.is_dir(): - die(f"breadhelp data_archive did not produce {content}") - - state_path = state_dir / "installed.json" - state_path.write_text(json.dumps({"track": "stable", "packages": installed}, indent=2) + "\n") - print(f"installed.json written ({len(installed)} packages): {', '.join(sorted(installed))}") - print(f"staged bins: {', '.join(sorted(p.name for p in bin_dir.iterdir() if p.is_file()))}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/ci-verify-bake.sh b/scripts/ci-verify-bake.sh deleted file mode 100755 index 1b01cab..0000000 --- a/scripts/ci-verify-bake.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -# Read-only checks that a builder home (and optionally a staged skel) has -# everything build-local.sh needs before mkarchiso. Exit non-zero on failure. -# -# LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh -# SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh -set -euo pipefail - -REPO="$(cd "$(dirname "$0")/.." && pwd)" -LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}" -LAPTOP_HOME="${LAPTOP_HOME:-/build-home}" -SKEL="${SKEL:-}" - -pass=0 -fail=0 -ok() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); } -bad() { printf ' FAIL %s\n' "$1" >&2; fail=$((fail + 1)); } - -if [[ ! -f "$LOCKFILE" ]]; then - echo "ERROR: lockfile missing: $LOCKFILE" >&2 - exit 1 -fi - -eval "$(python3 - "$LOCKFILE" <<'PY' -import sys, tomllib -path = sys.argv[1] -with open(path, "rb") as f: - data = tomllib.load(f) -required = data.get("required_bins") -optional = data.get("optional_bins") or [] -if required is None: - required = data.get("bins") or data.get("binaries") or [] -def emit(name, values): - print(f"{name}=(") - for v in values: - print(f" {v!r}") - print(")") -emit("REQUIRED_BINS", required) -emit("OPTIONAL_BINS", optional) -PY -)" - -echo "== lockfile $LOCKFILE ==" -echo " ${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional" -echo "== builder home $LAPTOP_HOME ==" - -check_exec() { - local path="$1" label="$2" - if [[ -x "$path" && -f "$path" ]]; then - ok "$label executable: $path" - else - bad "$label missing or not executable: $path" - fi -} - -check_dir() { - local path="$1" label="$2" - if [[ -d "$path" ]]; then - ok "$label: $path" - else - bad "$label missing: $path" - fi -} - -check_file() { - local path="$1" label="$2" - if [[ -f "$path" ]]; then - ok "$label: $path" - else - bad "$label missing: $path" - fi -} - -for b in "${REQUIRED_BINS[@]}"; do - check_exec "$LAPTOP_HOME/.local/bin/$b" "required bin $b" -done -for b in "${OPTIONAL_BINS[@]}"; do - if [[ -x "$LAPTOP_HOME/.local/bin/$b" ]]; then - ok "optional bin $b present" - else - printf ' ---- optional bin %s not staged (ok until bread ships it)\n' "$b" - fi -done - -check_dir "$LAPTOP_HOME/.local/share/breadhelp/content" "breadhelp content" -check_file "$LAPTOP_HOME/.cache/bakery/index.json" "bakery index cache" -check_file "$LAPTOP_HOME/.local/state/bakery/installed.json" "bakery installed.json" - -mapfile -t UNITS < <(python3 - "$LAPTOP_HOME/.local/state/bakery/installed.json" <<'PY' -import json, sys -path = sys.argv[1] -with open(path) as f: - data = json.load(f) -pkgs = data.get("packages", data) -for pkg in pkgs.values(): - for s in pkg.get("services", []): - print(s["unit"] if isinstance(s, dict) else s) -PY -) -if [[ ${#UNITS[@]} -eq 0 ]]; then - bad "installed.json lists no service units" -else - for unit in "${UNITS[@]}"; do - [[ -n "$unit" ]] || continue - check_file "$LAPTOP_HOME/.config/systemd/user/$unit" "unit $unit" - done -fi - -if [[ -n "$SKEL" ]]; then - echo "== staged skel $SKEL ==" - for b in "${REQUIRED_BINS[@]}"; do - check_exec "$SKEL/.local/bin/$b" "skel required bin $b" - done - check_dir "$SKEL/.local/share/breadhelp/content" "skel breadhelp content" - check_file "$SKEL/.cache/bakery/index.json" "skel bakery index cache" - for unit in "${UNITS[@]}"; do - [[ -n "$unit" ]] || continue - if [[ -f "$SKEL/.config/systemd/user/$unit" ]]; then - ok "skel unit $unit" - else - bad "skel unit missing: $SKEL/.config/systemd/user/$unit" - fi - done -fi - -echo -printf 'Result: %d passed, %d failed\n' "$pass" "$fail" -[[ "$fail" -eq 0 ]] diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index ab70559..6672447 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -49,21 +49,13 @@ echo "== bread ecosystem on PATH ==" for bin in bakery bread breadd breadbar breadbox breadbox-sync breadcrumbs breadpad breadman; do check "$bin found" "command -v $bin" done -for bin in bread-emit bread-module-host; do - if command -v "$bin" >/dev/null 2>&1; then - ok "$bin found" - else - note "$bin not on PATH (optional until stable bread ships it)" - fi -done echo "== bos-settings ==" check "bos-settings installed" "command -v bos-settings" echo "== breadhelp ==" check "breadhelp installed" "command -v breadhelp" -check "breadhelp content installed" \ - "[ -d \"\$HOME/.local/share/breadhelp/content\" ] || [ -d /etc/skel/.local/share/breadhelp/content ]" +check "breadhelp content installed" "[ -d /usr/share/breadhelp/content ]" check "bos-netcheck present" "command -v bos-netcheck" echo "== default dotfiles =="