diff --git a/AGENTS.md b/AGENTS.md index 3a9f4bf..8019d5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,4 +52,7 @@ There is no `dev` integration branch. - 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. + `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). diff --git a/README.md b/README.md index 5c7348c..387c5f4 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ wiring up dotfiles, no per-tool bakery installs. - **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 ecosystem**, baked into `/usr/local` from bakery-managed binaries + (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 them), `breadbar` (status bar + notifications), `breadbox` (launcher), `breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles), @@ -58,7 +59,7 @@ wiring up dotfiles, no per-tool bakery installs. | 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 | | **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) | | **Not shipped** | `breadcast`, `breadarr` | @@ -123,9 +124,15 @@ 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`: +`build-local.sh` builds the image natively (no container) and copies this +machine's bakery-installed bread binaries + breadhelp content from the +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 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. Post-install, `scripts/smoke-test.sh` (run as the installed user) checks -subvolumes, services, bakery bins, and breadhelp content under -`~/.local/share/breadhelp/content`. +subvolumes, services, bakery bins on PATH, and breadhelp content under +`/usr/local/share/breadhelp/content`. ## bos-settings @@ -234,7 +241,7 @@ repo, not here. ## The bread ecosystem 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 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 @@ -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` | | `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+/` | +| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `/usr/local/share/breadhelp/content` (bakery `content.tar.gz`, baked into the image). | `SUPER+/` | **System** diff --git a/build-local.sh b/build-local.sh index b06f58d..8548d86 100755 --- a/build-local.sh +++ b/build-local.sh @@ -41,13 +41,15 @@ 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 ------ +# --- Bake this machine's bakery-installed bread ecosystem into the image ------ # 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. +# has. Builder home stays user-layout (~/.local); the *image* is system-prefix +# /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. # 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_CACHE="$LAPTOP_HOME/.cache/bakery" 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 "lockfile: $LOCKFILE (${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional)" +echo "image prefix: /usr/local (bins $IMAGE_BIN, share $IMAGE_SHARE, units $IMAGE_UNITS)" missing=() for b in "${REQUIRED_BINS[@]}"; do @@ -124,9 +131,9 @@ for b in "${OPTIONAL_BINS[@]}"; do fi 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 - install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b" + install -m 0755 "$BAKERY_BIN/$b" "$IMAGE_BIN/$b" done # 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 fi 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 ------------------------------ # bakery extracts data_archive (breadhelp's content.tar.gz) to -# ~/.local/share// and writes desktop entries + licenses next to it. -# Copy those — never laptop-local state (clipboard history, WebKit cache, -# bread sync-repo, models). -echo "=== baking bakery share/data into skel ===" +# $prefix/share// and writes desktop entries + licenses next to it. +# Builder home is still ~/.local/share; copy into the image at +# /usr/local/share. Never laptop-local state (clipboard history, WebKit +# cache, bread sync-repo, models). +echo "=== baking bakery share/data into /usr/local/share ===" 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 "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 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" +install -d -m 0755 "$IMAGE_SHARE" +cp -a "$BAKERY_SHARE/breadhelp" "$IMAGE_SHARE/breadhelp" +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 index_path, src_share, dest_share, *bins = sys.argv[1:] wanted = set(bins) @@ -242,14 +250,16 @@ PY # 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. -echo "=== baking bakery service units into skel ===" +# lockfile packages. Units go to /usr/lib/systemd/user with ExecStart +# rewritten to /usr/local/bin (not %h/.local/bin). Recreate whichever +# *.target.wants enable symlink bakery created locally (or that skel +# already ships). Hand-committed skel units (breadd.service carries a +# RuntimeDirectoryPreserve=yes fix not yet upstreamed) are the source +# 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" SKEL_SYSTEMD="$SKEL/.config/systemd/user" +install -d -m 0755 "$IMAGE_UNITS" mapfile -t SERVICE_UNITS < <(python3 - "$SKEL/.local/state/bakery/installed.json" <<'PY' import json, sys 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) 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 [[ -n "$unit" ]] || continue if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then - echo " $unit already committed in skel, leaving as-is" - continue + src="$SKEL_SYSTEMD/$unit" + 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 - 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 + rewrite_exec_start "$src" "$IMAGE_UNITS/$unit" + if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then + rewrite_exec_start "$src" "$SKEL_SYSTEMD/$unit" fi - install -d -m 0755 "$SKEL_SYSTEMD" - sed "s#ExecStart=$LAPTOP_HOME/.local/bin/#ExecStart=%h/.local/bin/#" "$src" > "$SKEL_SYSTEMD/$unit" - for wants_dir in "$SYSTEMD_USER_DIR"/*.target.wants; do - [[ -L "$wants_dir/$unit" ]] || continue - target_name="$(basename "$wants_dir")" - install -d -m 0755 "$SKEL_SYSTEMD/$target_name" - ln -sf "../$unit" "$SKEL_SYSTEMD/$target_name/$unit" + for base in "$SYSTEMD_USER_DIR" "$SKEL_SYSTEMD"; do + [[ -d "$base" ]] || continue + for wants_dir in "$base"/*.target.wants; do + [[ -e "$wants_dir" || -L "$wants_dir" ]] || continue + [[ -L "$wants_dir/$unit" ]] || continue + target_name="$(basename "$wants_dir")" + install -d -m 0755 "$IMAGE_UNITS/$target_name" + ln -sf "../$unit" "$IMAGE_UNITS/$target_name/$unit" + done done - echo " baked $unit" + echo " baked $unit -> $IMAGE_UNITS/$unit" done # 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 # 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)" 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 sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh" 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 # boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID diff --git a/docs/hardware.md b/docs/hardware.md index 8add314..2f87838 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -24,7 +24,9 @@ submenu** (grub-btrfs), not `snapper rollback`. BOS GRUB pins `rootflags=subvol=@`. `snapper rollback` swaps the default 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 ISO and run `sudo bos-rescue`. It mounts `@` + the ESP and offers the same diff --git a/iso/airootfs/etc/bakery/config.toml b/iso/airootfs/etc/bakery/config.toml new file mode 100644 index 0000000..996b04a --- /dev/null +++ b/iso/airootfs/etc/bakery/config.toml @@ -0,0 +1,2 @@ +# Bakery desktop apps live under /usr/local so they ride snapper @ snapshots. +prefix = "/usr/local" diff --git a/iso/airootfs/etc/calamares/post-install.sh b/iso/airootfs/etc/calamares/post-install.sh index 2076dcf..ac13a97 100644 --- a/iso/airootfs/etc/calamares/post-install.sh +++ b/iso/airootfs/etc/calamares/post-install.sh @@ -425,9 +425,10 @@ if command -v ufw &>/dev/null; then 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 +# breadpad, bos-settings, breadhelp, ...) is bakery-managed, not pacman: +# binaries, share/data, and user units are baked into /usr/local and +# /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. # --------------------------------------------------------------------------- diff --git a/iso/airootfs/etc/greetd/breadgreet.toml b/iso/airootfs/etc/greetd/breadgreet.toml index 311dfad..24b48a4 100644 --- a/iso/airootfs/etc/greetd/breadgreet.toml +++ b/iso/airootfs/etc/greetd/breadgreet.toml @@ -7,8 +7,9 @@ # alongside BOS's own bos.desktop, and breadgreet's session picker matches by # .desktop file stem — with no override it picks "hyprland.desktop" over # "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 -# never sourced any other way). Confirmed via breadgreet's own test suite +# per-user tools; bakery apps are in /usr/local/bin). greetd starts no login +# 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] diff --git a/iso/airootfs/etc/pacman.conf b/iso/airootfs/etc/pacman.conf index 7e15c7a..4e4435e 100644 --- a/iso/airootfs/etc/pacman.conf +++ b/iso/airootfs/etc/pacman.conf @@ -29,7 +29,7 @@ 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. +# 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 # .forgejo/workflows/*.yml workflows in this repo (and breadlock's). diff --git a/iso/airootfs/etc/profile.d/bos-local-bin.sh b/iso/airootfs/etc/profile.d/bos-local-bin.sh index 642af43..734db46 100644 --- a/iso/airootfs/etc/profile.d/bos-local-bin.sh +++ b/iso/airootfs/etc/profile.d/bos-local-bin.sh @@ -1,8 +1,8 @@ -# Put the per-user bakery bin dir on PATH. The bread ecosystem (breadd, breadbar, -# breadbox, …) is installed there by bakery, and the Hyprland session launches -# them via `exec-once`, which resolves against the PATH it inherits from the -# login shell. Arch's stock /etc/profile does not add ~/.local/bin, so do it here -# for every login shell (live user and installed user alike). +# Keep ~/.local/bin on PATH for per-user tools. Arch already includes +# /usr/local/bin (where bakery desktop apps live on BOS). The Hyprland +# session resolves exec-once against the PATH it inherits from the login +# shell; Arch's stock /etc/profile does not add ~/.local/bin, so do it +# here for every login shell (live user and installed user alike). case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua index d71731a..07ed155 100644 --- a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua @@ -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-size 24", -- Clipboard history is breadclipd, a bakery-managed systemd --user - -- service (auto-started via skel — see build-local.sh's service bake) - -- rather than an exec-once here. + -- service (auto-started from /usr/lib/systemd/user — see + -- build-local.sh's service bake) rather than an exec-once here. "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1", "awww-daemon", -- 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. -- 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']], - -- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service, - -- enabled in skel). It autostarts at login but before Hyprland exists, so + -- breadd runs as a systemd user service (/usr/lib/systemd/user/breadd.service, + -- 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 -- 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", @@ -162,7 +162,8 @@ hl.on("hyprland.start", function() -- breadbox-sync is a Type=oneshot systemd --user service -- (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() return dofile(script_dir .. "system/autostart.lua")() end) diff --git a/iso/airootfs/etc/skel/.config/systemd/user/breadd.service b/iso/airootfs/etc/skel/.config/systemd/user/breadd.service index 49d6741..945c09c 100644 --- a/iso/airootfs/etc/skel/.config/systemd/user/breadd.service +++ b/iso/airootfs/etc/skel/.config/systemd/user/breadd.service @@ -3,8 +3,8 @@ Description=Bread Runtime Daemon [Service] Type=simple -# %h = the user's home — works for any account created from this skel. -ExecStart=%h/.local/bin/breadd +# System-prefix bakery install — same path for every account. +ExecStart=/usr/local/bin/breadd Restart=on-failure RestartSec=2 UMask=0077 diff --git a/iso/airootfs/etc/skel/.zshrc b/iso/airootfs/etc/skel/.zshrc index e4f1e69..3de4cc4 100644 --- a/iso/airootfs/etc/skel/.zshrc +++ b/iso/airootfs/etc/skel/.zshrc @@ -89,7 +89,7 @@ alias alt-install='yay -S' alias alt-uninstall='yay -R' 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" # Powerlevel10k prompt configuration. diff --git a/iso/airootfs/usr/local/bin/bos-session b/iso/airootfs/usr/local/bin/bos-session index d7655fc..5f27974 100644 --- a/iso/airootfs/usr/local/bin/bos-session +++ b/iso/airootfs/usr/local/bin/bos-session @@ -2,11 +2,10 @@ # BOS graphical session launcher, run by greetd on the INSTALLED system after # the user authenticates (see /etc/greetd/config.toml). # -# greetd does not start a login shell, so /etc/profile.d is never sourced — which -# means ~/.local/bin (where bakery installs the bread ecosystem: breadd, breadbar, -# breadbox-sync, …) would be missing from PATH and the Hyprland `exec-once` -# launches would fail. Source the login profile here so PATH is correct, set the -# Wayland session hints, then hand off to Hyprland. +# greetd does not start a login shell, so /etc/profile.d is never sourced. +# Bakery desktop apps live in /usr/local/bin (already on Arch PATH). Source +# the login profile here so ~/.local/bin (per-user tools) is also on PATH, +# set the Wayland session hints, then hand off to Hyprland. # # Launched via start-hyprland (ships with the hyprland package) rather than the # raw Hyprland binary — Hyprland upstream no longer recommends exec'ing it diff --git a/iso/airootfs/usr/local/bin/bos-update b/iso/airootfs/usr/local/bin/bos-update index 65f27c0..3b3cc04 100644 --- a/iso/airootfs/usr/local/bin/bos-update +++ b/iso/airootfs/usr/local/bin/bos-update @@ -8,11 +8,12 @@ # or other bakery desktop apps. Every transaction is # snapshotted by snap-pac; recover via the GRUB "snapshots" # 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, # breadcrumbs, breadpad, breadman, bread-theme, breadpaper, # 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. set -uo pipefail @@ -20,12 +21,11 @@ set -uo pipefail bold() { printf '\033[1m%s\033[0m\n' "$1"; } # Timed snapper pre snapshot before either channel. snap-pac already -# snapshots root around pacman; bakery writes ~/.local/bin ($HOME / @home), -# which is outside that root snapshot. This extra snapshot is still -# best-effort and covers bakery $HOME updates as well as possible — a -# home config if the installer created one, otherwise the root timeline -# around the whole update. Never fail the update if snapper is missing -# or the create errors. +# snapshots root around pacman; bakery now writes /usr/local (on @), so +# that root snapshot includes the desktop apps. This extra snapshot is +# still best-effort — a home config if the installer created one (user +# bakery state), plus a root timeline around the whole update. Never +# fail the update if snapper is missing or the create errors. if command -v snapper >/dev/null; then if snapper -c home list >/dev/null 2>&1; then snapper -c home create -t pre -c number \ diff --git a/iso/bread-lockfile.toml b/iso/bread-lockfile.toml index ac34e31..e1ef73a 100644 --- a/iso/bread-lockfile.toml +++ b/iso/bread-lockfile.toml @@ -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 # *required* binary fails the bake: a hollow ISO is worse than a failed build. diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index cc8e2e2..498ed39 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -206,10 +206,10 @@ yay-bin # /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 +# breadhelp — is bakery-managed and baked into /usr/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 +# also 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, …). diff --git a/iso/pacman.conf b/iso/pacman.conf index 506b4bb..be3d52f 100644 --- a/iso/pacman.conf +++ b/iso/pacman.conf @@ -46,7 +46,7 @@ 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. +# 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 # .forgejo/workflows/*.yml workflows in this repo (and breadlock's). diff --git a/scripts/ci-verify-bake.sh b/scripts/ci-verify-bake.sh index d02d0b0..0fcd06a 100755 --- a/scripts/ci-verify-bake.sh +++ b/scripts/ci-verify-bake.sh @@ -1,15 +1,20 @@ #!/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. # +# 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 # 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 REPO="$(cd "$(dirname "$0")/.." && pwd)" LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}" LAPTOP_HOME="${LAPTOP_HOME:-/build-home}" SKEL="${SKEL:-}" +AIROOTFS="${AIROOTFS:-}" pass=0 fail=0 @@ -112,21 +117,73 @@ else 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" +if [[ -n "$SKEL" && -z "$AIROOTFS" ]]; then + if [[ -d "$SKEL/usr/local/bin" ]]; then + AIROOTFS="$SKEL" + SKEL="$AIROOTFS/etc/skel" + elif [[ -d "$SKEL/../../usr/local" ]]; then + AIROOTFS="$(cd "$SKEL/../.." && pwd)" + fi +elif [[ -n "$AIROOTFS" && -z "$SKEL" ]]; then + SKEL="$AIROOTFS/etc/skel" +fi + +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 - bad "skel unit missing: $SKEL/.config/systemd/user/$unit" + bad "bakery prefix is not /usr/local in $AIROOTFS/etc/bakery/config.toml" 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 echo diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 19e7d75..d5b6fa9 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -63,7 +63,7 @@ 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 ]" + "[ -d /usr/local/share/breadhelp/content ] || [ -d \"\$HOME/.local/share/breadhelp/content\" ]" check "bos-netcheck present" "command -v bos-netcheck" check "bos-rescue present" "command -v bos-rescue" check "bos-first-boot present" "command -v bos-first-boot"