Compare commits
No commits in common. "a3ead6607a3fb8d3949281b72b86d6b40a199dc4" and "881ac41cbb5d399350569b9d6c10b2d17fd4b55c" have entirely different histories.
a3ead6607a
...
881ac41cbb
18 changed files with 317 additions and 1119 deletions
|
|
@ -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
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -45,7 +45,3 @@ logs/
|
|||
|
||||
# Local hygiene notes (not for commit)
|
||||
CLAUDE.md
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
55
AGENTS.md
55
AGENTS.md
|
|
@ -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.
|
||||
165
DESIGN.md
165
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 <pkg>` |
|
||||
| | Hyprland | "Open config in editor" + monitor list from `bread.state.monitors()` |
|
||||
|
||||
### Config loading pattern
|
||||
|
||||
Each view has a dedicated `load_config(path) -> Result<T>` 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 <N>` → notify user to reboot
|
||||
- Delete: `snapper delete <N>`
|
||||
- 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
|
||||
|
|
|
|||
192
README.md
192
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-<date>-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):**
|
||||
|
||||
|
|
|
|||
223
build-local.sh
223
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/<pkg>/ 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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "<section>.db" from Server.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "<section>.db" from Server.
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ one publishes on a push to `packaging/<name>/**`.
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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 ]]
|
||||
|
|
@ -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 =="
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue