iso: bake bakery apps into /usr/local

BOS opts in to bakery's system prefix so desktop apps live on @
and ride snapper/grub-btrfs snapshots. The builder home stays
~/.local; build-local.sh copies bins, share/data, and user units
onto the image. Per-user installed.json and the index cache stay
in skel. Recovery is still grub-btrfs, not snapper rollback.
This commit is contained in:
Breadway 2026-08-16 00:10:32 +08:00
parent 744f18cd90
commit 34043086b9
19 changed files with 218 additions and 103 deletions

View file

@ -52,4 +52,7 @@ There is no `dev` integration branch.
- Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs - Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs
work does not require it. work does not require it.
- Don't tell users to `snapper rollback` blindly; GRUB pins - Don't tell users to `snapper rollback` blindly; GRUB pins
`rootflags=subvol=@`. Recovery is grub-btrfs reboot. `rootflags=subvol=@`. Recovery is grub-btrfs reboot. Bakery desktop
apps on BOS are system-prefix `/usr/local` (`/etc/bakery/config.toml`);
snapper `@` snapshots include them. Do not move those bits back to
`~/.local` on the image (hermes / default bakery stay user-layout).

View file

@ -14,8 +14,9 @@ wiring up dotfiles, no per-tool bakery installs.
- **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated - **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated
keybinds, snappy animations, blur, and pywal-driven colours on a black base. keybinds, snappy animations, blur, and pywal-driven colours on a black base.
- **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries - **bread ecosystem**, baked into `/usr/local` from bakery-managed binaries
(no network needed at install time): the `bread`/`breadd` automation daemon (no network needed at install time; per-user bakery state is seeded in
`/etc/skel`): the `bread`/`breadd` automation daemon
(`bread-emit` / `bread-module-host` when the stable bread release publishes (`bread-emit` / `bread-module-host` when the stable bread release publishes
them), `breadbar` (status bar + notifications), `breadbox` (launcher), them), `breadbar` (status bar + notifications), `breadbox` (launcher),
`breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles), `breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles),
@ -58,7 +59,7 @@ wiring up dotfiles, no per-tool bakery installs.
| Channel | What | | 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, 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 `/usr/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 | | **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, …) | | **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) |
| **Not shipped** | `breadcast`, `breadarr` | | **Not shipped** | `breadcast`, `breadarr` |
@ -123,9 +124,15 @@ Push `origin` (and `github` when mirroring). Do not treat origin as GitHub.
## Building the ISO ## Building the ISO
`build-local.sh` builds the image natively (no container) and bakes this `build-local.sh` builds the image natively (no container) and copies this
machine's bakery-installed bread binaries + breadhelp content into machine's bakery-installed bread binaries + breadhelp content from the
`/etc/skel`: builder's `~/.local` into the image at `/usr/local` (bins, share/data,
desktop files, licenses) and `/usr/lib/systemd/user` (units). Per-user
bakery state (`installed.json` + index cache) is seeded in `/etc/skel`.
BOS opts in via `/etc/bakery/config.toml` (`prefix = "/usr/local"`);
default bakery without that file is still `~/.local`. Snapper `@`
snapshots include `/usr/local`; recovery is still grub-btrfs, not
`snapper rollback`.
```sh ```sh
sudo ./build-local.sh # release-quality (xz squashfs) sudo ./build-local.sh # release-quality (xz squashfs)
@ -190,8 +197,8 @@ Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to
avoid memory pressure. avoid memory pressure.
Post-install, `scripts/smoke-test.sh` (run as the installed user) checks Post-install, `scripts/smoke-test.sh` (run as the installed user) checks
subvolumes, services, bakery bins, and breadhelp content under subvolumes, services, bakery bins on PATH, and breadhelp content under
`~/.local/share/breadhelp/content`. `/usr/local/share/breadhelp/content`.
## bos-settings ## bos-settings
@ -234,7 +241,7 @@ repo, not here.
## The bread ecosystem ## The bread ecosystem
Everything below is a separate bakery-distributed project with its own repo 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 and release cadence, baked into `/usr/local` at ISO build time so a fresh
install has them all with no network round-trip. Some ship more than one 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 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 corresponding **bos-settings** panel; this table is about *using* the app
@ -258,7 +265,7 @@ directly.
| `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` | | `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` | | `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 | | `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+/` | | `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `/usr/local/share/breadhelp/content` (bakery `content.tar.gz`, baked into the image). | `SUPER+/` |
**System** **System**

View file

@ -41,13 +41,15 @@ if [ "${FAST_BUILD:-0}" = "1" ]; then
fi fi
grep airootfs_image_tool_options "$STAGE/profiledef.sh" grep airootfs_image_tool_options "$STAGE/profiledef.sh"
# --- Bake this machine's bakery-installed bread ecosystem into /etc/skel ------ # --- Bake this machine's bakery-installed bread ecosystem into the image ------
# The bread desktop apps are bakery-managed (release binaries from # The bread desktop apps are bakery-managed (release binaries from
# dl.breadway.dev / GitHub), not pacman. bakery needs DNS at install time, # 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 # 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 # 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 # has. Builder home stays user-layout (~/.local); the *image* is system-prefix
# offline. Copied at build time so the binaries never bloat the git repo. # /usr/local so apps live on @ and ride snapper/grub-btrfs snapshots.
# installed.json + index cache stay per-user in skel. Copied at build time
# so the binaries never bloat the git repo.
# #
# CI should prefer the stable bakery index when populating the builder home. # CI should prefer the stable bakery index when populating the builder home.
# Local builds still snapshot the builder. required_bins fail the bake if # Local builds still snapshot the builder. required_bins fail the bake if
@ -97,9 +99,14 @@ BAKERY_BIN="$LAPTOP_HOME/.local/bin"
BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery" BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery"
BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery" BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery"
BAKERY_SHARE="$LAPTOP_HOME/.local/share" BAKERY_SHARE="$LAPTOP_HOME/.local/share"
SKEL="$STAGE/airootfs/etc/skel" AIROOTFS="$STAGE/airootfs"
IMAGE_BIN="$AIROOTFS/usr/local/bin"
IMAGE_SHARE="$AIROOTFS/usr/local/share"
IMAGE_UNITS="$AIROOTFS/usr/lib/systemd/user"
SKEL="$AIROOTFS/etc/skel"
echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ===" echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ==="
echo "lockfile: $LOCKFILE (${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional)" echo "lockfile: $LOCKFILE (${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional)"
echo "image prefix: /usr/local (bins $IMAGE_BIN, share $IMAGE_SHARE, units $IMAGE_UNITS)"
missing=() missing=()
for b in "${REQUIRED_BINS[@]}"; do for b in "${REQUIRED_BINS[@]}"; do
@ -124,9 +131,9 @@ for b in "${OPTIONAL_BINS[@]}"; do
fi fi
done done
install -d -m 0755 "$SKEL/.local/bin" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery" install -d -m 0755 "$IMAGE_BIN" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery"
for b in "${BREAD_BINS[@]}"; do for b in "${BREAD_BINS[@]}"; do
install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b" install -m 0755 "$BAKERY_BIN/$b" "$IMAGE_BIN/$b"
done done
# Drop packages that are not in the lockfile (breadcast/breadarr must not # Drop packages that are not in the lockfile (breadcast/breadarr must not
@ -164,26 +171,27 @@ if [[ ! -f "$BAKERY_CACHE/index.json" ]]; then
exit 1 exit 1
fi fi
install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json" install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json"
echo "baked bins: $(ls "$SKEL/.local/bin")" echo "baked bins: $(ls "$IMAGE_BIN")"
# --- Bake bakery data dirs the apps need offline ------------------------------ # --- Bake bakery data dirs the apps need offline ------------------------------
# bakery extracts data_archive (breadhelp's content.tar.gz) to # bakery extracts data_archive (breadhelp's content.tar.gz) to
# ~/.local/share/<pkg>/ and writes desktop entries + licenses next to it. # $prefix/share/<pkg>/ and writes desktop entries + licenses next to it.
# Copy those — never laptop-local state (clipboard history, WebKit cache, # Builder home is still ~/.local/share; copy into the image at
# bread sync-repo, models). # /usr/local/share. Never laptop-local state (clipboard history, WebKit
echo "=== baking bakery share/data into skel ===" # cache, bread sync-repo, models).
echo "=== baking bakery share/data into /usr/local/share ==="
BREADHELP_CONTENT="$BAKERY_SHARE/breadhelp/content" BREADHELP_CONTENT="$BAKERY_SHARE/breadhelp/content"
if [[ ! -d "$BREADHELP_CONTENT" ]]; then if [[ ! -d "$BREADHELP_CONTENT" ]]; then
echo "ERROR: breadhelp content missing: $BREADHELP_CONTENT" >&2 echo "ERROR: breadhelp content missing: $BREADHELP_CONTENT" >&2
echo "bakery installs this from content.tar.gz into ~/.local/share/breadhelp/content" >&2 echo "bakery installs this from content.tar.gz into ~/.local/share/breadhelp/content on the builder" >&2
echo "A breadhelp binary without content is a hollow ISO." >&2 echo "A breadhelp binary without content is a hollow ISO." >&2
exit 1 exit 1
fi fi
install -d -m 0755 "$SKEL/.local/share" install -d -m 0755 "$IMAGE_SHARE"
cp -a "$BAKERY_SHARE/breadhelp" "$SKEL/.local/share/breadhelp" cp -a "$BAKERY_SHARE/breadhelp" "$IMAGE_SHARE/breadhelp"
echo " baked $SKEL/.local/share/breadhelp/content" echo " baked $IMAGE_SHARE/breadhelp/content"
python3 - "$BAKERY_CACHE/index.json" "$BAKERY_SHARE" "$SKEL/.local/share" "${BREAD_BINS[@]}" <<'PY' python3 - "$BAKERY_CACHE/index.json" "$BAKERY_SHARE" "$IMAGE_SHARE" "${BREAD_BINS[@]}" <<'PY'
import json, os, shutil, sys import json, os, shutil, sys
index_path, src_share, dest_share, *bins = sys.argv[1:] index_path, src_share, dest_share, *bins = sys.argv[1:]
wanted = set(bins) wanted = set(bins)
@ -242,14 +250,16 @@ PY
# silently left out, so those daemons never start on a fresh install/live # silently left out, so those daemons never start on a fresh install/live
# boot until the user re-runs `bakery install` (which needs network). # boot until the user re-runs `bakery install` (which needs network).
# Source of truth is the *filtered* installed.json we just wrote: only # Source of truth is the *filtered* installed.json we just wrote: only
# lockfile packages. Copy each unit with ExecStart rewritten from this # lockfile packages. Units go to /usr/lib/systemd/user with ExecStart
# laptop's literal home path to the portable `%h` specifier, and recreate # rewritten to /usr/local/bin (not %h/.local/bin). Recreate whichever
# whichever *.target.wants enable symlink bakery created locally. Units # *.target.wants enable symlink bakery created locally (or that skel
# already committed by hand (breadd.service carries a # already ships). Hand-committed skel units (breadd.service carries a
# RuntimeDirectoryPreserve=yes fix not yet upstreamed) are left alone. # RuntimeDirectoryPreserve=yes fix not yet upstreamed) are the source
echo "=== baking bakery service units into skel ===" # for that unit and also get their ExecStart rewritten in skel.
echo "=== baking bakery service units into /usr/lib/systemd/user ==="
SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user" SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user"
SKEL_SYSTEMD="$SKEL/.config/systemd/user" SKEL_SYSTEMD="$SKEL/.config/systemd/user"
install -d -m 0755 "$IMAGE_UNITS"
mapfile -t SERVICE_UNITS < <(python3 - "$SKEL/.local/state/bakery/installed.json" <<'PY' mapfile -t SERVICE_UNITS < <(python3 - "$SKEL/.local/state/bakery/installed.json" <<'PY'
import json, sys import json, sys
with open(sys.argv[1]) as f: with open(sys.argv[1]) as f:
@ -259,40 +269,72 @@ for pkg in d.get("packages", d).values():
print(s["unit"] if isinstance(s, dict) else s) print(s["unit"] if isinstance(s, dict) else s)
PY PY
) )
rewrite_exec_start() {
local src="$1" dest="$2"
python3 - "$src" "$dest" <<'PY'
import os, sys
src, dest = sys.argv[1], sys.argv[2]
text = open(src).read()
lines = []
for line in text.splitlines():
if line.lstrip().startswith("ExecStart="):
key, rest = line.split("=", 1)
argv = rest.split()
if argv:
name = os.path.basename(argv[0])
argv[0] = "/usr/local/bin/" + name
line = key + "=" + " ".join(argv)
lines.append(line)
out = "\n".join(lines)
if text.endswith("\n"):
out += "\n"
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "w") as f:
f.write(out)
PY
}
for unit in "${SERVICE_UNITS[@]}"; do for unit in "${SERVICE_UNITS[@]}"; do
[[ -n "$unit" ]] || continue [[ -n "$unit" ]] || continue
if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then
echo " $unit already committed in skel, leaving as-is" src="$SKEL_SYSTEMD/$unit"
continue echo " $unit using committed skel unit as source"
else
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 an image whose daemons will never start." >&2
exit 1
fi
fi fi
src="$SYSTEMD_USER_DIR/$unit" rewrite_exec_start "$src" "$IMAGE_UNITS/$unit"
if [[ ! -f "$src" ]]; then if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then
echo "ERROR: $unit listed in bakery installed.json but not found at $src" >&2 rewrite_exec_start "$src" "$SKEL_SYSTEMD/$unit"
echo "Refusing to bake a skel whose daemons will never start." >&2
exit 1
fi fi
install -d -m 0755 "$SKEL_SYSTEMD" for base in "$SYSTEMD_USER_DIR" "$SKEL_SYSTEMD"; do
sed "s#ExecStart=$LAPTOP_HOME/.local/bin/#ExecStart=%h/.local/bin/#" "$src" > "$SKEL_SYSTEMD/$unit" [[ -d "$base" ]] || continue
for wants_dir in "$SYSTEMD_USER_DIR"/*.target.wants; do for wants_dir in "$base"/*.target.wants; do
[[ -L "$wants_dir/$unit" ]] || continue [[ -e "$wants_dir" || -L "$wants_dir" ]] || continue
target_name="$(basename "$wants_dir")" [[ -L "$wants_dir/$unit" ]] || continue
install -d -m 0755 "$SKEL_SYSTEMD/$target_name" target_name="$(basename "$wants_dir")"
ln -sf "../$unit" "$SKEL_SYSTEMD/$target_name/$unit" install -d -m 0755 "$IMAGE_UNITS/$target_name"
ln -sf "../$unit" "$IMAGE_UNITS/$target_name/$unit"
done
done done
echo " baked $unit" echo " baked $unit -> $IMAGE_UNITS/$unit"
done done
# mkarchiso resets every airootfs file to 0644, so executables must be declared # mkarchiso resets every airootfs file to 0644, so executables must be declared
# in profiledef.sh's file_permissions array or they ship non-executable and the # in profiledef.sh's file_permissions array or they ship non-executable and the
# exec-once launches fail with "permission denied". Inject a 0755 entry for each # exec-once launches fail with "permission denied". Inject a 0755 entry for each
# baked binary right after the array opener (keeps the binary list in one place). # baked bakery binary right after the array opener (bos-* bins are already
# listed; keeps the bakery list in one place — the lockfile).
perm_file="$(mktemp)" perm_file="$(mktemp)"
for b in "${BREAD_BINS[@]}"; do for b in "${BREAD_BINS[@]}"; do
printf ' ["/etc/skel/.local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file" printf ' ["/usr/local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file"
done done
sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh" sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh"
rm -f "$perm_file" rm -f "$perm_file"
echo "=== file_permissions after injection ==="; grep -A14 '^file_permissions=(' "$STAGE/profiledef.sh" echo "=== file_permissions after injection ==="; grep -A40 '^file_permissions=(' "$STAGE/profiledef.sh"
# Pin one timestamp for the whole build. Without this, mkarchiso derives the # Pin one timestamp for the whole build. Without this, mkarchiso derives the
# boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID # boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID

View file

@ -24,7 +24,9 @@ submenu** (grub-btrfs), not `snapper rollback`.
BOS GRUB pins `rootflags=subvol=@`. `snapper rollback` swaps the default BOS GRUB pins `rootflags=subvol=@`. `snapper rollback` swaps the default
subvolume; the installed `grub.cfg` will still boot `@`. Pick the grub-btrfs subvolume; the installed `grub.cfg` will still boot `@`. Pick the grub-btrfs
entry so the kernel command line matches the snapshot you want. entry so the kernel command line matches the snapshot you want. Bakery
desktop apps live under `/usr/local` on `@`, so those same snapshots
include them.
If the system will not boot (lost EFI entry / broken GRUB), boot the live If the system will not boot (lost EFI entry / broken GRUB), boot the live
ISO and run `sudo bos-rescue`. It mounts `@` + the ESP and offers the same ISO and run `sudo bos-rescue`. It mounts `@` + the ESP and offers the same

View file

@ -0,0 +1,2 @@
# Bakery desktop apps live under /usr/local so they ride snapper @ snapshots.
prefix = "/usr/local"

View file

@ -425,9 +425,10 @@ if command -v ufw &>/dev/null; then
fi fi
# The whole bread ecosystem (bakery, bread, breadbar, breadbox, breadcrumbs, # The whole bread ecosystem (bakery, bread, breadbar, breadbox, breadcrumbs,
# breadpad, bos-settings, breadhelp, ...) is bakery-managed, not pacman: the # breadpad, bos-settings, breadhelp, ...) is bakery-managed, not pacman:
# binaries and bakery manifest live in /etc/skel/.local (baked in at ISO # binaries, share/data, and user units are baked into /usr/local and
# build time from iso/bread-lockfile.toml) and are copied into the user's # /usr/lib/systemd/user (system prefix). Per-user bakery state (installed.json
# + index cache) is seeded from /etc/skel/.local and copied into the user's
# home below, so the install works fully offline with no DNS for bakery. # home below, so the install works fully offline with no DNS for bakery.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -7,8 +7,9 @@
# alongside BOS's own bos.desktop, and breadgreet's session picker matches by # alongside BOS's own bos.desktop, and breadgreet's session picker matches by
# .desktop file stem — with no override it picks "hyprland.desktop" over # .desktop file stem — with no override it picks "hyprland.desktop" over
# "bos.desktop", which skips bos-session's PATH fixup (adds ~/.local/bin for # "bos.desktop", which skips bos-session's PATH fixup (adds ~/.local/bin for
# the bakery bread apps; greetd starts no login shell, so /etc/profile.d is # per-user tools; bakery apps are in /usr/local/bin). greetd starts no login
# never sourced any other way). Confirmed via breadgreet's own test suite # shell, so /etc/profile.d is never sourced any other way. Confirmed via
# breadgreet's own test suite
# (sessions.rs: discover_prefers_configured_default_over_first_entry). # (sessions.rs: discover_prefers_configured_default_over_first_entry).
[sessions] [sessions]

View file

@ -29,7 +29,7 @@ Include = /etc/pacman.d/mirrorlist
# Breadway custom repo — breadlock plus AUR republishes the ISO needs # Breadway custom repo — breadlock plus AUR republishes the ISO needs
# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin, # (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin,
# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp # zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp
# are NOT here; they are bakery-baked into /etc/skel at ISO build time. # are NOT here; they are bakery-baked into /usr/local at ISO build time.
# #
# Packages are published to the Forgejo Arch registry (group "os") by the # Packages are published to the Forgejo Arch registry (group "os") by the
# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). # .forgejo/workflows/*.yml workflows in this repo (and breadlock's).

View file

@ -1,8 +1,8 @@
# Put the per-user bakery bin dir on PATH. The bread ecosystem (breadd, breadbar, # Keep ~/.local/bin on PATH for per-user tools. Arch already includes
# breadbox, …) is installed there by bakery, and the Hyprland session launches # /usr/local/bin (where bakery desktop apps live on BOS). The Hyprland
# them via `exec-once`, which resolves against the PATH it inherits from the # session resolves exec-once against the PATH it inherits from the login
# login shell. Arch's stock /etc/profile does not add ~/.local/bin, so do it here # shell; Arch's stock /etc/profile does not add ~/.local/bin, so do it
# for every login shell (live user and installed user alike). # here for every login shell (live user and installed user alike).
case ":$PATH:" in case ":$PATH:" in
*":$HOME/.local/bin:"*) ;; *":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;; *) export PATH="$HOME/.local/bin:$PATH" ;;

View file

@ -124,8 +124,8 @@ hl.on("hyprland.start", function()
"gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Ice", "gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Ice",
"gsettings set org.gnome.desktop.interface cursor-size 24", "gsettings set org.gnome.desktop.interface cursor-size 24",
-- Clipboard history is breadclipd, a bakery-managed systemd --user -- Clipboard history is breadclipd, a bakery-managed systemd --user
-- service (auto-started via skel — see build-local.sh's service bake) -- service (auto-started from /usr/lib/systemd/user — see
-- rather than an exec-once here. -- build-local.sh's service bake) rather than an exec-once here.
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1", "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"awww-daemon", "awww-daemon",
-- Set the default wallpaper once the daemon is up (retry until ready). -- Set the default wallpaper once the daemon is up (retry until ready).
@ -140,8 +140,8 @@ hl.on("hyprland.start", function()
-- breadpaper reads) is baked into skel too, right beside colors.json. -- breadpaper reads) is baked into skel too, right beside colors.json.
-- pywal only runs for real once the user picks a wallpaper themselves. -- pywal only runs for real once the user picks a wallpaper themselves.
[[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']], [[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']],
-- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service, -- breadd runs as a systemd user service (/usr/lib/systemd/user/breadd.service,
-- enabled in skel). It autostarts at login but before Hyprland exists, so -- plus a skel copy). It autostarts at login but before Hyprland exists, so
-- push the compositor's Wayland env into the user manager and restart breadd -- push the compositor's Wayland env into the user manager and restart breadd
-- to pick it up — that's how it gets HYPRLAND_INSTANCE_SIGNATURE to talk to Hyprland. -- to pick it up — that's how it gets HYPRLAND_INSTANCE_SIGNATURE to talk to Hyprland.
"dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE", "dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE",
@ -162,7 +162,8 @@ hl.on("hyprland.start", function()
-- breadbox-sync is a Type=oneshot systemd --user service -- breadbox-sync is a Type=oneshot systemd --user service
-- (WantedBy=default.target, no Hyprland IPC dependency) — it already -- (WantedBy=default.target, no Hyprland IPC dependency) — it already
-- runs on login via the unit baked into skel, independent of this list. -- runs on login via the unit baked into /usr/lib/systemd/user,
-- independent of this list.
local ok, extra = pcall(function() local ok, extra = pcall(function()
return dofile(script_dir .. "system/autostart.lua")() return dofile(script_dir .. "system/autostart.lua")()
end) end)

View file

@ -3,8 +3,8 @@ Description=Bread Runtime Daemon
[Service] [Service]
Type=simple Type=simple
# %h = the user's home — works for any account created from this skel. # System-prefix bakery install — same path for every account.
ExecStart=%h/.local/bin/breadd ExecStart=/usr/local/bin/breadd
Restart=on-failure Restart=on-failure
RestartSec=2 RestartSec=2
UMask=0077 UMask=0077

View file

@ -89,7 +89,7 @@ alias alt-install='yay -S'
alias alt-uninstall='yay -R' alias alt-uninstall='yay -R'
alias alt-srchpkg='yay -Ss' alias alt-srchpkg='yay -Ss'
# ~/.local/bin holds the bread* binaries baked in at build time. # Per-user tools. Bakery desktop apps live in /usr/local/bin (already on PATH).
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
# Powerlevel10k prompt configuration. # Powerlevel10k prompt configuration.

View file

@ -2,11 +2,10 @@
# BOS graphical session launcher, run by greetd on the INSTALLED system after # BOS graphical session launcher, run by greetd on the INSTALLED system after
# the user authenticates (see /etc/greetd/config.toml). # the user authenticates (see /etc/greetd/config.toml).
# #
# greetd does not start a login shell, so /etc/profile.d is never sourced — which # greetd does not start a login shell, so /etc/profile.d is never sourced.
# means ~/.local/bin (where bakery installs the bread ecosystem: breadd, breadbar, # Bakery desktop apps live in /usr/local/bin (already on Arch PATH). Source
# breadbox-sync, …) would be missing from PATH and the Hyprland `exec-once` # the login profile here so ~/.local/bin (per-user tools) is also on PATH,
# launches would fail. Source the login profile here so PATH is correct, set the # set the Wayland session hints, then hand off to Hyprland.
# Wayland session hints, then hand off to Hyprland.
# #
# Launched via start-hyprland (ships with the hyprland package) rather than the # Launched via start-hyprland (ships with the hyprland package) rather than the
# raw Hyprland binary — Hyprland upstream no longer recommends exec'ing it # raw Hyprland binary — Hyprland upstream no longer recommends exec'ing it

View file

@ -8,11 +8,12 @@
# or other bakery desktop apps. Every transaction is # or other bakery desktop apps. Every transaction is
# snapshotted by snap-pac; recover via the GRUB "snapshots" # snapshotted by snap-pac; recover via the GRUB "snapshots"
# submenu (grub-btrfs), not `snapper rollback`. # submenu (grub-btrfs), not `snapper rollback`.
# 2. bakery — the bread ecosystem apps in ~/.local/bin (whatever `bakery list` # 2. bakery — the bread ecosystem apps in /usr/local (whatever `bakery list`
# reports as installed — bakery, bread, breadbar, breadbox, # reports as installed — bakery, bread, breadbar, breadbox,
# breadcrumbs, breadpad, breadman, bread-theme, breadpaper, # breadcrumbs, breadpad, breadman, bread-theme, breadpaper,
# breadmon, breadsearch, breadclip, breadshot, bos-settings, # breadmon, breadsearch, breadclip, breadshot, bos-settings,
# breadhelp, ...). # breadhelp, ...). Those bits live on @ and ride snapper
# root snapshots; recover via grub-btrfs, not `snapper rollback`.
# #
# Best-effort: a failure in one channel doesn't abort the other. # Best-effort: a failure in one channel doesn't abort the other.
set -uo pipefail set -uo pipefail
@ -20,12 +21,11 @@ set -uo pipefail
bold() { printf '\033[1m%s\033[0m\n' "$1"; } bold() { printf '\033[1m%s\033[0m\n' "$1"; }
# Timed snapper pre snapshot before either channel. snap-pac already # Timed snapper pre snapshot before either channel. snap-pac already
# snapshots root around pacman; bakery writes ~/.local/bin ($HOME / @home), # snapshots root around pacman; bakery now writes /usr/local (on @), so
# which is outside that root snapshot. This extra snapshot is still # that root snapshot includes the desktop apps. This extra snapshot is
# best-effort and covers bakery $HOME updates as well as possible — a # still best-effort — a home config if the installer created one (user
# home config if the installer created one, otherwise the root timeline # bakery state), plus a root timeline around the whole update. Never
# around the whole update. Never fail the update if snapper is missing # fail the update if snapper is missing or the create errors.
# or the create errors.
if command -v snapper >/dev/null; then if command -v snapper >/dev/null; then
if snapper -c home list >/dev/null 2>&1; then if snapper -c home list >/dev/null 2>&1; then
snapper -c home create -t pre -c number \ snapper -c home create -t pre -c number \

View file

@ -1,4 +1,4 @@
# Bakery binaries baked into the live/installed skel. # Bakery binaries baked into the live/installed image at /usr/local.
# #
# build-local.sh and CI (scripts/ci-stage-bakery.py) read this file. A missing # 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. # *required* binary fails the bake: a hollow ISO is worse than a failed build.

View file

@ -206,10 +206,10 @@ yay-bin
# /etc/pam.d/breadlock). Everything else — bakery, bread/breadd/bread-emit/ # /etc/pam.d/breadlock). Everything else — bakery, bread/breadd/bread-emit/
# bread-module-host, breadbar, breadbox, breadcrumbs, breadpad, breadpaper, # bread-module-host, breadbar, breadbox, breadcrumbs, breadpad, breadpaper,
# bread-theme, breadmon, breadsearch, breadclip, breadshot, bos-settings, # bread-theme, breadmon, breadsearch, breadclip, breadshot, bos-settings,
# breadhelp — is bakery-managed and baked into /etc/skel/.local at ISO build # breadhelp — is bakery-managed and baked into /usr/local at ISO build
# time from iso/bread-lockfile.toml (see build-local.sh). breadcast and # time from iso/bread-lockfile.toml (see build-local.sh). breadcast and
# breadarr are not shipped. bos-settings/breadhelp desktop entries are # breadarr are not shipped. bos-settings/breadhelp desktop entries are
# committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime # also committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime
# deps stay listed even though no bread package depends on them via pacman # deps stay listed even though no bread package depends on them via pacman
# (gtk4, gtk4-layer-shell, webkit2gtk-4.1, iw, libpulse, librsvg, …). # (gtk4, gtk4-layer-shell, webkit2gtk-4.1, iw, libpulse, librsvg, …).

View file

@ -46,7 +46,7 @@ Include = /etc/pacman.d/mirrorlist
# Breadway custom repo — breadlock plus AUR republishes the ISO needs # Breadway custom repo — breadlock plus AUR republishes the ISO needs
# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin, # (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin,
# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp # zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp
# are NOT here; they are bakery-baked into /etc/skel at ISO build time. # are NOT here; they are bakery-baked into /usr/local at ISO build time.
# #
# Packages are published to the Forgejo Arch registry (group "os") by the # Packages are published to the Forgejo Arch registry (group "os") by the
# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). # .forgejo/workflows/*.yml workflows in this repo (and breadlock's).

View file

@ -1,15 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Read-only checks that a builder home (and optionally a staged skel) has # Read-only checks that a builder home (and optionally a staged image) has
# everything build-local.sh needs before mkarchiso. Exit non-zero on failure. # everything build-local.sh needs before mkarchiso. Exit non-zero on failure.
# #
# Builder home stays user-layout (~/.local). The image is system-prefix
# /usr/local; pass SKEL and/or AIROOTFS to check those destinations.
#
# LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh # LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh
# SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh # SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh
# AIROOTFS=/tmp/bos-iso-stage/airootfs ./scripts/ci-verify-bake.sh
set -euo pipefail set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)" REPO="$(cd "$(dirname "$0")/.." && pwd)"
LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}" LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}"
LAPTOP_HOME="${LAPTOP_HOME:-/build-home}" LAPTOP_HOME="${LAPTOP_HOME:-/build-home}"
SKEL="${SKEL:-}" SKEL="${SKEL:-}"
AIROOTFS="${AIROOTFS:-}"
pass=0 pass=0
fail=0 fail=0
@ -112,21 +117,73 @@ else
done done
fi fi
if [[ -n "$SKEL" ]]; then if [[ -n "$SKEL" && -z "$AIROOTFS" ]]; then
echo "== staged skel $SKEL ==" if [[ -d "$SKEL/usr/local/bin" ]]; then
for b in "${REQUIRED_BINS[@]}"; do AIROOTFS="$SKEL"
check_exec "$SKEL/.local/bin/$b" "skel required bin $b" SKEL="$AIROOTFS/etc/skel"
done elif [[ -d "$SKEL/../../usr/local" ]]; then
check_dir "$SKEL/.local/share/breadhelp/content" "skel breadhelp content" AIROOTFS="$(cd "$SKEL/../.." && pwd)"
check_file "$SKEL/.cache/bakery/index.json" "skel bakery index cache" fi
for unit in "${UNITS[@]}"; do elif [[ -n "$AIROOTFS" && -z "$SKEL" ]]; then
[[ -n "$unit" ]] || continue SKEL="$AIROOTFS/etc/skel"
if [[ -f "$SKEL/.config/systemd/user/$unit" ]]; then fi
ok "skel unit $unit"
if [[ -n "$AIROOTFS" || -n "$SKEL" ]]; then
if [[ -n "$AIROOTFS" ]]; then
echo "== staged image $AIROOTFS =="
check_file "$AIROOTFS/etc/bakery/config.toml" "bakery prefix config"
if [[ -f "$AIROOTFS/etc/bakery/config.toml" ]] && grep -q 'prefix[[:space:]]*=[[:space:]]*"/usr/local"' "$AIROOTFS/etc/bakery/config.toml"; then
ok "bakery prefix = /usr/local"
else else
bad "skel unit missing: $SKEL/.config/systemd/user/$unit" bad "bakery prefix is not /usr/local in $AIROOTFS/etc/bakery/config.toml"
fi fi
done for b in "${REQUIRED_BINS[@]}"; do
check_exec "$AIROOTFS/usr/local/bin/$b" "image required bin $b"
done
check_dir "$AIROOTFS/usr/local/share/breadhelp/content" "image breadhelp content"
fi
if [[ -n "$SKEL" ]]; then
echo "== staged skel $SKEL =="
check_file "$SKEL/.cache/bakery/index.json" "skel bakery index cache"
check_file "$SKEL/.local/state/bakery/installed.json" "skel bakery installed.json"
for b in "${REQUIRED_BINS[@]}"; do
if [[ -e "$SKEL/.local/bin/$b" ]]; then
bad "skel still has bakery bin $b (belongs in /usr/local/bin)"
fi
done
fi
image_units_json=""
if [[ -n "$SKEL" && -f "$SKEL/.local/state/bakery/installed.json" ]]; then
image_units_json="$SKEL/.local/state/bakery/installed.json"
fi
if [[ -n "$image_units_json" ]]; then
mapfile -t IMAGE_UNITS < <(python3 - "$image_units_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
)
else
IMAGE_UNITS=("${UNITS[@]}")
fi
if [[ -n "$AIROOTFS" ]]; then
for unit in "${IMAGE_UNITS[@]}"; do
[[ -n "$unit" ]] || continue
check_file "$AIROOTFS/usr/lib/systemd/user/$unit" "image unit $unit"
if [[ -f "$AIROOTFS/usr/lib/systemd/user/$unit" ]]; then
if grep -q '^ExecStart=/usr/local/bin/' "$AIROOTFS/usr/lib/systemd/user/$unit"; then
ok "image unit $unit ExecStart uses /usr/local/bin"
elif grep -q '^ExecStart=' "$AIROOTFS/usr/lib/systemd/user/$unit"; then
bad "image unit $unit ExecStart is not /usr/local/bin: $(grep '^ExecStart=' "$AIROOTFS/usr/lib/systemd/user/$unit")"
fi
fi
done
fi
fi fi
echo echo

View file

@ -63,7 +63,7 @@ check "bos-settings installed" "command -v bos-settings"
echo "== breadhelp ==" echo "== breadhelp =="
check "breadhelp installed" "command -v breadhelp" check "breadhelp installed" "command -v breadhelp"
check "breadhelp content installed" \ check "breadhelp content installed" \
"[ -d \"\$HOME/.local/share/breadhelp/content\" ] || [ -d /etc/skel/.local/share/breadhelp/content ]" "[ -d /usr/local/share/breadhelp/content ] || [ -d \"\$HOME/.local/share/breadhelp/content\" ]"
check "bos-netcheck present" "command -v bos-netcheck" check "bos-netcheck present" "command -v bos-netcheck"
check "bos-rescue present" "command -v bos-rescue" check "bos-rescue present" "command -v bos-rescue"
check "bos-first-boot present" "command -v bos-first-boot" check "bos-first-boot present" "command -v bos-first-boot"