diff --git a/.forgejo/workflows/python-pywal.yml b/.forgejo/workflows/python-pywal.yml deleted file mode 100644 index f5f7d61..0000000 --- a/.forgejo/workflows/python-pywal.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build and publish python-pywal - -# python-pywal was dropped from Arch's [extra] repo (AUR-only now), but the ISO -# needs the `wal` binary (bread-theme extracts the wallpaper palette with it). -# BOS keeps an in-house PKGBUILD and publishes to the [breadway] repo — same -# pattern as calamares / bibata / powerlevel10k / yay-bin. -on: - push: - paths: - - 'packaging/python-pywal/**' - workflow_dispatch: - -jobs: - python-pywal: - runs-on: [self-hosted, hestia] - container: - image: archlinux:latest - steps: - - name: Build and publish - env: - PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - pacman -Syu --noconfirm base-devel git \ - python python-build python-installer python-wheel python-setuptools imagemagick - useradd -m builder - git config --global --add safe.directory '*' - # Clone the ref that triggered this run (not the default branch) — - # same as the other packaging workflows. - git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src - chown -R builder:builder /home/builder/src - su builder -c "cd /home/builder/src/packaging/python-pywal && makepkg -f --noconfirm" - PKG=$(find /home/builder/src/packaging/python-pywal -name '*.pkg.tar.zst' | head -1) - curl -fsS -X PUT \ - -H "Authorization: token ${PUBLISH_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@${PKG}" \ - "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/release-iso.yml b/.forgejo/workflows/release-iso.yml index 162fa16..7191c22 100644 --- a/.forgejo/workflows/release-iso.yml +++ b/.forgejo/workflows/release-iso.yml @@ -12,8 +12,8 @@ name: Build and release ISO # MIRROR_TOKEN — GitHub personal access token with repo scope # GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing" # identity (releases@breadway.dev); public half is committed -# at KEYS.asc. Signs ISO SHA256SUMS here; the same secret -# signs the [breadway] repo in signed-repo.yml. No passphrase +# 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). @@ -41,10 +41,7 @@ jobs: steps: - name: Install build dependencies run: | - # grub is required by profiledef.sh bootmodes=('uefi.grub'): - # mkarchiso validates grub-install on the *builder*, not the image. - # archiso pulls syslinux/squashfs-tools/libisoburn; it does not pull grub. - pacman -Syu --noconfirm archiso grub curl python git minisign + pacman -Syu --noconfirm archiso curl python git minisign - name: Determine tag and version id: vars diff --git a/.forgejo/workflows/signed-repo.yml b/.forgejo/workflows/signed-repo.yml deleted file mode 100644 index caa5d15..0000000 --- a/.forgejo/workflows/signed-repo.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Publish signed [breadway] repo - -# Host job on hestia (no container:) so it can write /srv/breadway-dl, same -# as bakery releases. breadlock package.yml uses archlinux:latest and cannot -# see host /srv — do not add container: here. -# -# Collects breadlock + the ISO AUR republishes from the Forgejo Arch -# registry, detach-signs each .pkg.tar.zst, repo-add -s, publishes -# https://dl.breadway.dev/arch/x86_64/. Does not PUT to the registry -# (existing packaging workflows keep doing that). Does not flip ISO SigLevel. -# -# Required secret: GPG_PRIVATE_KEY (same BOS release key as release-iso.yml). - -on: - workflow_dispatch: - repository_dispatch: - types: [publish-signed-repo] - workflow_run: - workflows: - - Build and publish calamares - - Build and publish bibata-cursor-theme - - Build and publish powerlevel10k - - Build and publish yay-bin - - Build and publish python-pywal - types: [completed] - -concurrency: - group: signed-repo - cancel-in-progress: false - -jobs: - publish: - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: [self-hosted, hestia] - steps: - - name: Clone repository - run: | - set -euo pipefail - REF="${GITHUB_REF_NAME:-main}" - rm -rf src - git clone --depth 1 --branch "$REF" \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - - name: Sign packages and publish repo - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - run: | - set -euo pipefail - if [ -z "${GPG_PRIVATE_KEY:-}" ]; then - echo "GPG_PRIVATE_KEY secret is missing; refusing to publish an unsigned [breadway] repo." >&2 - exit 1 - fi - bash src/scripts/ci-publish-signed-repo.sh diff --git a/AGENTS.md b/AGENTS.md index 8019d5e..3a9f4bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,4 @@ 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. 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). + `rootflags=subvol=@`. Recovery is grub-btrfs reboot. diff --git a/DESIGN.md b/DESIGN.md index 6a075a8..31b22f8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -16,7 +16,7 @@ taken as current: | 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]` | **Yes, as of the signed repo.** `[breadway]` points at `https://dl.breadway.dev/arch` where `scripts/ci-publish-signed-repo.sh` detach-signs every `.pkg.tar.zst` and the db with the BOS release key (`56203B86…`, `KEYS.asc`). That key is trusted in the pacman keyring at build time, on the live medium, and on the installed target. | +| `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. | --- diff --git a/README.md b/README.md index 22b0108..ccaf576 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,8 @@ 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 `/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 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), @@ -46,11 +45,9 @@ wiring up dotfiles, no per-tool bakery installs. Mesa only — **NVIDIA proprietary drivers are not included** and NVIDIA is unsupported out of the box (see [docs/hardware.md](docs/hardware.md)). - **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every - pacman transaction (**root `@` only** — snapper does not cover `@home`); - home backup is **Settings → Backup** (restic, local path or SFTP); 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=@`). See + 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=@`). See [docs/hardware.md](docs/hardware.md). - **Security**: optional full-disk encryption is **LUKS1** (GRUB cannot unlock LUKS2 + Argon2id). Secure Boot is **self-signed Setup Mode only** via @@ -61,7 +58,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 `/usr/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 `~/.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` | @@ -96,12 +93,11 @@ bos/ ├── scripts/ │ ├── ci-stage-bakery.py # CI: minisign-verified index → $LAPTOP_HOME │ ├── ci-verify-bake.sh # CI: read-only checks before mkarchiso -│ ├── ci-publish-signed-repo.sh # CI: signed [breadway] repo → /srv/breadway-dl/arch │ └── smoke-test.sh ├── docs/ │ ├── hardware.md # Mesa only, NVIDIA, grub-btrfs recovery -│ └── signed-repo.md # dl.breadway.dev/arch signing -├── .forgejo/workflows/ # CI: AUR republish + signed repo + tagged ISO +│ └── signed-repo.md # future dl.breadway.dev/arch signing +├── .forgejo/workflows/ # CI: AUR republish + tagged ISO release ├── build-local.sh # native ISO build for this machine ├── README.md └── DESIGN.md # historical plan @@ -127,16 +123,9 @@ 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 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`. -User units are `systemctl --global enable`'d so a later `useradd -m` -starts them on first login. 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`. +`build-local.sh` builds the image natively (no container) and bakes this +machine's bakery-installed bread binaries + breadhelp content into +`/etc/skel`: ```sh sudo ./build-local.sh # release-quality (xz squashfs) @@ -174,10 +163,10 @@ 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). The same key signs -the ISO checksums **and** the `[breadway]` pacman repo — every package and -the db at `https://dl.breadway.dev/arch` carry a `.sig` from it, and that -section is `SigLevel = Required` (see +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` until a signed repo exists — see [docs/signed-repo.md](docs/signed-repo.md)). To verify a download: ```sh @@ -201,38 +190,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 on PATH, breadhelp content under -`/usr/local/share/breadhelp/content`, and that bakery user units are -`--global` enabled (or the preset / wants files exist). - -## Second account - -Bakery desktop apps live in `/usr/local` — shared, already on PATH. A later -account does **not** get a private copy of those binaries. - -`/etc/default/useradd` keeps `SKEL=/etc/skel`. Stock `useradd -m` is enough: - -```sh -sudo useradd -m alice -sudo passwd alice -``` - -- **Apps**: `/usr/local/bin` (and `/usr/local/share`) — already there. -- **Session files**: `useradd -m` copies `/etc/skel` (Hyprland, bread - config, bakery `installed.json` + index cache) so first login has a - session. Skel does not contain bakery binaries. -- **Daemons**: `breadd`, `breadbox-sync`, `breadclipd`, `breadcrumbs`, - `breadmill`, … are `systemctl --global enable`'d at install (and on - the live image). Creating a user starts them on first login. -- **Login**: greetd/breadgreet lists any local user with a login shell - (`SHELL=/usr/bin/zsh` is the useradd default). - -`breadclipd` is WantedBy=`graphical-session.target`. BOS does not activate -that target (no uwsm), so Hyprland still `systemctl --user start`s it after -the compositor is up. `--global enable` still records it for every account. - -Rollback is still the GRUB snapshots submenu (grub-btrfs), not -`snapper rollback`. `/usr/local` rides the `@` snapshot. +subvolumes, services, bakery bins, and breadhelp content under +`~/.local/share/breadhelp/content`. ## bos-settings @@ -267,8 +226,7 @@ also get live systemd status + Start/Stop/Restart/Logs. | Packages | `bakery` installed list + updates, pacman system update | | AUR | Search via `yay`; installing opens a terminal (AUR build scripts need review) | | Firmware | `fwupd` device list + updates | -| Snapshots | `snapper` list (number / date / description); reboot to pick in GRUB (grub-btrfs); delete — **root (`@`) only** | -| Backup | restic of `$HOME` (`@home`) via Settings → Backup; snapper does not cover home | +| 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. @@ -276,7 +234,7 @@ repo, not here. ## The bread ecosystem Everything below is a separate bakery-distributed project with its own repo -and release cadence, baked into `/usr/local` at ISO build time so a fresh +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 @@ -300,7 +258,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 `/usr/local/share/breadhelp/content` (bakery `content.tar.gz`, baked into the image). | `SUPER+/` | +| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `~/.local/share/breadhelp/content` (bakery `content.tar.gz`, baked into skel). | `SUPER+/` | **System** @@ -381,24 +339,20 @@ until `dl.breadway.dev/arch` exists). - **Snapshots assume btrfs**: the snapper/grub-btrfs tooling expects the default btrfs subvolume layout the installer creates. Recovery is the GRUB snapshots submenu, not `snapper rollback` — [docs/hardware.md](docs/hardware.md). -- **`[breadway]` signatures**: `SigLevel = Required` — the signed repo at - `dl.breadway.dev/arch` is live (db + every package `.sig`ned with the BOS - release key). See [docs/signed-repo.md](docs/signed-repo.md). +- **`[breadway]` signatures**: `SigLevel = Never` until a signed repo is + stood up at `dl.breadway.dev/arch`. See [docs/signed-repo.md](docs/signed-repo.md). ## Recovery -**An update broke something (system still boots):** reboot → **GRUB -“snapshots” submenu** (grub-btrfs), then boot that entry. +**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. -BOS Settings → Snapshots lists each snapshot’s number, date, and -description so you know which GRUB entry to pick. It does not roll the -running root back in place. Snapper is root only. Home files are -**Settings → Backup** (restic restore into `~/bos-restore-`, not -over `$HOME`). - -Do **not** run `snapper rollback`. BOS GRUB pins `rootflags=subvol=@`, so -a snapper-swapped default subvolume is not what the installed grub.cfg -will boot next. Details: [docs/hardware.md](docs/hardware.md). +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. Details: +[docs/hardware.md](docs/hardware.md). A/B root swapping (SteamOS-style) is a **future** idea in DESIGN.md — it is not shipped. @@ -406,15 +360,14 @@ not shipped. **The system won't boot (broken GRUB / lost EFI entry):** 1. Boot the BOS ISO and open a terminal (`SUPER+Return`). -2. Run `sudo bos-rescue`. It finds the installed btrfs `@` and the ESP, - prints the devices it will use, and asks `YES` before writing. It can - `arch-chroot` and/or reinstall GRUB with the same sequence the - installer uses (NVRAM + `--removable` + `grub-mkconfig`). -3. Manual equivalent, if you would rather type it: +2. Mount the installed root and EFI, then chroot: ```sh mount -o subvol=@ /dev/sdXN /mnt mount /dev/sdXP /mnt/boot/efi # the EFI partition arch-chroot /mnt + ``` +3. Reinstall the bootloader (the same sequence the installer uses): + ```sh grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck grub-mkconfig -o /boot/grub/grub.cfg diff --git a/build-local.sh b/build-local.sh index 4b95661..b06f58d 100755 --- a/build-local.sh +++ b/build-local.sh @@ -25,19 +25,14 @@ OUT="${OUT:-$REPO/out}" STAGE=/tmp/bos-iso-stage rm -rf "$STAGE" && cp -a "$REPO/iso" "$STAGE" -# [breadway] now points at the signed public repo https://dl.breadway.dev/arch -# (SigLevel = Required) — no Forgejo-registry URL rewrite needed anymore. -# -# Trust the [breadway] repo key in *this* build host's pacman keyring so -# `pacstrap` can verify [breadway] packages while assembling the airootfs. -# The same key is baked into the image at etc/pacman.d/breadway-repo.asc and -# re-trusted on the live medium / installed target (calamares/post-install.sh). -BREADWAY_KEY_FPR="56203B86A110695AE7F310934AF3323D678EB5E2" -BREADWAY_KEY_SRC="$REPO/iso/airootfs/etc/pacman.d/breadway-repo.asc" -if ! pacman-key --list-keys "$BREADWAY_KEY_FPR" &>/dev/null; then - echo "=== trusting [breadway] repo key ($BREADWAY_KEY_FPR) in the host pacman keyring ===" - pacman-key --add "$BREADWAY_KEY_SRC" - pacman-key --lsign-key "$BREADWAY_KEY_FPR" +# Rewrite the [breadway] pacman repo URL to the fastest reachable address. +# CI_BUILD=1 — container runs on hestia with --network=host; localhost:3002 is direct +# default — building on hermes; git.breadway.dev is flaky from there, use Tailscale +# Only ever rewrites the staged copy, never the committed pacman.conf. +if [ "${CI_BUILD:-0}" = "1" ]; then + sed -i 's#https://git.breadway.dev/api/packages/Breadway/arch/os#http://localhost:3002/api/packages/Breadway/arch/os#' "$STAGE/pacman.conf" +else + sed -i 's#https://git.breadway.dev/api/packages/Breadway/arch/os#http://100.66.238.26:3002/api/packages/Breadway/arch/os#' "$STAGE/pacman.conf" fi if [ "${FAST_BUILD:-0}" = "1" ]; then @@ -46,15 +41,13 @@ if [ "${FAST_BUILD:-0}" = "1" ]; then fi grep airootfs_image_tool_options "$STAGE/profiledef.sh" -# --- Bake this machine's bakery-installed bread ecosystem into the image ------ +# --- 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. 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. +# 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 @@ -104,14 +97,9 @@ 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" -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" +SKEL="$STAGE/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 @@ -136,9 +124,9 @@ for b in "${OPTIONAL_BINS[@]}"; do fi done -install -d -m 0755 "$IMAGE_BIN" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery" +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" "$IMAGE_BIN/$b" + install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b" done # Drop packages that are not in the lockfile (breadcast/breadarr must not @@ -176,27 +164,26 @@ 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 "$IMAGE_BIN")" +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 -# $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 ===" +# ~/.local/share// and writes desktop entries + licenses next to it. +# Copy those — never laptop-local state (clipboard history, WebKit cache, +# bread sync-repo, models). +echo "=== baking bakery share/data into skel ===" BREADHELP_CONTENT="$BAKERY_SHARE/breadhelp/content" if [[ ! -d "$BREADHELP_CONTENT" ]]; then echo "ERROR: breadhelp content missing: $BREADHELP_CONTENT" >&2 - echo "bakery installs this from content.tar.gz into ~/.local/share/breadhelp/content on the builder" >&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 "$IMAGE_SHARE" -cp -a "$BAKERY_SHARE/breadhelp" "$IMAGE_SHARE/breadhelp" -echo " baked $IMAGE_SHARE/breadhelp/content" +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" "$IMAGE_SHARE" "${BREAD_BINS[@]}" <<'PY' +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) @@ -254,194 +241,58 @@ PY # 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). -# Units come from installed.json + the bakery index + local unit files -# whose ExecStart is a lockfile binary (installed.json has omitted -# breadcrumbs.service before). Units go to /usr/lib/systemd/user with -# ExecStart rewritten to /usr/local/bin. Recreate whichever -# *.target.wants enable symlink bakery created locally (or that skel -# already ships), and write /etc/systemd/user/*.wants/ (--global). -# 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 ===" +# 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 ===" SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user" SKEL_SYSTEMD="$SKEL/.config/systemd/user" -install -d -m 0755 "$IMAGE_UNITS" -# installed.json on the builder can omit a service even when the index and -# the local unit file exist (breadcrumbs has done this). Merge all three -# so every lockfile daemon is baked and can be --global enabled. -mapfile -t SERVICE_UNITS < <(python3 - \ - "$SKEL/.local/state/bakery/installed.json" \ - "$BAKERY_CACHE/index.json" \ - "$SYSTEMD_USER_DIR" \ - "${BREAD_BINS[@]}" <<'PY' -import json, os, sys - -installed_path, index_path, user_dir, *bins = sys.argv[1:] -wanted = set(bins) -units = set() - -def add_svc(svc): - name = svc["unit"] if isinstance(svc, dict) else svc - if not name or str(name).startswith(("breadcast", "breadarr")): - return - units.add(str(name)) - -if os.path.isfile(installed_path): - with open(installed_path) as f: - data = json.load(f) - for pkg in data.get("packages", data).values(): - if isinstance(pkg, dict): - for svc in pkg.get("services") or []: - add_svc(svc) - -if os.path.isfile(index_path): - with open(index_path) as f: - idx = json.load(f) - for name, pkg in (idx.get("packages") or {}).items(): - if not isinstance(pkg, dict): - continue - 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): - for svc in pkg.get("services") or []: - add_svc(svc) - -if os.path.isdir(user_dir): - for fn in os.listdir(user_dir): - if not fn.endswith(".service"): - continue - path = os.path.join(user_dir, fn) - if not os.path.isfile(path): - continue - try: - text = open(path).read() - except OSError: - continue - for line in text.splitlines(): - if line.lstrip().startswith("ExecStart="): - argv0 = line.split("=", 1)[1].split() - if argv0 and os.path.basename(argv0[0]) in wanted: - add_svc(fn) - break - -for unit in sorted(units): - print(unit) +mapfile -t SERVICE_UNITS < <(python3 - "$SKEL/.local/state/bakery/installed.json" <<'PY' +import json, sys +with open(sys.argv[1]) 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 ) -if [[ ! " ${SERVICE_UNITS[*]} " =~ " breadd.service " ]]; then - echo "ERROR: breadd.service not in the bakery unit list — refusing to bake" >&2 - exit 1 -fi -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 - 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 as a bakery service but not found at $src" >&2 - echo "Refusing to bake an image whose daemons will never start." >&2 - exit 1 - fi + echo " $unit already committed in skel, leaving as-is" + continue fi - rewrite_exec_start "$src" "$IMAGE_UNITS/$unit" - if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then - rewrite_exec_start "$src" "$SKEL_SYSTEMD/$unit" + 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 fi - 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 + 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" done - # systemctl --global enable equivalent: /etc/systemd/user/.wants/ - # so the live image and a later useradd inherit the unit without a per-home - # enable. Vendor wants above are extra; this is what --global writes. - python3 - "$IMAGE_UNITS/$unit" "$AIROOTFS/etc/systemd/user" "$unit" <<'PY' -import os, sys -unit_path, etc_user, unit = sys.argv[1:] -in_install = False -targets = [] -for line in open(unit_path): - s = line.strip() - if s.startswith("[") and s.endswith("]"): - in_install = s == "[Install]" - continue - if in_install and s.startswith("WantedBy="): - targets.extend(t for t in s.split("=", 1)[1].split() if t) -for target in targets: - wants = os.path.join(etc_user, f"{target}.wants") - os.makedirs(wants, exist_ok=True) - dest = os.path.join(wants, unit) - if os.path.lexists(dest): - os.remove(dest) - os.symlink(f"/usr/lib/systemd/user/{unit}", dest) - print(f" global enable {unit} -> {dest}") -PY - echo " baked $unit -> $IMAGE_UNITS/$unit" + echo " baked $unit" done -# Document the baked set. The committed preset is the fallback; the staged -# copy lists whatever this bake actually shipped. -preset_dest="$AIROOTFS/usr/lib/systemd/user-preset/90-bos-bakery.preset" -install -d -m 0755 "$(dirname "$preset_dest")" -{ - echo "# Bakery systemd --user units baked into this image." - echo "# Applied by systemctl --global enable (post-install + live setup)" - echo "# so a later useradd starts them on first login." - echo "# breadclipd is also started from hyprland.lua: WantedBy=" - echo "# graphical-session.target is not reached on BOS (no uwsm)." - for unit in "${SERVICE_UNITS[@]}"; do - [[ -n "$unit" ]] || continue - printf 'enable %s\n' "$unit" - done -} >"$preset_dest" -echo " wrote $preset_dest" - # 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 bakery binary right after the array opener (bos-* bins are already -# listed; keeps the bakery list in one place — the lockfile). +# baked binary right after the array opener (keeps the binary list in one place). perm_file="$(mktemp)" for b in "${BREAD_BINS[@]}"; do - printf ' ["/usr/local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file" + printf ' ["/etc/skel/.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 -A40 '^file_permissions=(' "$STAGE/profiledef.sh" +echo "=== file_permissions after injection ==="; grep -A14 '^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 cf6ebbe..4bc2d40 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -4,43 +4,18 @@ BOS ships the generic **Mesa** stack. AMD and Intel work out of the box. -The proprietary NVIDIA driver is **not on the ISO**. NVIDIA firmware is -not on the image either (`linux-firmware-nvidia` stays commented out in -`packages.x86_64`). Default Hyprland env is vendor-neutral. - -On first graphical login, `bos-first-boot` probes `lspci` / `/proc` and, -if an NVIDIA GPU is present, writes `~/.local/state/bos/nvidia-offer.json` -and notifies that the proprietary driver is not on the ISO. It does **not** -install anything. - -The optional proprietary path is `bos-nvidia-setup` or the Settings → -Updates NVIDIA button. That installs `nvidia` + `nvidia-utils` (never -cuda) and writes `~/.config/hypr/nvidia.lua`. `hyprland.lua` dofiles that -drop-in **only if the file exists**, so Mesa machines stay unchanged. -Reboot after. Installing the packages by hand without the drop-in is not -enough for a working Hyprland session. - -The same probe leaves a HiDPI hint at `~/.local/state/bos/hidpi-hint.json` -when scale > 1 or the panel is dense; it never rewrites `monitors.json`. -A VM without `/dev/dri` gets a notification only. +**NVIDIA is unsupported.** The proprietary driver is not included, NVIDIA +firmware is not on the image, and there is no Hyprland NVIDIA env wiring. +Installing `nvidia` / `nvidia-utils` after the fact is not a product path. ## Recovery -An update that breaks the system is recovered by **reboot → GRUB -“snapshots” submenu** (grub-btrfs). `snapper rollback` will not change -what GRUB boots (`rootflags=subvol=@`). +An update that breaks the system is recovered from the **GRUB "snapshots" +submenu** (grub-btrfs), not `snapper rollback`. -`snapper rollback` swaps the default subvolume; the installed `grub.cfg` -still boots `@`. Pick the grub-btrfs entry so the kernel command line -matches the snapshot you want. +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. -BOS Settings → Snapshots lists snapshot number, date, and description so -you know which GRUB entry to pick. It does not roll the running root back -in place. 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 -`grub-install` NVRAM + `--removable` sequence as `post-install.sh`. - -A/B root swapping is not implemented. See the README Recovery section. +A/B root swapping is not implemented. See the README Recovery section for +the "system will not boot" GRUB/EFI repair path. diff --git a/docs/signed-repo.md b/docs/signed-repo.md index bda8656..112e254 100644 --- a/docs/signed-repo.md +++ b/docs/signed-repo.md @@ -1,28 +1,18 @@ # Signed `[breadway]` repo -**Status: live.** The ISO's `[breadway]` section is `SigLevel = Required` -and points at `https://dl.breadway.dev/arch/$arch`, where every -`.pkg.tar.zst` and the db carry a detached `.sig` from the BOS release key -(`56203B86…`, `KEYS.asc`, `releases@breadway.dev`). That key is trusted in -the pacman keyring at build time (`build-local.sh`), on the live medium -(`iso/airootfs/root/customize_airootfs.sh`), and on the installed target -(`iso/airootfs/etc/calamares/post-install.sh`). +Today the ISO's `[Breadway.os.git.breadway.dev]` section is +`SigLevel = Never`. That is TLS-only integrity: packages come from Forgejo's +Arch registry, which does **not** serve pacman-compatible database +signatures. `KEYS.asc` signs **ISO `SHA256SUMS` only** — it is not imported +as a pacman repo key. Do not flip `SigLevel` to `Required` on that section +until a signed repo exists and has been verified; Required without +signatures breaks the ISO and every installed system. -Forgejo publishing is unchanged: `package.yml` / packaging workflows still -PUT unsigned `.pkg.tar.zst` to Forgejo's Arch registry. The signed tree at -`dl.breadway.dev/arch` is rebuilt from that registry by -`.forgejo/workflows/signed-repo.yml` + `scripts/ci-publish-signed-repo.sh`. - -The rest of this doc is the original stand-up / verification procedure, -kept for reference and for re-verifying after key rotation. +The signed repo belongs at `https://dl.breadway.dev/arch`, not on Forgejo's +registry. ## Stand up `dl.breadway.dev/arch` -CI job: **Publish signed `[breadway]` repo** -(`.forgejo/workflows/signed-repo.yml`), host runner on hestia — **no -container**, so it can write `/srv/breadway-dl` like bakery releases. -breadlock `package.yml` uses `archlinux:latest` and cannot see host `/srv`. - Use the same release-signing key already in CI: - Public half: [`KEYS.asc`](../KEYS.asc) @@ -30,7 +20,6 @@ Use the same release-signing key already in CI: `releases@breadway.dev`) - Private half: the `GPG_PRIVATE_KEY` Forgejo secret (armoured secret key, no passphrase). Same secret `release-iso.yml` uses to sign `SHA256SUMS`. - The workflow **fails** if this secret is missing. Layout (example for `x86_64`): @@ -44,126 +33,53 @@ https://dl.breadway.dev/arch/x86_64/ breadway.files.sig ``` -On disk: `/srv/breadway-dl/arch/x86_64/` (nginx already serves -`/srv/breadway-dl` as `https://dl.breadway.dev/`). - -The job collects the current ISO `[breadway]` set from the Forgejo Arch -registry (breadlock + calamares, zen-browser-bin, bibata-cursor-theme-bin, -zsh-theme-powerlevel10k, yay-bin, python-pywal). Leftover bakery-channel pacman packages -still sitting in that registry are **not** copied. Optional -`BREADWAY_PKG_DIR` on the runner overrides individual files. - -Then it detach-signs each `.pkg.tar.zst` as a **binary** sidecar (pacman -wants `.sig`, not armoured `.asc`) and builds the database with -`repo-add -s`: +Build the database **and sign it** with `repo-add -s`: ```sh export GNUPGHOME=/tmp/gnupg-breadway-repo mkdir -m 700 -p "$GNUPGHOME" printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import -gpg --batch --yes --local-user releases@breadway.dev \ - --detach-sign breadlock--1-x86_64.pkg.tar.zst -# → breadlock--1-x86_64.pkg.tar.zst.sig - -cd /srv/breadway-dl/arch/x86_64 +cd /srv/dl.breadway.dev/arch/x86_64 repo-add -s -k releases@breadway.dev breadway.db.tar.gz *.pkg.tar.zst ``` `repo-add -s` writes `breadway.db.tar.gz.sig` (and the `.files` pair). Pacman fetches `
.db` + `
.db.sig` from `Server`. -## Dispatch the workflow - -Forgejo UI: **Actions → "Publish signed [breadway] repo" → Run workflow**. -Select `main`. - -API (`workflow_dispatch`): +Package signatures are separate from the database signature. Detach-sign +each `.pkg.tar.zst` as a **binary** sidecar (pacman wants `.sig`, not +armoured `.asc`): ```sh -curl -fsS -X POST \ - -H "Authorization: token ${RELEASE_TOKEN}" \ - -H "Content-Type: application/json" \ - "https://git.breadway.dev/api/v1/repos/Breadway/bos/actions/workflows/signed-repo.yml/dispatches" \ - -d '{"ref":"main"}' +gpg --batch --yes --local-user releases@breadway.dev \ + --detach-sign breadlock--1-x86_64.pkg.tar.zst +# → breadlock--1-x86_64.pkg.tar.zst.sig ``` -It also runs after the in-repo AUR republish workflows complete -(`calamares` / `bibata` / `powerlevel10k` / `yay-bin` / `python-pywal`). breadlock lives in -another repo; that job can fire this one with `repository_dispatch` event -`publish-signed-repo` (or dispatch from the UI after a breadlock tag). - -## Verify - -Confirm the signed db is actually served **before** touching ISO -`SigLevel` or `Server`: - -```sh -curl -fsSIL https://dl.breadway.dev/arch/x86_64/breadway.db -curl -fsSIL https://dl.breadway.dev/arch/x86_64/breadway.db.sig -``` - -Both must be HTTP 200. A 404 on `breadway.db.sig` means do **not** flip -`SigLevel` to `Required`. - -Import `KEYS.asc` and check the detached signatures: - -```sh -gpg --import KEYS.asc -curl -fsSL -o /tmp/breadway.db https://dl.breadway.dev/arch/x86_64/breadway.db -curl -fsSL -o /tmp/breadway.db.sig https://dl.breadway.dev/arch/x86_64/breadway.db.sig -gpg --verify /tmp/breadway.db.sig /tmp/breadway.db -``` - -On a throwaway Arch box (not the ISO tree): - -```sh -sudo pacman-key --add KEYS.asc -sudo pacman-key --lsign-key 56203B86A110695AE7F310934AF3323D678EB5E2 - -# Temporary /etc/pacman.conf snippet — do not commit this to the ISO: -# [breadway] -# SigLevel = Required -# Server = https://dl.breadway.dev/arch/$arch - -sudo pacman -Sy -``` - -`pacman -Sy` must fetch `breadway.db` + `breadway.db.sig` without -"missing or invalid signature". Then `pacman -Si breadlock` (and the AUR -republishes) should list the `[breadway]` section. - ## breadlock `package.yml` sidecar [`breadlock` `package.yml`](https://git.breadway.dev/Breadway/breadlock/src/branch/main/.forgejo/workflows/package.yml) -still `makepkg`s and PUTs the archive at Forgejo's registry. That path -stays; Never installs keep working. The signed tree is rebuilt by the bos -workflow above (registry fetch + sign + `repo-add -s`), not by writing -`/srv` from breadlock's container. - -## The ISO flip (done) - -All three steps have landed: - -1. **Key trusted.** The public key is committed at - `iso/airootfs/etc/pacman.d/breadway-repo.asc`. `build-local.sh` - `pacman-key --add` + `--lsign-key`s it into the build host keyring; - `customize_airootfs.sh` does the same in the airootfs; - `calamares/post-install.sh` re-does it in the target chroot. -2. **`Server`** in `iso/pacman.conf` and `iso/airootfs/etc/pacman.conf` - points at `https://dl.breadway.dev/arch/$arch`, section renamed to - `[breadway]` (matching `breadway.db`). -3. **`SigLevel = Required`** on that section. - -### Re-verify after any build - -In a VM booted from a fresh ISO: +already `makepkg`s and PUTs the archive at Forgejo's registry. When the +signed repo exists, that job can also emit the sidecar and publish both +files to `dl.breadway.dev/arch`: ```sh -sudo pacman -Sy # must fetch breadway.db + .sig, no signature error -sudo pacman -Si breadlock # lists the [breadway] section -sudo pacman -S --noconfirm yay-bin # installs with no key prompt +PKG=$(find packaging/arch -name '*.pkg.tar.zst' | head -1) +printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import +gpg --batch --yes --local-user releases@breadway.dev --detach-sign "$PKG" +# upload "$PKG" and "${PKG}.sig" to dl.breadway.dev/arch/x86_64/ +# then repo-add -s as above ``` -Then run the installer and, on the installed system, `sudo pacman -Sy` -again — the target keyring must already trust `56203B86…`. +Keep publishing to Forgejo until installs have been switched. The ISO +section stays `SigLevel = Never` until the signed tree is live. + +## After the signed repo exists + +1. Import `KEYS.asc` into the ISO keyring (`pacman-key --add` + `--lsign-key`). +2. Point `[breadway]` `Server` at `https://dl.breadway.dev/arch/$arch`. +3. Only then flip that section to `SigLevel = Required`. + +Do not do those three steps against Forgejo's registry. See +`iso/pacman.conf` and `iso/airootfs/etc/pacman.conf`. diff --git a/iso/airootfs/etc/bakery/config.toml b/iso/airootfs/etc/bakery/config.toml deleted file mode 100644 index 996b04a..0000000 --- a/iso/airootfs/etc/bakery/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -# Bakery desktop apps live under /usr/local so they ride snapper @ snapshots. -prefix = "/usr/local" diff --git a/iso/airootfs/etc/calamares/modules/packages.conf b/iso/airootfs/etc/calamares/modules/packages.conf index a3646e0..c327cb6 100644 --- a/iso/airootfs/etc/calamares/modules/packages.conf +++ b/iso/airootfs/etc/calamares/modules/packages.conf @@ -1,24 +1,10 @@ --- -# Optional online pacman refresh. The previous packages step used -# update_db:true with no skip/ignore, so `pacman -Sy` aborted offline -# installs (the case bos-netcheck exists for). skip_if_no_internet -# skips the whole module when Calamares sees no network; -# ignore_update_db_error keeps a flake-mirror -Sy from failing the -# install. update_system stays false — this is not a -Syu. -# -# try_install is empty: pipewire-pulse / pipewire-alsa already come -# from packages.x86_64 via unpackfs. No extra packages (and no -# nvidia) are pulled here. backend: pacman -skip_if_no_internet: true -update_db: true -ignore_update_db_error: true -update_system: false +options: + - update_db: true -pacman: - num_retries: 1 - disable_download_timeout: false - needed_only: true - -operations: [] +operations: + - try_install: + - pipewire-pulse + - pipewire-alsa diff --git a/iso/airootfs/etc/calamares/modules/welcome.conf b/iso/airootfs/etc/calamares/modules/welcome.conf index c31ea71..33bf7ad 100644 --- a/iso/airootfs/etc/calamares/modules/welcome.conf +++ b/iso/airootfs/etc/calamares/modules/welcome.conf @@ -3,19 +3,9 @@ showSupportUrl: false showKnownIssuesUrl: false showReleaseNotesUrl: false -# 3.4.2 schema: `check` is shown; only `required` blocks Next. Internet is -# informational so offline installs proceed. Do not probe archlinux.org. requirements: requiredStorage: 20 requiredRam: 2.0 - internetCheckUrl: "https://breadway.dev" - check: - - storage - - ram - - power - - internet - - root - required: - - storage - - ram - - root + checkInternet: true + checkPower: true + internetCheckUrl: "https://archlinux.org" diff --git a/iso/airootfs/etc/calamares/post-install.sh b/iso/airootfs/etc/calamares/post-install.sh index d7df9be..2076dcf 100644 --- a/iso/airootfs/etc/calamares/post-install.sh +++ b/iso/airootfs/etc/calamares/post-install.sh @@ -8,6 +8,8 @@ # Best-effort: do NOT use `set -e`; a single failure here must not abort the rest. set -uo pipefail +MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)" + # Whether Calamares encrypted the root partition (LUKS) — checked once here, # used below to conditionally wire mkinitcpio's encrypt hook and GRUB's # cryptodisk support. `lsblk TYPE` reports "crypt" for a cryptsetup-opened @@ -31,14 +33,6 @@ rm -f /usr/local/bin/bos-live-setup /usr/local/bin/bos-launch-calamares rm -f /etc/sudoers.d/99-bos-live userdel -r liveuser 2>/dev/null || true -# Live ISO creates liveuser as UID 1000; Calamares then creates the real -# account as 1001. Capture AFTER userdel so Snapper ALLOW_USERS and skel -# copy the installed user, not the deleted live account. -MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)" -if [[ -z "$MAIN_USER" || "$MAIN_USER" == "liveuser" ]]; then - MAIN_USER="$(getent passwd | awk -F: '$3 >= 1000 && $3 < 60000 && $1 != "liveuser" { print $1; exit }')" -fi - # unpackfs copies the entire live squashfs onto the target. Remove live-only # packages (Calamares + archiso boot chain + memtest/EFI-shell payloads) so # they do not stay on disk forever. pacman -Rs (not -Rns) keeps /etc configs @@ -73,23 +67,15 @@ 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 verifies official -# Arch packages; the BOS release key (56203B86…, shipped at -# /etc/pacman.d/breadway-repo.asc) verifies the signed [breadway] repo at -# dl.breadway.dev/arch — SigLevel = Required there, every package and the db -# carry a .sig from it. +# 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. # --------------------------------------------------------------------------- -BREADWAY_KEY_FPR="56203B86A110695AE7F310934AF3323D678EB5E2" if command -v pacman-key &>/dev/null; then pacman-key --init || echo "WARN: pacman-key --init failed" pacman-key --populate archlinux || echo "WARN: pacman-key --populate failed" - if [[ -f /etc/pacman.d/breadway-repo.asc ]]; then - pacman-key --add /etc/pacman.d/breadway-repo.asc \ - && pacman-key --lsign-key "$BREADWAY_KEY_FPR" \ - || echo "WARN: could not trust the [breadway] repo key — pacman -Sy will fail on [breadway]" - else - echo "WARN: /etc/pacman.d/breadway-repo.asc missing — [breadway] (SigLevel=Required) will not verify" - fi fi # --------------------------------------------------------------------------- @@ -439,20 +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: -# 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 +# 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. -# -# systemd --user units in /usr/lib/systemd/user are not enabled for new -# accounts unless enabled --global (or the user enables them). Do that here -# so a later `useradd -m` starts breadd / breadbox-sync / breadclipd / -# breadcrumbs / breadmill on first login. Safe if the helper is missing. -if [[ -x /usr/local/bin/bos-enable-bakery-user-units ]]; then - /usr/local/bin/bos-enable-bakery-user-units \ - || echo "WARN: enabling bakery user units globally failed" -fi # --------------------------------------------------------------------------- # Deploy dotfiles + the bakery bread ecosystem into the user's home (Calamares diff --git a/iso/airootfs/etc/calamares/settings.conf b/iso/airootfs/etc/calamares/settings.conf index 1daec33..8872182 100644 --- a/iso/airootfs/etc/calamares/settings.conf +++ b/iso/airootfs/etc/calamares/settings.conf @@ -35,6 +35,12 @@ sequence: - users - networkcfg - hwclock + # packages module removed: it set update_db:true with no + # skip_if_no_internet/ignore_update_db_error, so an offline install (the + # exact case bos-welcome's nmtui step exists for) aborted here with a + # fatal pacman -Sy failure. Its only try_install packages (pipewire-pulse, + # pipewire-alsa) are already in packages.x86_64 and installed by + # unpackfs, so the step did nothing useful even when it succeeded. # archiso strips the kernel from the squashfs; stage it, drop the archiso # initramfs config, and write a stock mkinitcpio preset before initcpio runs. - shellprocess@kernel @@ -51,12 +57,6 @@ sequence: # BOS finalization: GRUB install + cleanup + snapper + services + dotfiles. # All fast, and runs after initcpio so /boot has the kernel + initramfs. - shellprocess - # Optional online pacman -Sy. After post-install so the target keyring - # exists. skip_if_no_internet + ignore_update_db_error: an offline - # install (or a flake-mirror -Sy) must not abort. operations is empty — - # pipewire-pulse/alsa already come from unpackfs; nothing extra (and - # no nvidia) is installed here. - - packages - umount - show: - finished diff --git a/iso/airootfs/etc/default/useradd b/iso/airootfs/etc/default/useradd index 4cae86c..f16b7d8 100644 --- a/iso/airootfs/etc/default/useradd +++ b/iso/airootfs/etc/default/useradd @@ -3,8 +3,5 @@ GROUP=users HOME=/home INACTIVE=-1 EXPIRE= -# useradd -m copies Hyprland + bakery per-user state from here. Bakery -# binaries live in /usr/local/bin (not skel). User units are enabled -# --global so a second account starts them on first login. SKEL=/etc/skel CREATE_MAIL_SPOOL=no diff --git a/iso/airootfs/etc/greetd/breadgreet.toml b/iso/airootfs/etc/greetd/breadgreet.toml index 24b48a4..311dfad 100644 --- a/iso/airootfs/etc/greetd/breadgreet.toml +++ b/iso/airootfs/etc/greetd/breadgreet.toml @@ -7,9 +7,8 @@ # 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 -# 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 +# 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 # (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 2f53c10..7e15c7a 100644 --- a/iso/airootfs/etc/pacman.conf +++ b/iso/airootfs/etc/pacman.conf @@ -29,20 +29,21 @@ 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 /usr/local at ISO build time. +# are NOT here; they are bakery-baked into /etc/skel at ISO build time. # # Packages are published to the Forgejo Arch registry (group "os") by the -# .forgejo/workflows/*.yml workflows; scripts/ci-publish-signed-repo.sh then -# collects them, detach-signs each .pkg.tar.zst with the BOS release key -# (releases@breadway.dev), runs `repo-add -s`, and publishes the signed db -# at https://dl.breadway.dev/arch/$arch (signed-repo.yml). +# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). # -# SigLevel = Required: every package AND the db carry a .sig from key -# 56203B86A110695AE7F310934AF3323D678EB5E2 — the same key committed as -# KEYS.asc / etc/pacman.d/breadway-repo.asc, imported into the pacman -# keyring at build time (build-local.sh), on the live medium, and on the -# installed target (calamares/post-install.sh). +# 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. # ----------------------------------------------------------------------- -[breadway] -SigLevel = Required -Server = https://dl.breadway.dev/arch/$arch +# The section name must match Forgejo's served db filename +# ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. +[Breadway.os.git.breadway.dev] +SigLevel = Never +Server = https://git.breadway.dev/api/packages/Breadway/arch/os/$arch diff --git a/iso/airootfs/etc/pacman.d/breadway-repo.asc b/iso/airootfs/etc/pacman.d/breadway-repo.asc deleted file mode 100644 index fe380fd..0000000 --- a/iso/airootfs/etc/pacman.d/breadway-repo.asc +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEakhwGhYJKwYBBAHaRw8BAQdA/sZ/GYec5M2MD+w20mVF5tMUhGji210Dg7zL -TAhNsg60WUJPUyBSZWxlYXNlIFNpZ25pbmcgKGdpdC5icmVhZHdheS5kZXYvQnJl -YWR3YXkvYm9zIHJlbGVhc2VzIG9ubHkpIDxyZWxlYXNlc0BicmVhZHdheS5kZXY+ -iJYEExYKAD4WIQRWIDuGoRBpWufzEJNK8zI9Z4614gUCakhwGgIbIwUJA8JnAAUL -CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRBK8zI9Z4614ggYAQDP8FTZ14i9YPKD -ARvZuP5QaYOUFhQ8uyG0CowXKy9O0AEAqYfjnvyJI3N651pVFSNUXyP16w1kMPSs -K0g3CLsztQ+4OARqSHAaEgorBgEEAZdVAQUBAQdAuJFuy2GHz5m9wXTm/PdSpLE9 -gERwHOLyM1OFuttrJW4DAQgHiH4EGBYKACYWIQRWIDuGoRBpWufzEJNK8zI9Z461 -4gUCakhwGgIbDAUJA8JnAAAKCRBK8zI9Z4614nzLAP9grcIFsAAeCyVKhziHmpXq -E0Hm6FfIr4sdEf63HZkyfwD/XeKeWfb3EWvVsloJrZZ9tDmR67iK52Hwl82wfFAU -cAo= -=Mrh1 ------END PGP PUBLIC KEY BLOCK----- diff --git a/iso/airootfs/etc/profile.d/bos-local-bin.sh b/iso/airootfs/etc/profile.d/bos-local-bin.sh index 734db46..642af43 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 @@ -# 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). +# 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). case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; diff --git a/iso/airootfs/etc/skel/.config/bread/modules/external-monitors.lua b/iso/airootfs/etc/skel/.config/bread/modules/external-monitors.lua deleted file mode 100644 index 27a276c..0000000 --- a/iso/airootfs/etc/skel/.config/bread/modules/external-monitors.lua +++ /dev/null @@ -1,228 +0,0 @@ --- external-monitors — behave like a normal laptop desktop --- --- Plug in any display (HDMI, DisplayPort, USB-C dock, a random TV) and --- the session just works. No output names to edit. --- --- • the laptop panel stays at its preferred (native) mode --- • each external uses its preferred mode and refresh --- • new screens clone the laptop (set ARRANGE = "extend" to sit to the right) --- • closing the lid does not sleep while an external is on --- • unplug everything and the laptop is the only display again --- --- Drop-in: copy to ~/.config/bread/modules/ and `bread reload`. - -local M = bread.module({ - name = "external-monitors", - version = "1.0.0", - after = { "bread.monitors" }, -}) - --- "mirror" = every external clones the laptop (presentations, TVs) --- "extend" = extra desktop to the right -local ARRANGE = "mirror" -local SCALE = "auto" - -local INTERNAL_RE = "^eDP" -local INHIBITOR = "/tmp/bread-lid-inhibitor.pid" - -local function inhibit_lid() - if bread.fs.exists(INHIBITOR) then return end - bread.exec( - "bash -c 'systemd-inhibit --what=handle-lid-switch --who=bread " - .. "--why=external-display sleep infinity & echo $! > " - .. INHIBITOR - .. "'" - ) -end - -local function release_lid() - bread.exec( - "bash -c 'kill $(cat " .. INHIBITOR .. " 2>/dev/null) 2>/dev/null; rm -f " .. INHIBITOR .. "'" - ) -end - -local function is_internal(name) - return type(name) == "string" and name:match(INTERNAL_RE) ~= nil -end - -local function drm_status(name) - for card = 0, 5 do - local raw = bread.fs.read(string.format("/sys/class/drm/card%d-%s/status", card, name)) - if raw then - return raw:match("^%s*(%S+)") - end - end - return nil -end - -local function drm_first_mode(name) - for card = 0, 5 do - local raw = bread.fs.read(string.format("/sys/class/drm/card%d-%s/modes", card, name)) - if raw then - local w, h = raw:match("(%d+)x(%d+)") - if w then - return tonumber(w), tonumber(h) - end - end - end - return 1920, 1080 -end - -local function list_connectors() - local names = {} - local ok, out = bread.exec_capture("ls /sys/class/drm", { timeout_ms = 500 }) - if not ok or not out then - return names - end - for ent in out:gmatch("[^%s]+") do - local name = ent:match("^card%d+%-(.+)$") - if name and not name:match("^Writeback") then - names[#names + 1] = name - end - end - table.sort(names) - return names -end - -local function connected() - local internal, externals = nil, {} - for _, name in ipairs(list_connectors()) do - if drm_status(name) == "connected" then - if is_internal(name) then - internal = internal or name - else - externals[#externals + 1] = name - end - end - end - return internal or "eDP-1", externals -end - --- BOS Hyprland talks Lua (`hl.monitor`). Stock Hyprland uses the --- `monitor=` keyword. Try eval first, then keyword. -local function apply_monitor(opts) - local extra = "" - if opts.mirror and opts.mirror ~= "" then - extra = string.format(", mirror = %q", opts.mirror) - end - local expr = string.format( - "hl.monitor({ output = %q, mode = %q, position = %q, scale = %q%s })", - opts.output, - opts.mode or "preferred", - opts.position or "0x0", - opts.scale or SCALE, - extra - ) - local resp = bread.hyprland.eval(expr) - if type(resp) == "string" and resp:match("error") then - local spec = string.format( - "%s, %s, %s, %s", - opts.output, - opts.mode or "preferred", - opts.position or "0x0", - opts.scale or SCALE - ) - if opts.mirror and opts.mirror ~= "" then - spec = spec .. ", mirror, " .. opts.mirror - end - bread.hyprland.keyword("monitor", spec) - end -end - -local function apply(internal, externals) - apply_monitor({ - output = internal, - mode = "preferred", - position = "0x0", - scale = SCALE, - }) - - if ARRANGE == "mirror" then - for _, name in ipairs(externals) do - apply_monitor({ - output = name, - mode = "preferred", - position = "0x0", - scale = SCALE, - mirror = internal, - }) - end - return - end - - local x = select(1, drm_first_mode(internal)) or 1920 - for _, name in ipairs(externals) do - apply_monitor({ - output = name, - mode = "preferred", - position = x .. "x0", - scale = SCALE, - }) - local w = select(1, drm_first_mode(name)) or 1920 - x = x + w - end -end - -function M.on_load() - local last = nil - local applied = false - - local function evaluate() - local internal, externals = connected() - local sig = internal .. "|" .. table.concat(externals, ",") - if sig == last then - return - end - last = sig - - if #externals == 0 then - if applied then - apply_monitor({ - output = internal, - mode = "preferred", - position = "0x0", - scale = SCALE, - }) - release_lid() - applied = false - end - return - end - - apply(internal, externals) - inhibit_lid() - applied = true - bread.log("[external-monitors] " .. internal .. " + " .. table.concat(externals, ", ")) - end - - local settle = bread.debounce(1500, evaluate) - - bread.on("bread.hyprland.monitor.connected", function(event) - local name = event.data and event.data.name - if name and not is_internal(name) then - bread.notify("Display connected: " .. name, { urgency = "low" }) - end - settle() - end) - - bread.on("bread.hyprland.monitor.disconnected", function() - settle() - end) - - bread.on("bread.device.**", function(event) - local sub = event.data and event.data.subsystem - if sub == "drm" then - settle() - end - end) - - bread.hyprland.on_raw("configreloaded", function() - last = nil - evaluate() - end) - - bread.every(3000, evaluate) - settle() -end - -return M diff --git a/iso/airootfs/etc/skel/.config/hypr/autostart.json b/iso/airootfs/etc/skel/.config/hypr/autostart.json index be2b4ac..87de11a 100644 --- a/iso/airootfs/etc/skel/.config/hypr/autostart.json +++ b/iso/airootfs/etc/skel/.config/hypr/autostart.json @@ -3,11 +3,9 @@ { "command": "breadbar", "label": "Bar (breadbar)", "enabled": true }, { "command": "hypridle", "label": "Idle / lock daemon (hypridle)", "enabled": true }, { "command": "bos-netcheck", "label": "Network connectivity check", "enabled": true }, - { "command": "bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'", "label": "First-boot hardware probe", "enabled": true }, { "command": "breadhelp --autostart", "label": "BOS Help (first-run onboarding)", "enabled": true }, { "command": "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", "label": "Wallpaper command bus (breadpaper listen)", "enabled": true }, { "command": "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", "label": "Screenshot command bus (breadshot listen)", "enabled": true }, - { "command": "bash -c 'command -v breadlock >/dev/null && exec breadlock listen'", "label": "Lock command bus (breadlock listen)", "enabled": true }, { "command": "bash -c 'command -v breadbox >/dev/null && exec breadbox listen'", "label": "Launcher command bus (breadbox listen)", "enabled": true }, { "command": "bash -c 'command -v breadhelp >/dev/null && exec breadhelp listen'", "label": "Help command bus (breadhelp listen)", "enabled": true }, { "command": "bash -c 'command -v breadsearch >/dev/null && exec breadsearch listen'", "label": "Search command bus (breadsearch listen)", "enabled": true }, diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua index cfa488b..57360a3 100644 --- a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua @@ -63,16 +63,6 @@ end hl.window_rule({ name = "breadhelp", match = { class = "^(com\\.breadway\\.breadhelp)$" }, float = true, size = { 880, 600 } }) hl.window_rule({ name = "bos-netsetup", match = { class = "^(bos-netsetup)$" }, float = true, size = { 700, 560 } }) --- --------------------------------------------------------------------------- --- Layer-shell rules — blur / ignore-alpha / per-namespace motion for the --- bread shell (breadbar island + osd + notif + panel, breadbox launcher, --- breadclip popup). Theme-driven via ~/.config/hypr/layerrules.json (written --- by `bread-theme layerrules` in the bootstrap below), with a hardcoded --- fallback so a missing/broken theme file can never leave the shell --- unblurred. Without these the launcher and popups get no compositor blur. --- --------------------------------------------------------------------------- -pcall(dofile, script_dir .. "ui/rules.lua") - -- --------------------------------------------------------------------------- -- Environment (vendor-neutral; no GPU-specific vars so it works on Intel/AMD). -- --------------------------------------------------------------------------- @@ -86,26 +76,6 @@ hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1") hl.env("SDL_VIDEODRIVER", "wayland") hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") hl.env("_JAVA_AWT_WM_NONREPARENTING", "1") --- GTK4's default renderer (ngl/vulkan on GTK 4.14+) renders transparent --- layer-shell surfaces as opaque black on wlroots — the whole bread shell --- (breadbox launcher, breadclip popup, breadbar, breadhelp, bos-settings) --- goes black-on-black, and it's worse under a VM's virtio-gpu where no real --- GL/Vulkan context is available. cairo (software) always composites --- transparency correctly; the shell is flat colour/text/icons so there's no --- visible perf cost, and idle memory drops (~40%, no Mesa driver resident). --- Set GSK_RENDERER=gl in the session to override for a GPU-heavy GTK4 app. -hl.env("GSK_RENDERER", "cairo") - --- Optional NVIDIA env from bos-nvidia-setup. Mesa machines have no file. --- bos-nvidia-setup: optional proprietary env; no-op when the file is absent -do - local nvidia = (os.getenv("HOME") or "") .. "/.config/hypr/nvidia.lua" - local f = io.open(nvidia, "r") - if f then - f:close() - pcall(dofile, nvidia) - end -end -- kitty sets its own background_opacity (see kitty.conf), so the global blur -- above blurs behind the terminal while keeping text fully opaque. @@ -147,12 +117,6 @@ hl.on("hyprland.start", function() -- Generate the shared bread GUI stylesheet first, so breadbar/breadbox/ -- bos-settings load it on start (they also live-reload if it changes). "bread-theme generate", - -- Write ~/.config/hypr/layerrules.json from the active shell theme's - -- [compositor] table. scripts/ui/rules.lua (loaded above) already ran - -- with the hardcoded fallback; this refreshes the JSON for the next - -- `hyprctl reload` / theme switch. Harmless if bread-theme lacks the - -- verb on an older build. - "sh -c 'bread-theme layerrules 2>/dev/null || true'", -- Global dark theme: GTK4/libadwaita + GTK3 theme + icon + cursor. "gsettings set org.gnome.desktop.interface color-scheme prefer-dark", "gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark", @@ -160,12 +124,9 @@ 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 from /usr/lib/systemd/user — see - -- build-local.sh's service bake) rather than an exec-once here. - -- Prefer bread-polkit if it is on PATH (not baked; lockfile does not - -- ship it). Otherwise the ISO's polkit-gnome agent. command -v so a - -- missing binary does not leave the session without an auth agent. - "sh -c 'if command -v bread-polkit >/dev/null; then exec bread-polkit; else exec /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1; fi'", + -- service (auto-started via skel — 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). -- Raw `awww img`, NOT `breadpaper set` — breadpaper set also runs real @@ -179,9 +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 (/usr/lib/systemd/user/breadd.service, - -- enabled --global so every account starts it). It autostarts at login - -- but before Hyprland exists, so + -- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service, + -- enabled in skel). 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", @@ -202,8 +162,7 @@ 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 /usr/lib/systemd/user, - -- independent of this list. + -- runs on login via the unit baked into skel, independent of this list. local ok, extra = pcall(function() return dofile(script_dir .. "system/autostart.lua")() end) @@ -215,11 +174,9 @@ hl.on("hyprland.start", function() "breadbar", "hypridle", "bos-netcheck", - "bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'", "breadhelp --autostart", "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", - "bash -c 'command -v breadlock >/dev/null && exec breadlock listen'", "bash -c 'command -v breadbox >/dev/null && exec breadbox listen'", "bash -c 'command -v breadhelp >/dev/null && exec breadhelp listen'", "bash -c 'command -v breadsearch >/dev/null && exec breadsearch listen'", diff --git a/iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua b/iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua index 63e364f..ce4465f 100644 --- a/iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua +++ b/iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua @@ -18,11 +18,9 @@ local DEFAULT_EXTRA = { { command = "breadbar", enabled = true }, { command = "hypridle", enabled = true }, { command = "bos-netcheck", enabled = true }, - { command = "bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'", enabled = true }, { command = "breadhelp --autostart", enabled = true }, { command = "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", enabled = true }, { command = "bash -c 'command -v breadshot >/dev/null && exec breadshot listen'", enabled = true }, - { command = "bash -c 'command -v breadlock >/dev/null && exec breadlock listen'", enabled = true }, { command = "bash -c 'command -v breadbox >/dev/null && exec breadbox listen'", enabled = true }, { command = "bash -c 'command -v breadhelp >/dev/null && exec breadhelp listen'", enabled = true }, { command = "bash -c 'command -v breadsearch >/dev/null && exec breadsearch listen'", enabled = true }, diff --git a/iso/airootfs/etc/skel/.config/hypr/scripts/ui/rules.lua b/iso/airootfs/etc/skel/.config/hypr/scripts/ui/rules.lua deleted file mode 100644 index 79e0a2f..0000000 --- a/iso/airootfs/etc/skel/.config/hypr/scripts/ui/rules.lua +++ /dev/null @@ -1,136 +0,0 @@ --- scripts/ui/rules.lua — layer-shell (compositor) rules for the bread shell. --- --- Blur / ignore-alpha / per-namespace motion for breadbar (island + osd + --- notif + panel + dismiss), the breadbox launcher, and the breadclip popup. --- Window rules (float/centre for onboarding popups) live in hyprland.lua; --- this file is layer rules only. --- --- Theme-driven (bos-ui-demos THEME_SYSTEM_PLAN.md §9): `bread-theme --- layerrules` writes ~/.config/hypr/layerrules.json from the active shell --- theme's [compositor] table — blur / ignore_alpha / blur_popups / --- animation / no_anim, keyed by layer-shell namespace. That table owns --- *appearance* only; placement / workspace / focus are never touched here. --- If the JSON is missing, unreadable, malformed, or anything goes wrong --- while parsing it, apply_hardcoded_layer_rules() runs instead — the exact --- set this file hardcoded before the theme system existed — so a broken or --- absent theme file can never disable compositor blur. - -local LAYERRULES_JSON = os.getenv("HOME") .. "/.config/hypr/layerrules.json" - -local function apply_hardcoded_layer_rules() - hl.layer_rule({ - name = "breadbar-island", - match = { namespace = "^breadbar$" }, - blur = true, - ignore_alpha = 0.2, - blur_popups = true, - animation = "slide top", - }) - - hl.layer_rule({ - name = "breadbar-osd", - match = { namespace = "^breadbar-osd$" }, - blur = true, - ignore_alpha = 0.2, - animation = "slide bottom", - }) - - hl.layer_rule({ - name = "breadbar-notif", - match = { namespace = "^breadbar-notif$" }, - blur = true, - ignore_alpha = 0.2, - animation = "slide right", - }) - - hl.layer_rule({ - name = "breadbar-panel", - match = { namespace = "^breadbar-panel$" }, - blur = true, - ignore_alpha = 0.2, - animation = "slide right", - }) - - hl.layer_rule({ - name = "breadbar-dismiss", - match = { namespace = "^breadbar-dismiss$" }, - no_anim = true, - }) - - hl.layer_rule({ - match = "breadbox", - blur = true, - ignore_alpha = 0.2, - }) -end - --- Parses ~/.config/hypr/layerrules.json into a plain array of per-namespace --- rule tables, sorted by namespace for a deterministic emission order. --- Returns nil (not an error) for anything short of a well-formed, non-empty --- JSON object, so the pcall wrapping this only guards against a genuine Lua --- error (e.g. json.lua failing to load) rather than every malformed case. -local function parsed_layer_rules() - local json = dofile(os.getenv("HOME") .. "/.config/hypr/scripts/lib/json.lua") - local parsed = json.load(LAYERRULES_JSON) - if type(parsed) ~= "table" then - return nil - end - - local namespaces = {} - for ns, rule in pairs(parsed) do - if type(ns) == "string" and type(rule) == "table" then - namespaces[#namespaces + 1] = ns - end - end - if #namespaces == 0 then - return nil - end - table.sort(namespaces) - - local rules = {} - for _, ns in ipairs(namespaces) do - local r = parsed[ns] - rules[#rules + 1] = { - namespace = ns, - blur = r.blur == true, - ignore_alpha = r.ignore_alpha, - blur_popups = r.blur_popups == true, - animation = r.animation, - no_anim = r.no_anim == true, - } - end - return rules -end - --- Emits one hl.layer_rule per parsed namespace. Built as pure data by --- parsed_layer_rules() first (no hl.layer_rule calls during parsing), so a --- parse failure can never leave a partial JSON-derived rule set applied --- before the caller falls back to apply_hardcoded_layer_rules(). -local function apply_json_layer_rules(rules) - for _, r in ipairs(rules) do - hl.layer_rule({ - name = r.namespace, - match = { namespace = "^" .. r.namespace .. "$" }, - blur = r.blur, - ignore_alpha = r.ignore_alpha, - blur_popups = r.blur_popups, - animation = r.animation, - no_anim = r.no_anim, - }) - end -end - -local ok, rules = pcall(parsed_layer_rules) -if ok and rules then - apply_json_layer_rules(rules) -else - apply_hardcoded_layer_rules() -end - --- breadclip is not part of the shell theme's [compositor] table yet — always --- hardcoded, regardless of the JSON above. -hl.layer_rule({ - match = "breadclip", - blur = true, - ignore_alpha = 0.0, -}) diff --git a/iso/airootfs/etc/skel/.config/systemd/user/breadd.service b/iso/airootfs/etc/skel/.config/systemd/user/breadd.service index 945c09c..49d6741 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 -# System-prefix bakery install — same path for every account. -ExecStart=/usr/local/bin/breadd +# %h = the user's home — works for any account created from this skel. +ExecStart=%h/.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 3de4cc4..e4f1e69 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' -# Per-user tools. Bakery desktop apps live in /usr/local/bin (already on PATH). +# ~/.local/bin holds the bread* binaries baked in at build time. export PATH="$HOME/.local/bin:$PATH" # Powerlevel10k prompt configuration. diff --git a/iso/airootfs/etc/systemd/user/default.target.wants/breadd.service b/iso/airootfs/etc/systemd/user/default.target.wants/breadd.service deleted file mode 120000 index d284527..0000000 --- a/iso/airootfs/etc/systemd/user/default.target.wants/breadd.service +++ /dev/null @@ -1 +0,0 @@ -/usr/lib/systemd/user/breadd.service \ No newline at end of file diff --git a/iso/airootfs/root/customize_airootfs.sh b/iso/airootfs/root/customize_airootfs.sh deleted file mode 100644 index 149c6c4..0000000 --- a/iso/airootfs/root/customize_airootfs.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# Run by mkarchiso inside the airootfs chroot, after packages are installed -# and before the squashfs is built. (archiso prints a deprecation warning for -# this hook, but there is no non-deprecated replacement for "trust an extra -# pacman repo key in the image keyring", and BOS ships no pacman-init.service.) -# -# Purpose: trust the BOS release key (56203B86…) in the image's pacman -# keyring so the signed [breadway] repo (SigLevel = Required, -# https://dl.breadway.dev/arch) verifies both on the live medium and — via -# calamares' unpackfs, which copies this squashfs to the target — on the -# installed system. calamares/post-install.sh re-does this in the target -# chroot as a fallback (unpackfs can skip /etc/pacman.d/gnupg). -set -euo pipefail - -BREADWAY_KEY_FPR="56203B86A110695AE7F310934AF3323D678EB5E2" -KEY_FILE="/etc/pacman.d/breadway-repo.asc" - -pacman-key --init -pacman-key --populate archlinux - -if [[ -f "$KEY_FILE" ]]; then - pacman-key --add "$KEY_FILE" - pacman-key --lsign-key "$BREADWAY_KEY_FPR" - echo "customize_airootfs: trusted [breadway] repo key $BREADWAY_KEY_FPR" -else - echo "customize_airootfs: WARNING $KEY_FILE missing; [breadway] will not verify" >&2 -fi diff --git a/iso/airootfs/usr/lib/systemd/user-preset/90-bos-bakery.preset b/iso/airootfs/usr/lib/systemd/user-preset/90-bos-bakery.preset deleted file mode 100644 index f7f73f9..0000000 --- a/iso/airootfs/usr/lib/systemd/user-preset/90-bos-bakery.preset +++ /dev/null @@ -1,12 +0,0 @@ -# Bakery systemd --user units. `systemctl --global enable` (post-install and -# live setup) applies these so a later `useradd -m` starts them on first login. -# Bake rewrites this list from the units actually copied into the image. -# -# breadclipd is WantedBy=graphical-session.target. BOS does not activate that -# target (no uwsm); Hyprland still `systemctl --user start`s it after the -# compositor is up. --global enable still records it for every account. -enable breadd.service -enable breadbox-sync.service -enable breadclipd.service -enable breadcrumbs.service -enable breadmill.service diff --git a/iso/airootfs/usr/local/bin/bos-enable-bakery-user-units b/iso/airootfs/usr/local/bin/bos-enable-bakery-user-units deleted file mode 100755 index 46c128e..0000000 --- a/iso/airootfs/usr/local/bin/bos-enable-bakery-user-units +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash -# Enable bakery systemd --user units for every account (current and future). -# -# `systemctl --global enable` writes /etc/systemd/user/.wants/ so a -# later `useradd -m` does not need per-home enablement. Bins live in -# /usr/local; only per-user state comes from skel. -# -# Safe on the live image and in the Calamares post-install chroot. -# Idempotent. Does not start units (no user session required). -# -# breadclipd is WantedBy=graphical-session.target. BOS does not activate -# that target (no uwsm), so Hyprland still `systemctl --user start`s it. -# --global enable still records it for every account / bos-settings. -set -uo pipefail - -UNITS_DIR=/usr/lib/systemd/user -PRESET=/usr/lib/systemd/user-preset/90-bos-bakery.preset - -is_blocked() { - case "$1" in - breadcast*|breadarr*) return 0 ;; - *) return 1 ;; - esac -} - -is_bakery_unit() { - local unit="$1" path="$UNITS_DIR/$unit" - [[ -f "$path" ]] || return 1 - is_blocked "$unit" && return 1 - grep -qE '^ExecStart=/usr/local/bin/' "$path" -} - -list_from_preset() { - [[ -f "$PRESET" ]] || return 0 - awk '/^enable[[:space:]]/ { print $2 }' "$PRESET" -} - -list_from_units_dir() { - [[ -d "$UNITS_DIR" ]] || return 0 - local path unit - for path in "$UNITS_DIR"/*.service; do - [[ -f "$path" ]] || continue - unit="$(basename "$path")" - is_bakery_unit "$unit" && printf '%s\n' "$unit" - done -} - -list_from_installed_json() { - local json=/etc/skel/.local/state/bakery/installed.json - [[ -f "$json" ]] || return 0 - command -v python3 >/dev/null 2>&1 || return 0 - python3 - "$json" <<'PY' -import json, sys -with open(sys.argv[1]) as f: - data = json.load(f) -for pkg in data.get("packages", data).values(): - if not isinstance(pkg, dict): - continue - for svc in pkg.get("services") or []: - name = svc["unit"] if isinstance(svc, dict) else svc - if name and not str(name).startswith(("breadcast", "breadarr")): - print(name) -PY -} - -mapfile -t units < <( - { list_from_preset; list_from_units_dir; list_from_installed_json; } \ - | sed '/^$/d' | sort -u -) - -if [[ ${#units[@]} -eq 0 ]]; then - echo "WARN: no bakery user units found to enable globally" - exit 0 -fi - -if ! command -v systemctl >/dev/null 2>&1; then - echo "WARN: systemctl missing — cannot --global enable bakery user units" - exit 0 -fi - -for unit in "${units[@]}"; do - [[ -f "$UNITS_DIR/$unit" ]] || continue - is_blocked "$unit" && continue - if ! grep -q '^\[Install\]' "$UNITS_DIR/$unit"; then - echo "WARN: $unit has no [Install] section — skip --global enable" - continue - fi - systemctl --global enable "$unit" \ - || echo "WARN: systemctl --global enable $unit failed" -done diff --git a/iso/airootfs/usr/local/bin/bos-first-boot b/iso/airootfs/usr/local/bin/bos-first-boot deleted file mode 100755 index d17fc70..0000000 --- a/iso/airootfs/usr/local/bin/bos-first-boot +++ /dev/null @@ -1,187 +0,0 @@ -#!/bin/bash -# bos-first-boot — one-shot hardware probe after the first graphical login. -# -# Detects NVIDIA (offer file + notify; never auto-installs a driver), a VM -# without GL, and HiDPI (hint file only — never rewrites monitors.json). -# -# Non-fatal: missing tools, notify-send, or hyprctl must not block login. -# Guarded with `command -v`. Flag: ~/.local/state/bos/first-boot-done. -set -u - -STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/bos" -FLAG="$STATE_DIR/first-boot-done" -NVIDIA_OFFER="$STATE_DIR/nvidia-offer.json" -HIDPI_HINT="$STATE_DIR/hidpi-hint.json" -VM_HINT="$STATE_DIR/vm-gl-hint.json" - -# Never run on the live/installer session — only on an installed system. -[[ "$(id -un)" == "liveuser" ]] && exit 0 - -# Already probed this home. -[[ -f "$FLAG" ]] && exit 0 - -notify() { - local msg="$1" - local urgency="${2:-normal}" - command -v notify-send >/dev/null 2>&1 || return 0 - notify-send -u "$urgency" "BOS" "$msg" 2>/dev/null || true -} - -json_escape() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -iso_now() { - date -Iseconds 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ -} - -# Best-effort: hyprland.start can beat the notification daemon by a beat. -if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then - sleep 1 -fi - -mkdir -p "$STATE_DIR" 2>/dev/null || exit 0 - -# --------------------------------------------------------------------------- -# NVIDIA — hardware only. Do not install nvidia / nvidia-utils. -# --------------------------------------------------------------------------- -nvidia_present=0 -nvidia_pci="" -if command -v lspci >/dev/null 2>&1; then - nvidia_pci="$(lspci -d 10de: -nn 2>/dev/null | grep -iE 'VGA|3D|Display' || true)" - [[ -n "$nvidia_pci" ]] && nvidia_present=1 -fi -if [[ "$nvidia_present" != "1" ]]; then - if [[ -d /proc/driver/nvidia || -d /sys/module/nvidia ]]; then - nvidia_present=1 - nvidia_pci="${nvidia_pci:-module}" - fi -fi -if [[ "$nvidia_present" == "1" ]]; then - cat >"$NVIDIA_OFFER" </dev/null 2>&1; then - virt="$(systemd-detect-virt 2>/dev/null || true)" - [[ -n "$virt" ]] || virt="none" -fi -has_gl=0 -shopt -s nullglob -dri_nodes=(/dev/dri/card* /dev/dri/renderD*) -(( ${#dri_nodes[@]} > 0 )) && has_gl=1 -shopt -u nullglob - -if [[ "$virt" != "none" && "$has_gl" != "1" ]]; then - cat >"$VM_HINT" < 1 from hyprctl, or computed DPI >= 140. -# --------------------------------------------------------------------------- -if command -v hyprctl >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then - # Compositor may still be settling when autostart fires. - mon_json="" - tries=0 - while [[ -z "$mon_json" && "$tries" -lt 5 ]]; do - mon_json="$(hyprctl -j monitors 2>/dev/null || true)" - if [[ -z "$mon_json" || "$mon_json" == "[]" ]]; then - mon_json="" - sleep 1 - fi - tries=$((tries + 1)) - done - if [[ -n "$mon_json" ]]; then - BOS_HYPR_MONITORS="$mon_json" python3 - "$HIDPI_HINT" "$(iso_now)" <<'PY' || true -import json, os, sys -hint_path, noted_at = sys.argv[1], sys.argv[2] -try: - monitors = json.loads(os.environ.get("BOS_HYPR_MONITORS") or "") -except Exception: - sys.exit(0) -if not isinstance(monitors, list): - sys.exit(0) - -hits = [] -for m in monitors: - if not isinstance(m, dict): - continue - name = m.get("name") or m.get("output") or "" - try: - scale = float(m.get("scale") or 1) - except (TypeError, ValueError): - scale = 1.0 - try: - w = int(m.get("width") or 0) - h = int(m.get("height") or 0) - except (TypeError, ValueError): - w = h = 0 - mm_w = mm_h = 0 - phys = m.get("physicalSize") - if isinstance(phys, dict): - mm_w = phys.get("x") or phys.get("width") or 0 - mm_h = phys.get("y") or phys.get("height") or 0 - elif isinstance(phys, (list, tuple)) and len(phys) >= 2: - mm_w, mm_h = phys[0], phys[1] - else: - mm_w = m.get("physicalWidth") or 0 - mm_h = m.get("physicalHeight") or 0 - try: - mm_w = float(mm_w or 0) - mm_h = float(mm_h or 0) - except (TypeError, ValueError): - mm_w = mm_h = 0.0 - dpi = round(w / (mm_w / 25.4), 1) if mm_w and w else 0.0 - px_per_mm = round(w / mm_w, 3) if mm_w and w else 0.0 - # High px/mm (dense panel) or Hyprland already chose scale > 1. - hidpi = scale > 1.01 or dpi >= 140 - if hidpi: - hits.append({ - "name": name, - "width": w, - "height": h, - "scale": scale, - "dpi": dpi, - "px_per_mm": px_per_mm, - }) - -if not hits: - sys.exit(0) -with open(hint_path, "w") as f: - json.dump({ - "suggested": True, - "rewrote_monitors_json": False, - "reason": "scale > 1 or DPI >= 140", - "monitors": hits, - "noted_at": noted_at, - }, f, indent=2) - f.write("\n") -PY - fi -fi - -# Mark done even if every probe was a no-op — do not nag next login. -printf '%s\n' "$(iso_now)" >"$FLAG" 2>/dev/null || true -exit 0 diff --git a/iso/airootfs/usr/local/bin/bos-live-setup b/iso/airootfs/usr/local/bin/bos-live-setup index 0609cac..f7b1d26 100644 --- a/iso/airootfs/usr/local/bin/bos-live-setup +++ b/iso/airootfs/usr/local/bin/bos-live-setup @@ -7,17 +7,9 @@ # bos-launch-calamares). Runs once at boot, before the tty1 autologin getty. set -e -# Bakery user units live in /usr/lib/systemd/user. --global enable writes -# /etc/systemd/user/*.wants/ so liveuser (and any later account) starts -# them on first login. Idempotent; bins are already in /usr/local. -if [[ -x /usr/local/bin/bos-enable-bakery-user-units ]]; then - /usr/local/bin/bos-enable-bakery-user-units \ - || echo "WARN: enabling bakery user units globally failed" -fi - # useradd -m copies /etc/skel, so the live user gets the real BOS desktop -# (hypr + bread config + bakery state) — proper live-media functionality, -# not an installer kiosk. Binaries are /usr/local, not skel. +# (breadd + breadbar + breadbox + keybinds) — proper live-media functionality, +# not an installer kiosk. if ! id liveuser &>/dev/null; then useradd -m -s /usr/bin/zsh liveuser for g in wheel video input audio storage power; do diff --git a/iso/airootfs/usr/local/bin/bos-nvidia-setup b/iso/airootfs/usr/local/bin/bos-nvidia-setup deleted file mode 100755 index d287f53..0000000 --- a/iso/airootfs/usr/local/bin/bos-nvidia-setup +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash -# bos-nvidia-setup — optional proprietary NVIDIA driver + Hyprland env. -# -# Installs nvidia + nvidia-utils only (never cuda). Writes -# ~/.config/hypr/nvidia.lua, which skel hyprland.lua dofiles only when -# the file exists — Mesa machines stay unchanged. Existing installs get -# the same include patched in if it is missing. -# -# Click-to-install from Settings, or run by hand. Not invoked from -# bos-first-boot. Idempotent. Prints "reboot required". -# -# Must run on an installed system. Elevates via pkexec, then sudo. -set -uo pipefail - -usage() { - cat <<'EOF' -Usage: bos-nvidia-setup [--home DIR] - -Install nvidia + nvidia-utils (not cuda) and write the Hyprland NVIDIA -env drop-in for this user. Reboot after. - - --home DIR user home that owns ~/.config/hypr (required under pkexec - if PKEXEC_UID / SUDO_USER cannot be resolved) -EOF -} - -TARGET_HOME="" -while [[ $# -gt 0 ]]; do - case "$1" in - --home) - TARGET_HOME="${2:-}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "bos-nvidia-setup: unknown argument: $1" >&2 - usage >&2 - exit 2 - ;; - esac -done - -if [[ "$(id -un)" == "liveuser" || -d /run/archiso ]]; then - echo "bos-nvidia-setup is for an installed system, not the live ISO." >&2 - exit 1 -fi - -if [[ "$(id -u)" -ne 0 ]]; then - home="${TARGET_HOME:-${HOME:-}}" - if [[ -z "$home" ]]; then - echo "bos-nvidia-setup: cannot determine home; pass --home" >&2 - exit 1 - fi - self="$(command -v bos-nvidia-setup 2>/dev/null || true)" - [[ -n "$self" ]] || self="$(readlink -f "$0" 2>/dev/null || printf '%s' "$0")" - if command -v pkexec >/dev/null 2>&1; then - exec pkexec "$self" --home "$home" - fi - if command -v sudo >/dev/null 2>&1; then - exec sudo "$self" --home "$home" - fi - echo "bos-nvidia-setup: need root (pkexec or sudo)" >&2 - exit 1 -fi - -if [[ -z "$TARGET_HOME" ]]; then - if [[ -n "${PKEXEC_UID:-}" ]]; then - TARGET_HOME="$(getent passwd "$PKEXEC_UID" | cut -d: -f6 || true)" - elif [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != root ]]; then - TARGET_HOME="$(getent passwd "$SUDO_USER" | cut -d: -f6 || true)" - fi -fi - -if [[ -z "$TARGET_HOME" || "$TARGET_HOME" == /root || ! -d "$TARGET_HOME" ]]; then - echo "bos-nvidia-setup: cannot determine user home (pass --home)" >&2 - exit 1 -fi - -HYPR_DIR="$TARGET_HOME/.config/hypr" -NVIDIA_LUA="$HYPR_DIR/nvidia.lua" -HYPR_LUA="$HYPR_DIR/hyprland.lua" - -# Hyprland 0.56 (Aquamarine). Wiki (https://wiki.hypr.land/Nvidia/): -# LIBVA_DRIVER_NAME + __GLX_VENDOR_LIBRARY_NAME. NVD_BACKEND is the -# current VA-API hint. No WLR_* (not wlroots). No GBM_BACKEND (not -# required; older docs cargo-culted it and it can break Firefox). -NVIDIA_LUA_BODY='-- Written by bos-nvidia-setup. hyprland.lua dofiles this only when it exists. --- Hyprland 0.56 (Aquamarine) — no WLR_* variables. --- https://wiki.hypr.land/Nvidia/ -hl.env("LIBVA_DRIVER_NAME", "nvidia") -hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") -hl.env("NVD_BACKEND", "direct") -' - -# Self-contained so it is safe to append to a hand-edited hyprland.lua. -HYPR_INCLUDE='-- bos-nvidia-setup: optional proprietary env; no-op when the file is absent -do - local nvidia = (os.getenv("HOME") or "") .. "/.config/hypr/nvidia.lua" - local f = io.open(nvidia, "r") - if f then - f:close() - pcall(dofile, nvidia) - end -end -' - -own_as_user() { - local path="$1" - [[ -e "$path" ]] || return 0 - local owner - owner="$(stat -c '%u:%g' "$TARGET_HOME" 2>/dev/null || true)" - [[ -n "$owner" ]] || return 0 - chown "$owner" "$path" 2>/dev/null || true -} - -echo "==> Installing nvidia + nvidia-utils (not cuda)" -if ! command -v pacman >/dev/null 2>&1; then - echo "bos-nvidia-setup: pacman not found" >&2 - exit 1 -fi -if ! pacman -S --needed --noconfirm -- nvidia nvidia-utils; then - echo "bos-nvidia-setup: pacman install failed" >&2 - exit 1 -fi - -echo "==> Writing $NVIDIA_LUA" -mkdir -p "$HYPR_DIR" || { - echo "bos-nvidia-setup: cannot create $HYPR_DIR" >&2 - exit 1 -} -printf '%s' "$NVIDIA_LUA_BODY" >"$NVIDIA_LUA" || { - echo "bos-nvidia-setup: cannot write $NVIDIA_LUA" >&2 - exit 1 -} -own_as_user "$NVIDIA_LUA" - -if [[ -f "$HYPR_LUA" ]] && ! grep -q 'nvidia\.lua' "$HYPR_LUA"; then - echo "==> Including nvidia.lua from $HYPR_LUA" - if [[ -n "$(tail -c1 "$HYPR_LUA" 2>/dev/null || true)" ]]; then - printf '\n' >>"$HYPR_LUA" - fi - printf '%s\n' "$HYPR_INCLUDE" >>"$HYPR_LUA" - own_as_user "$HYPR_LUA" -fi - -echo "reboot required" -exit 0 diff --git a/iso/airootfs/usr/local/bin/bos-rescue b/iso/airootfs/usr/local/bin/bos-rescue deleted file mode 100755 index f814865..0000000 --- a/iso/airootfs/usr/local/bin/bos-rescue +++ /dev/null @@ -1,598 +0,0 @@ -#!/bin/bash -# bos-rescue — live-ISO helper for an installed BOS that will not boot. -# -# Finds the installed btrfs `@` and the ESP, mounts them, then offers to -# arch-chroot and/or reinstall GRUB using the same sequence as -# post-install.sh / README Recovery: -# UEFI: grub-install NVRAM + --removable, then grub-mkconfig -# BIOS: grub-install i386-pc onto the disk hosting / -# -# Recovery is this script or the GRUB "snapshots" submenu (grub-btrfs). -# GRUB pins rootflags=subvol=@ — a snapper-swapped default subvolume is -# not what the installed grub.cfg will boot. Never snapper-rollback. -# -# Safe: prints the devices it will use and requires YES before writing. -# Best-effort: do not use `set -e`; a failed probe must not abort the rest. -set -uo pipefail - -MNT="${BOS_RESCUE_MNT:-}" -MOUNTED_ROOT=0 -MOUNTED_ESP=0 -ROOT_DEV="" -ESP_DEV="" -ROOT_ENCRYPTED=0 - -bold() { printf '\033[1m%s\033[0m\n' "$1" >&2; } -info() { printf ' %s\n' "$1" >&2; } -warn() { printf 'WARN: %s\n' "$1" >&2; } - -usage() { - cat <<'EOF' -Usage: bos-rescue - -Live-ISO helper: find the installed BOS btrfs @ and ESP, mount them, -then arch-chroot and/or reinstall GRUB. - - UEFI: grub-install (NVRAM) + grub-install --removable + grub-mkconfig - BIOS: grub-install --target=i386-pc onto the disk hosting / - -Prints the devices it will use and asks YES before writing anything. - -Do not snapper-rollback. GRUB pins rootflags=subvol=@. Pick a grub-btrfs -snapshot entry, or reinstall GRUB with this script. - -Must be run as root. Intended from the live ISO (SUPER+Return). -EOF -} - -need_root() { - if [[ "$(id -u)" -ne 0 ]]; then - echo "bos-rescue must run as root (sudo bos-rescue)." >&2 - exit 1 - fi -} - -confirm_yes() { - local prompt="$1" - local reply="" - printf '%s [type YES]: ' "$prompt" >&2 - read -r reply || return 1 - [[ "$reply" == "YES" ]] -} - -is_live_iso() { - [[ -d /run/archiso ]] || [[ -x /usr/local/bin/bos-live-setup ]] -} - -already_on_installed() { - # Installed BOS: / is the @ subvolume and this is not the live medium. - is_live_iso && return 1 - local src opts - src="$(findmnt -no SOURCE / 2>/dev/null | sed 's/\[.*\]//')" - opts="$(findmnt -no OPTIONS / 2>/dev/null || true)" - [[ -n "$src" ]] || return 1 - [[ "$opts" == *subvol=/@* || "$opts" == *subvol=@* ]] || return 1 - [[ -f /etc/os-release ]] && grep -qE '^ID=bos$' /etc/os-release -} - -pick_mnt() { - if [[ -n "$MNT" ]]; then - return - fi - if findmnt -n /mnt >/dev/null 2>&1; then - MNT=/mnt/bos-rescue - info "/mnt is already a mountpoint — using $MNT" - else - MNT=/mnt - fi -} - -lsblk_line() { - lsblk -pnlo NAME,FSTYPE,SIZE,LABEL,UUID,PARTTYPENAME "$1" 2>/dev/null | head -n1 -} - -# Open LUKS containers so a later btrfs scan can see @. -offer_luks() { - command -v cryptsetup >/dev/null || return 0 - local dev name reply - while read -r dev; do - [[ -n "$dev" ]] || continue - [[ -e "$dev" ]] || continue - if lsblk -no TYPE "$dev" 2>/dev/null | grep -qx crypt; then - continue - fi - # Skip already-mapped parents. - if lsblk -nlo TYPE "$dev" 2>/dev/null | grep -qx crypt; then - continue - fi - printf '\nLUKS container: %s\n %s\n' "$dev" "$(lsblk_line "$dev")" >&2 - printf 'Unlock this container? [y/N]: ' >&2 - read -r reply || reply="" - if [[ "$reply" == [yY] ]]; then - name="bos-rescue-$(basename "$dev")" - if cryptsetup open "$dev" "$name"; then - info "opened $dev as /dev/mapper/$name" - else - warn "cryptsetup open failed for $dev" - fi - fi - done < <(lsblk -pnlo NAME,FSTYPE | awk '$2 == "crypto_LUKS" { print $1 }') -} - -# Probe a btrfs device for an @ subvolume that looks like BOS (or any @). -# Prints: DEVICEKINDPRETTY where KIND is bos|other -probe_btrfs_dev() { - local dev="$1" - local tmp pretty kind id - tmp="$(mktemp -d /tmp/bos-rescue.XXXXXX)" || return 1 - kind="other" - pretty="" - if mount -o ro,subvol=@ "$dev" "$tmp" 2>/dev/null; then - if [[ -f "$tmp/etc/os-release" ]]; then - id="$(grep -E '^ID=' "$tmp/etc/os-release" | head -n1 | cut -d= -f2- | tr -d '"')" - pretty="$(grep -E '^PRETTY_NAME=' "$tmp/etc/os-release" | head -n1 | cut -d= -f2- | tr -d '"')" - [[ "$id" == "bos" ]] && kind="bos" - fi - umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true - rmdir "$tmp" 2>/dev/null || true - printf '%s\t%s\t%s\n' "$dev" "$kind" "${pretty:-btrfs @}" - return 0 - fi - # Some volumes only accept a top-level probe first. - if mount -o ro,subvolid=5 "$dev" "$tmp" 2>/dev/null; then - if [[ -d "$tmp/@" ]] || btrfs subvolume show "$tmp/@" &>/dev/null; then - umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true - rmdir "$tmp" 2>/dev/null || true - printf '%s\t%s\t%s\n' "$dev" "other" "btrfs @ (unreadable os-release)" - return 0 - fi - umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true - fi - rmdir "$tmp" 2>/dev/null || true - return 1 -} - -find_root_candidates() { - local dev - while read -r dev; do - [[ -n "$dev" ]] || continue - probe_btrfs_dev "$dev" || true - done < <(lsblk -pnlo NAME,FSTYPE | awk '$2 == "btrfs" { print $1 }') -} - -# Prefer the ESP named in the installed fstab; else EFI type / BOS bits. -find_esp_for_root() { - local root="$1" - local tmp fstab_uuid fstab_dev dev fstype parttype label - tmp="$(mktemp -d /tmp/bos-rescue.XXXXXX)" || return 1 - if mount -o ro,subvol=@ "$root" "$tmp" 2>/dev/null; then - if [[ -f "$tmp/etc/fstab" ]]; then - fstab_uuid="$(awk '$2 == "/boot/efi" { - if ($1 ~ /^UUID=/) { sub(/^UUID=/, "", $1); print $1; exit } - }' "$tmp/etc/fstab")" - fi - umount "$tmp" 2>/dev/null || umount -l "$tmp" 2>/dev/null || true - fi - rmdir "$tmp" 2>/dev/null || true - - if [[ -n "${fstab_uuid:-}" ]]; then - fstab_dev="$(blkid -U "$fstab_uuid" 2>/dev/null || true)" - if [[ -n "$fstab_dev" ]]; then - printf '%s\n' "$fstab_dev" - return 0 - fi - fi - - local best="" scored=0 score - # PARTTYPE is the GPT GUID — no spaces, unlike PARTTYPENAME ("EFI System"). - local efi_guid="c12a7328-f81f-11d2-ba4b-00a716dde993" - while read -r dev fstype parttype; do - [[ -n "$dev" ]] || continue - score=0 - [[ "$fstype" == "vfat" || "$fstype" == "fat32" || "$fstype" == "FAT-32" ]] && score=$((score + 1)) - [[ "${parttype,,}" == "$efi_guid" ]] && score=$((score + 3)) - if (( score > scored )); then - best="$dev" - scored=$score - fi - done < <(lsblk -pnlo NAME,FSTYPE,PARTTYPE) - - # Prefer an ESP that already has BOS or removable fallback bits. - local probe mp - for dev in $best $(lsblk -pnlo NAME,FSTYPE | awk '$2 == "vfat" { print $1 }'); do - [[ -n "$dev" ]] || continue - mp="$(mktemp -d /tmp/bos-rescue.XXXXXX)" || continue - if mount -o ro "$dev" "$mp" 2>/dev/null; then - if [[ -f "$mp/EFI/BOS/grubx64.efi" || -f "$mp/EFI/BOOT/BOOTX64.EFI" ]]; then - umount "$mp" 2>/dev/null || true - rmdir "$mp" 2>/dev/null || true - printf '%s\n' "$dev" - return 0 - fi - umount "$mp" 2>/dev/null || true - fi - rmdir "$mp" 2>/dev/null || true - done - - [[ -n "$best" ]] && printf '%s\n' "$best" -} - -select_from_list() { - local title="$1" - shift - local -a items=("$@") - local i choice - if (( ${#items[@]} == 0 )); then - return 1 - fi - if (( ${#items[@]} == 1 )); then - printf '%s\n' "${items[0]}" - return 0 - fi - bold "$title" - for i in "${!items[@]}"; do - printf ' %d) %s\n' "$((i + 1))" "${items[$i]}" >&2 - done - printf 'Select [1-%d]: ' "${#items[@]}" >&2 - read -r choice || return 1 - if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#items[@]} )); then - printf '%s\n' "${items[$((choice - 1))]}" - return 0 - fi - return 1 -} - -discover_and_choose() { - bold "Scanning for an installed BOS (btrfs @) …" - offer_luks - - local -a bos_devs=() other_devs=() - local dev kind pretty line - while IFS=$'\t' read -r dev kind pretty; do - [[ -n "$dev" ]] || continue - line="$dev (${pretty:-$kind})" - if [[ "$kind" == "bos" ]]; then - bos_devs+=("$dev") - else - other_devs+=("$dev") - fi - info "found $line" - done < <(find_root_candidates) - - if (( ${#bos_devs[@]} == 0 && ${#other_devs[@]} == 0 )); then - echo "No btrfs @ subvolume found. Unlock LUKS first if the install is encrypted." >&2 - return 1 - fi - - if (( ${#bos_devs[@]} == 1 )); then - ROOT_DEV="${bos_devs[0]}" - info "Using BOS root $ROOT_DEV" - elif (( ${#bos_devs[@]} > 1 )); then - ROOT_DEV="$(select_from_list "More than one BOS @ found:" "${bos_devs[@]}")" || return 1 - else - warn "No ID=bos os-release on @ — offering every btrfs @ found" - ROOT_DEV="$(select_from_list "Select the installed root device:" "${other_devs[@]}")" || return 1 - fi - - ESP_DEV="$(find_esp_for_root "$ROOT_DEV" || true)" - if [[ -n "$ESP_DEV" ]]; then - info "Using ESP $ESP_DEV" - fi - if [[ -z "$ESP_DEV" ]]; then - local -a esps=() - while read -r dev; do - [[ -n "$dev" ]] && esps+=("$dev") - done < <(lsblk -pnlo NAME,FSTYPE,PARTTYPE | awk ' - $2 == "vfat" || tolower($3) == "c12a7328-f81f-11d2-ba4b-00a716dde993" { print $1 } - ') - if (( ${#esps[@]} == 0 )); then - warn "No ESP found. GRUB reinstall on UEFI will fail; chroot is still available." - else - ESP_DEV="$(select_from_list "Select the EFI System Partition:" "${esps[@]}")" || true - fi - fi -} - -mount_install() { - pick_mnt - mkdir -p "$MNT" - if ! findmnt -n "$MNT" >/dev/null 2>&1; then - if ! mount -o subvol=@ "$ROOT_DEV" "$MNT"; then - warn "failed to mount $ROOT_DEV subvol=@ at $MNT" - return 1 - fi - MOUNTED_ROOT=1 - fi - if [[ -n "$ESP_DEV" ]]; then - mkdir -p "$MNT/boot/efi" - if ! findmnt -n "$MNT/boot/efi" >/dev/null 2>&1; then - if mount "$ESP_DEV" "$MNT/boot/efi"; then - MOUNTED_ESP=1 - else - warn "failed to mount ESP $ESP_DEV at $MNT/boot/efi" - fi - fi - fi - if [[ "$(lsblk -no TYPE "$ROOT_DEV" 2>/dev/null)" == "crypt" ]]; then - ROOT_ENCRYPTED=1 - fi -} - -unmount_install() { - if [[ "$MOUNTED_ESP" == "1" ]]; then - umount "$MNT/boot/efi" 2>/dev/null || umount -l "$MNT/boot/efi" 2>/dev/null || true - MOUNTED_ESP=0 - fi - if [[ "$MOUNTED_ROOT" == "1" ]]; then - umount "$MNT" 2>/dev/null || umount -l "$MNT" 2>/dev/null || true - MOUNTED_ROOT=0 - fi -} - -print_plan() { - echo >&2 - bold "Devices" - info "root: ${ROOT_DEV:-unset} $([[ -n "$ROOT_DEV" ]] && lsblk_line "$ROOT_DEV")" - info "ESP: ${ESP_DEV:-none} $([[ -n "$ESP_DEV" ]] && lsblk_line "$ESP_DEV")" - info "mount: ${MNT:-unset}" - if [[ -d /sys/firmware/efi ]]; then - info "firmware: UEFI" - else - info "firmware: BIOS" - fi - if [[ "$ROOT_ENCRYPTED" == "1" ]]; then - info "root is LUKS (grub-install will include cryptodisk modules)" - fi - echo >&2 - info "Recovery is grub-btrfs (GRUB snapshots submenu) or this GRUB reinstall." - info "GRUB pins rootflags=subvol=@ — do not swap the default subvolume." -} - -run_in_target() { - local cmd="$1" - if command -v arch-chroot >/dev/null; then - arch-chroot "$MNT" bash -c "$cmd" - return $? - fi - # arch-install-scripts is not guaranteed on the ISO — bind the API - # filesystems the same way arch-chroot would, then chroot. - mount --bind /proc "$MNT/proc" 2>/dev/null || mount -t proc proc "$MNT/proc" - mount --bind /sys "$MNT/sys" 2>/dev/null || mount -t sysfs sys "$MNT/sys" - mount --bind /dev "$MNT/dev" 2>/dev/null || mount -t devtmpfs udev "$MNT/dev" - mkdir -p "$MNT/run" - mount --bind /run "$MNT/run" 2>/dev/null || mount -t tmpfs tmpfs "$MNT/run" - if [[ -d /sys/firmware/efi ]]; then - mkdir -p "$MNT/sys/firmware/efi/efivars" - mount -t efivarfs efivarfs "$MNT/sys/firmware/efi/efivars" 2>/dev/null || true - fi - chroot "$MNT" bash -c "$cmd" - local rc=$? - umount "$MNT/sys/firmware/efi/efivars" 2>/dev/null || true - umount "$MNT/run" 2>/dev/null || true - umount "$MNT/dev" 2>/dev/null || true - umount "$MNT/sys" 2>/dev/null || true - umount "$MNT/proc" 2>/dev/null || true - return "$rc" -} - -grub_commands_preview() { - if [[ -d /sys/firmware/efi ]]; then - cat <<'EOF' >&2 - grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck - grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck - grub-mkconfig -o /boot/grub/grub.cfg -EOF - else - cat <<'EOF' >&2 - grub-install --target=i386-pc --recheck - grub-mkconfig -o /boot/grub/grub.cfg -EOF - fi -} - -reinstall_grub() { - if [[ ! -d "$MNT/boot" ]]; then - warn "target $MNT/boot missing — mount the installed @ first" - return 1 - fi - echo >&2 - bold "This will write a bootloader using:" - info "root ${ROOT_DEV:-/} ESP ${ESP_DEV:-n/a} chroot $MNT" - grub_commands_preview - echo >&2 - if ! confirm_yes "Reinstall GRUB now?"; then - info "skipped" - return 0 - fi - - # Same sequence as post-install.sh (UEFI NVRAM + --removable, or BIOS MBR). - local script - script="$(cat <<'EOS' -set -uo pipefail -ROOT_SRC="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" -if [[ "$(lsblk -no TYPE "$ROOT_SRC" 2>/dev/null)" == "crypt" ]]; then - ROOT_ENCRYPTED=1 -else - ROOT_ENCRYPTED=0 -fi -if [[ "$ROOT_ENCRYPTED" == "1" ]] && [[ -f /etc/default/grub ]] \ - && ! grep -q '^GRUB_ENABLE_CRYPTODISK=' /etc/default/grub; then - echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub \ - || echo "WARN: adding GRUB_ENABLE_CRYPTODISK failed" -fi -if ! command -v grub-install >/dev/null; then - echo "ERROR: grub-install not found in the installed system" >&2 - exit 1 -fi -CRYPT_MODULES=() -[[ "$ROOT_ENCRYPTED" == "1" ]] && CRYPT_MODULES=(--modules="cryptodisk luks luks2") -if [[ -d /sys/firmware/efi ]]; then - grub-install --target=x86_64-efi --efi-directory=/boot/efi \ - --bootloader-id=BOS --recheck "${CRYPT_MODULES[@]}" \ - || echo "WARN: grub-install (nvram) failed" - grub-install --target=x86_64-efi --efi-directory=/boot/efi \ - --removable --recheck "${CRYPT_MODULES[@]}" \ - || echo "WARN: grub-install (removable) failed" -else - ROOT_DEV="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" - ROOT_DISK="$(lsblk -no pkname "$ROOT_DEV" 2>/dev/null)" - if [[ -n "$ROOT_DISK" ]]; then - grub-install --target=i386-pc --recheck "${CRYPT_MODULES[@]}" "/dev/$ROOT_DISK" \ - || echo "WARN: grub-install (BIOS) failed" - else - echo "WARN: could not determine the disk hosting / — BIOS grub-install skipped" - fi -fi -if command -v grub-mkconfig >/dev/null; then - grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed" -else - echo "WARN: grub-mkconfig not found" -fi -EOS -)" - if run_in_target "$script"; then - bold "GRUB reinstall finished." - info "Firmware that lost its NVRAM entry can still boot EFI/BOOT/BOOTX64.EFI." - else - warn "GRUB reinstall returned non-zero — see messages above" - return 1 - fi -} - -do_chroot() { - if [[ ! -d "$MNT/etc" ]]; then - warn "target $MNT is not a mounted system" - return 1 - fi - bold "Entering chroot at $MNT (exit to return)." - if command -v arch-chroot >/dev/null; then - arch-chroot "$MNT" - else - run_in_target "exec bash -l" - fi -} - -menu_live() { - local choice - while true; do - echo - bold "bos-rescue" - print_plan - cat <<'EOF' >&2 - 1) arch-chroot into the installed system - 2) Reinstall GRUB (NVRAM + --removable + grub-mkconfig) - 3) Reinstall GRUB, then chroot - 4) Unmount and quit - q) Quit (leave mounts) -EOF - printf 'Choice: ' >&2 - read -r choice || choice="q" - case "$choice" in - 1) do_chroot ;; - 2) reinstall_grub ;; - 3) reinstall_grub; do_chroot ;; - 4) unmount_install; bold "Unmounted."; return 0 ;; - q|Q) info "Leaving mounts in place at $MNT"; return 0 ;; - *) info "unknown choice" ;; - esac - done -} - -menu_installed() { - ROOT_DEV="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" - ESP_DEV="$(findmnt -no SOURCE /boot/efi 2>/dev/null || true)" - MNT="/" - if [[ "$(lsblk -no TYPE "$ROOT_DEV" 2>/dev/null)" == "crypt" ]]; then - ROOT_ENCRYPTED=1 - fi - echo - bold "Already running the installed BOS (not the live ISO)." - info "Root and ESP are already mounted — chroot is not needed." - print_plan - if confirm_yes "Reinstall GRUB on this running system?"; then - # Running on the installed root: no extra mount/chroot. - local old_mnt="$MNT" - MNT="/" - # run_in_target would chroot into / — just run locally. - if [[ -d /sys/firmware/efi && -z "$ESP_DEV" ]]; then - warn " /boot/efi is not mounted — refusing to write" - return 1 - fi - bash -c "$(cat <<'EOS' -set -uo pipefail -ROOT_SRC="$(findmnt -no SOURCE / | sed 's/\[.*\]//')" -if [[ "$(lsblk -no TYPE "$ROOT_SRC" 2>/dev/null)" == "crypt" ]]; then - ROOT_ENCRYPTED=1 -else - ROOT_ENCRYPTED=0 -fi -if [[ "$ROOT_ENCRYPTED" == "1" ]] && [[ -f /etc/default/grub ]] \ - && ! grep -q '^GRUB_ENABLE_CRYPTODISK=' /etc/default/grub; then - echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub \ - || echo "WARN: adding GRUB_ENABLE_CRYPTODISK failed" -fi -CRYPT_MODULES=() -[[ "$ROOT_ENCRYPTED" == "1" ]] && CRYPT_MODULES=(--modules="cryptodisk luks luks2") -if [[ -d /sys/firmware/efi ]]; then - grub-install --target=x86_64-efi --efi-directory=/boot/efi \ - --bootloader-id=BOS --recheck "${CRYPT_MODULES[@]}" \ - || echo "WARN: grub-install (nvram) failed" - grub-install --target=x86_64-efi --efi-directory=/boot/efi \ - --removable --recheck "${CRYPT_MODULES[@]}" \ - || echo "WARN: grub-install (removable) failed" -else - ROOT_DISK="$(lsblk -no pkname "$ROOT_SRC" 2>/dev/null)" - if [[ -n "$ROOT_DISK" ]]; then - grub-install --target=i386-pc --recheck "${CRYPT_MODULES[@]}" "/dev/$ROOT_DISK" \ - || echo "WARN: grub-install (BIOS) failed" - fi -fi -grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed" -EOS -)" - MNT="$old_mnt" - else - info "skipped" - fi -} - -main() { - if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 - fi - need_root - local req - for req in mount lsblk blkid findmnt; do - if ! command -v "$req" >/dev/null; then - echo "bos-rescue: missing required tool '$req'" >&2 - exit 1 - fi - done - bold "bos-rescue" - info "Live-ISO recovery helper. Prints devices and asks YES before writing." - info "Use grub-btrfs (GRUB snapshots submenu) for a bootable snapshot." - info "Do not snapper-rollback — GRUB pins rootflags=subvol=@." - echo - - if already_on_installed; then - menu_installed - return 0 - fi - - if ! is_live_iso; then - warn "This does not look like the BOS live ISO (/run/archiso missing)." - info "Continuing anyway — will scan disks for a BOS @." - fi - - discover_and_choose || exit 1 - print_plan - if ! confirm_yes "Mount these devices and continue?"; then - info "nothing mounted" - exit 0 - fi - mount_install || exit 1 - menu_live -} - -main "$@" diff --git a/iso/airootfs/usr/local/bin/bos-session b/iso/airootfs/usr/local/bin/bos-session index 5f27974..d7655fc 100644 --- a/iso/airootfs/usr/local/bin/bos-session +++ b/iso/airootfs/usr/local/bin/bos-session @@ -2,10 +2,11 @@ # 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. -# 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. +# 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. # # 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 a34f70b..65f27c0 100644 --- a/iso/airootfs/usr/local/bin/bos-update +++ b/iso/airootfs/usr/local/bin/bos-update @@ -8,12 +8,11 @@ # 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 /usr/local (whatever `bakery list` +# 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, ...). Those bits live on @ and ride snapper -# root snapshots; recover via grub-btrfs, not `snapper rollback`. +# breadhelp, ...). # # Best-effort: a failure in one channel doesn't abort the other. set -uo pipefail @@ -21,11 +20,12 @@ 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 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. +# 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. 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 \ @@ -47,22 +47,10 @@ fi echo bold "==> Bread ecosystem (bakery update --all)" if command -v bakery >/dev/null; then - # /usr/local is root-owned. Never run bakery as the user against it; - # bakery itself also tries sudo -n then pkexec for privileged writes. - if sudo -n true >/dev/null 2>&1; then - sudo -n bakery update --all || echo "WARN: bakery update failed" - elif command -v pkexec >/dev/null; then - pkexec bakery update --all || echo "WARN: bakery update failed" - else - echo "WARN: bakery update needs sudo -n or pkexec for /usr/local" - fi + bakery update --all || echo "WARN: bakery update failed" else echo "bakery not found; skipping" fi echo bold "==> BOS is up to date." -echo -bold "Recovery" -echo "If this update goes badly: reboot → GRUB “snapshots” submenu." -echo "snapper rollback will not change what GRUB boots (rootflags=subvol=@)." diff --git a/iso/bread-lockfile.toml b/iso/bread-lockfile.toml index 8e21593..ac34e31 100644 --- a/iso/bread-lockfile.toml +++ b/iso/bread-lockfile.toml @@ -1,10 +1,10 @@ -# Bakery binaries baked into the live/installed image at /usr/local. +# 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. bread 0.8.0 ships bread-emit and -# bread-module-host, so those are required_bins. +# skipped with a warning when it does not (today: bread 0.8.0 has no +# bread-emit / bread-module-host). # # A flat `bins` list is still accepted and treated as required_bins. # @@ -23,8 +23,6 @@ required_bins = [ "bakery", "bread", "breadd", - "bread-emit", - "bread-module-host", "breadman", "breadbar", "breadbox", @@ -43,21 +41,30 @@ required_bins = [ "breadhelp", ] +# Bake if the verified index publishes them; do not fail the ISO if absent. +optional_bins = [ + "bread-emit", + "bread-module-host", +] + # Package name → version. Must exist at dl.breadway.dev/// and -# should match the signed index so CI can verify sha256. +# should match the signed index so CI can verify sha256. Newer git tags +# that bakery release CI did not publish (breadpad 0.5.1, breadmon 0.1.3, +# breadclip 0.2.2, breadshot 0.1.2, breadhelp 0.2.4, breadcrumbs 2.1.7) +# stay off this list until those artifacts exist. # [[pin]] { package, version } is accepted as well and merged (conflict = bake error). [versions] -bakery = "0.7.4" +bakery = "0.7.2" bread = "0.8.0" -bread-theme = "0.7.4" -breadbar = "0.3.2" -breadbox = "0.3.2" -breadcrumbs = "2.1.8" -breadpad = "0.5.2" -breadpaper = "0.1.13" -breadmon = "0.1.4" -breadsearch = "0.3.2" -breadclip = "0.2.3" -breadshot = "0.1.3" -bos-settings = "0.8.1" -breadhelp = "0.2.5" +bread-theme = "0.7.2" +breadbar = "0.3.1" +breadbox = "0.3.1" +breadcrumbs = "2.1.6" +breadpad = "0.5.0" +breadpaper = "0.1.12" +breadmon = "0.1.2" +breadsearch = "0.3.1" +breadclip = "0.1.1" +breadshot = "0.1.1" +bos-settings = "0.8.0" +breadhelp = "0.2.3" diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index d30488a..cc8e2e2 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -78,8 +78,6 @@ snapper snap-pac grub-btrfs inotify-tools -# Home backup (Settings → Backup). Snapper is root (`@`) only; restic covers $HOME. -restic # Wayland / Hyprland hyprland @@ -208,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 /usr/local at ISO build +# 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 -# also committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime +# 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, …). @@ -225,8 +223,6 @@ slurp wl-clipboard playerctl # Wallpaper daemon + pywal (drives the bread* colour palette from the wallpaper). -# python-pywal was dropped from Arch [extra] (AUR-only now) — republished to -# [breadway], see packaging/python-pywal. awww python-pywal # Boot splash (BOS logo + spinner instead of kernel text). diff --git a/iso/pacman.conf b/iso/pacman.conf index abb7e2f..506b4bb 100644 --- a/iso/pacman.conf +++ b/iso/pacman.conf @@ -46,20 +46,21 @@ 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 /usr/local at ISO build time. +# are NOT here; they are bakery-baked into /etc/skel at ISO build time. # # Packages are published to the Forgejo Arch registry (group "os") by the -# .forgejo/workflows/*.yml workflows; scripts/ci-publish-signed-repo.sh then -# collects them, detach-signs each .pkg.tar.zst with the BOS release key -# (releases@breadway.dev), runs `repo-add -s`, and publishes the signed db -# at https://dl.breadway.dev/arch/$arch (signed-repo.yml). +# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). # -# SigLevel = Required: every package AND the db carry a .sig from key -# 56203B86A110695AE7F310934AF3323D678EB5E2 — the same key committed as -# KEYS.asc / airootfs/etc/pacman.d/breadway-repo.asc, imported into the -# pacman keyring at build time (build-local.sh), on the live medium, and -# on the installed target (calamares/post-install.sh). +# 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. # ----------------------------------------------------------------------- -[breadway] -SigLevel = Required -Server = https://dl.breadway.dev/arch/$arch +# The section name must match Forgejo's served db filename +# ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. +[Breadway.os.git.breadway.dev] +SigLevel = Never +Server = https://git.breadway.dev/api/packages/Breadway/arch/os/$arch diff --git a/iso/profiledef.sh b/iso/profiledef.sh index 462f74e..c438138 100644 --- a/iso/profiledef.sh +++ b/iso/profiledef.sh @@ -29,8 +29,4 @@ file_permissions=( ["/usr/local/bin/bos-session"]="0:0:755" ["/usr/local/bin/bos-netcheck"]="0:0:755" ["/usr/local/bin/bos-update"]="0:0:755" - ["/usr/local/bin/bos-rescue"]="0:0:755" - ["/usr/local/bin/bos-first-boot"]="0:0:755" - ["/usr/local/bin/bos-nvidia-setup"]="0:0:755" - ["/usr/local/bin/bos-enable-bakery-user-units"]="0:0:755" ) diff --git a/packaging/arch/README.md b/packaging/arch/README.md index 1116280..ce80f7e 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -3,7 +3,7 @@ Arch packaging This directory only holds `PKGBUILD`s for third-party AUR packages BOS republishes to the `[breadway]` pacman repo (`calamares`, `bibata`, -`powerlevel10k`, `yay-bin`, `python-pywal`) — not the user's own code. See each +`powerlevel10k`, `yay-bin`) — not the user's own code. See each subdirectory's `.forgejo/workflows/.yml` (in this repo) for how each one publishes on a push to `packaging//**`. diff --git a/packaging/python-pywal/PKGBUILD b/packaging/python-pywal/PKGBUILD deleted file mode 100644 index b6796f5..0000000 --- a/packaging/python-pywal/PKGBUILD +++ /dev/null @@ -1,44 +0,0 @@ -# BOS in-house rebuild of python-pywal. -# -# python-pywal was dropped from Arch's [extra] repo (it is now AUR-only), but -# BOS needs the `wal` binary: bread-theme shells out to it to extract a colour -# palette from the user's wallpaper. Republished to [breadway] so the ISO can -# pull it via pacman, same pattern as calamares / bibata / powerlevel10k / -# yay-bin. Source of truth: https://aur.archlinux.org/packages/python-pywal -# -# Maintainer: Breadway -# Upstream maintainer: Morten Linderud -# Contributor: Sean Haugh - -pkgname=python-pywal -pkgver=3.3.0 -pkgrel=11 -pkgdesc="Generate and change colorschemes on the fly" -arch=('any') -url="https://github.com/dylanaraps/pywal/" -license=('MIT') -depends=('python' 'imagemagick') -makedepends=('python-build' 'python-installer' 'python-wheel' 'python-setuptools') -optdepends=('feh: set wallpaper' - 'nitrogen: set wallpaper') -# BOS PKGBUILDs verify sources by sha256 only (no source PGP), matching -# calamares / powerlevel10k here. -source=("$pkgname-$pkgver.tar.gz::https://github.com/dylanaraps/pywal/archive/${pkgver}.tar.gz") -sha256sums=('fe8fc1c29d1cad1a1a8580293dcfe32e1fac259f9dbfd5c8877439fa5948d189') - -build() { - cd "pywal-${pkgver}" - # setup.py-only project: python-build injects the setuptools backend. - python -m build --wheel --no-isolation -} - -check() { - cd "pywal-${pkgver}" - python -m unittest discover -vs tests -} - -package() { - cd "pywal-${pkgver}" - python -m installer --destdir="$pkgdir" dist/*.whl - install -Dm644 LICENSE.md "$pkgdir/usr/share/licenses/$pkgname/LICENSE" -} diff --git a/scripts/ci-publish-signed-repo.sh b/scripts/ci-publish-signed-repo.sh deleted file mode 100755 index 6ded0e0..0000000 --- a/scripts/ci-publish-signed-repo.sh +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env bash -# Collect the current [breadway] ISO packages, detach-sign them with the -# BOS release key (releases@breadway.dev), and publish a signed pacman db -# under /srv/breadway-dl/arch/x86_64/ (https://dl.breadway.dev/arch/x86_64/). -# -# Does not change ISO SigLevel and does not write to the Forgejo Arch -# registry — existing package.yml / packaging/*.yml PUTs stay as they are. -# -# Required env: -# GPG_PRIVATE_KEY armoured secret key (same secret as release-iso.yml) -# Optional env: -# BREADWAY_DEST publish dir (default /srv/breadway-dl/arch/x86_64) -# BREADWAY_PKG_DIR extra directory of .pkg.tar.zst to prefer over the registry -# BREADWAY_REGISTRY Forgejo Arch registry base -# BREADWAY_SIGN_ONLY=1 skip collect; sign+index BREADWAY_REPO_DIR only -set -euo pipefail - -PACKAGES=( - breadlock - calamares - zen-browser-bin - bibata-cursor-theme-bin - zsh-theme-powerlevel10k - yay-bin - python-pywal -) - -ARCH="${BREADWAY_ARCH:-x86_64}" -REGISTRY="${BREADWAY_REGISTRY:-https://git.breadway.dev/api/packages/Breadway/arch/os}" -DEST="${BREADWAY_DEST:-/srv/breadway-dl/arch/${ARCH}}" -KEY_ID="${BREADWAY_KEY_ID:-releases@breadway.dev}" -DB_NAME="${BREADWAY_REGISTRY_DB:-Breadway.os.git.breadway.dev.db}" -REPO_DIR="${BREADWAY_REPO_DIR:-}" - -SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" - -die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } - -need_key() { - if [[ -z "${GPG_PRIVATE_KEY:-}" ]]; then - die "GPG_PRIVATE_KEY is missing; refusing to publish an unsigned [breadway] repo." - fi -} - -urlencode() { - python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe="-._~"))' "$1" -} - -pkginfo_name() { - local pkg="$1" info - info="$(tar -xOf "$pkg" .PKGINFO 2>/dev/null || zstd -dc "$pkg" | tar -xO .PKGINFO)" - awk -F ' = ' '$1=="pkgname" {print $2; exit}' <<<"$info" -} - -import_key() { - export GNUPGHOME="${GNUPGHOME:-$(mktemp -d "${TMPDIR:-/tmp}/gnupg-breadway-repo.XXXXXX")}" - mkdir -m 700 -p "$GNUPGHOME" - printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import -} - -detach_sign_pkgs() { - local pkg - shopt -s nullglob - for pkg in *.pkg.tar.zst; do - gpg --batch --yes --local-user "$KEY_ID" --detach-sign "$pkg" - done - shopt -u nullglob -} - -repo_add_signed() { - local pkgs=() - shopt -s nullglob - pkgs=(*.pkg.tar.zst) - shopt -u nullglob - (( ${#pkgs[@]} > 0 )) || die "no .pkg.tar.zst files to index" - rm -f breadway.db breadway.db.tar.gz breadway.db.sig breadway.db.tar.gz.sig \ - breadway.files breadway.files.tar.gz breadway.files.sig breadway.files.tar.gz.sig - if repo-add --help 2>&1 | grep -q -- '--include-sigs'; then - repo-add -s -k "$KEY_ID" --include-sigs breadway.db.tar.gz "${pkgs[@]}" - else - repo-add -s -k "$KEY_ID" breadway.db.tar.gz "${pkgs[@]}" - fi - [[ -e breadway.db.tar.gz.sig || -e breadway.db.sig ]] \ - || die "repo-add -s did not write breadway.db*.sig" - # gpg writes 0600; nginx and the next publish need world-readable files. - find . -maxdepth 1 -type f -exec chmod a+r {} + || true -} - -ensure_arch_tools() { - if ! command -v gpg >/dev/null 2>&1; then - command -v pacman >/dev/null 2>&1 || die "gpg not on PATH" - pacman -Sy --noconfirm --needed gnupg - fi - command -v repo-add >/dev/null 2>&1 || die "repo-add not on PATH" - command -v gpg >/dev/null 2>&1 || die "gpg not on PATH" -} - -sign_and_index() { - local dir="$1" - [[ -d "$dir" ]] || die "repo dir missing: $dir" - need_key - ensure_arch_tools - import_key - ( - cd "$dir" - detach_sign_pkgs - repo_add_signed - ) -} - -container_runtime() { - if command -v docker >/dev/null 2>&1; then - printf '%s\n' docker - elif command -v podman >/dev/null 2>&1; then - printf '%s\n' podman - else - return 1 - fi -} - -sign_and_index_anywhere() { - local dir="$1" - if command -v repo-add >/dev/null 2>&1 && command -v gpg >/dev/null 2>&1; then - sign_and_index "$dir" - return - fi - local rt - rt="$(container_runtime)" || die \ - "need host gpg+repo-add, or docker/podman to run archlinux:latest (no Forgejo container: — host must see /srv/breadway-dl)" - # Host job + bind-mount, same reason bakery writes /srv without container:. - # Run as the runner user: root-owned 0600 .sig files made chmod/nginx fail - # (run 1050) and would block the next `rm -rf` of a previous tree. - "$rt" run --rm --network=host \ - --user "$(id -u):$(id -g)" \ - -e HOME=/tmp \ - -e TMPDIR=/tmp \ - -e GPG_PRIVATE_KEY \ - -e BREADWAY_SIGN_ONLY=1 \ - -e BREADWAY_REPO_DIR=/repo \ - -e BREADWAY_KEY_ID="$KEY_ID" \ - -v "$dir:/repo" \ - -v "$SCRIPT_PATH:/ci-publish-signed-repo.sh:ro" \ - archlinux:latest \ - bash /ci-publish-signed-repo.sh -} - -parse_registry_db() { - local db="$1" - python3 - "$db" "${PACKAGES[@]}" <<'PY' -import sys, tarfile - -db = sys.argv[1] -want = set(sys.argv[2:]) -found = {} -with tarfile.open(db, "r:*") as tf: - for member in tf.getmembers(): - if not member.name.endswith("/desc") or not member.isfile(): - continue - fh = tf.extractfile(member) - if fh is None: - continue - text = fh.read().decode() - fields = {} - key = None - buf = [] - def flush(): - if key is not None: - fields[key] = "\n".join(buf).strip() - for line in text.splitlines(): - if line.startswith("%") and line.endswith("%") and len(line) > 2: - flush() - key = line.strip("%") - buf = [] - else: - buf.append(line) - flush() - name = fields.get("NAME", "") - filename = fields.get("FILENAME", "") - if name in want and filename: - found[name] = filename - -missing = sorted(want - set(found)) -if missing: - sys.stderr.write("registry db missing packages: " + " ".join(missing) + "\n") - raise SystemExit(1) -for name in sys.argv[2:]: - print(f"{name}\t{found[name]}") -PY -} - -copy_local_overrides() { - local dir="$1" - [[ -n "$dir" && -d "$dir" ]] || return 0 - local pkg name - shopt -s nullglob - for pkg in "$dir"/*.pkg.tar.zst "$dir"/*/*.pkg.tar.zst; do - [[ -f "$pkg" ]] || continue - name="$(pkginfo_name "$pkg")" - [[ -n "$name" ]] || continue - local wanted=0 p - for p in "${PACKAGES[@]}"; do - if [[ "$p" == "$name" ]]; then - wanted=1 - break - fi - done - if (( wanted )); then - printf 'local override: %s -> %s\n' "$name" "$(basename "$pkg")" - cp -a "$pkg" "$STAGE/$(basename "$pkg")" - fi - done - shopt -u nullglob -} - -has_pkg_named() { - local name="$1" pkg got - shopt -s nullglob - for pkg in "$STAGE"/*.pkg.tar.zst; do - got="$(pkginfo_name "$pkg")" - if [[ "$got" == "$name" ]]; then - shopt -u nullglob - return 0 - fi - done - shopt -u nullglob - return 1 -} - -collect_from_registry() { - local work db name filename enc url - work="$(mktemp -d "${TMPDIR:-/tmp}/breadway-db.XXXXXX")" - db="$work/$DB_NAME" - curl -fL --retry 3 --retry-delay 2 -o "$db" "$REGISTRY/$ARCH/$DB_NAME" \ - || die "failed to fetch $REGISTRY/$ARCH/$DB_NAME" - while IFS=$'\t' read -r name filename; do - if has_pkg_named "$name"; then - printf 'using local %s, skip registry\n' "$name" - continue - fi - enc="$(urlencode "$filename")" - url="$REGISTRY/$ARCH/$enc" - printf 'fetch %s\n' "$filename" - curl -fL --retry 3 --retry-delay 2 -o "$STAGE/$filename" "$url" \ - || die "failed to fetch $url" - done < <(parse_registry_db "$db") - rm -rf "$work" -} - -publish_tree() { - local parent dest_name prev - parent="$(dirname "$DEST")" - dest_name="$(basename "$DEST")" - mkdir -p "$parent" - chmod a+rX "$STAGE" || true - # gpg --detach-sign often writes 0600 files the runner cannot chmod; - # do not fail the publish after repo-add -s already succeeded. - find "$STAGE" -type f -exec chmod a+r {} + || true - prev="$parent/${dest_name}.prev" - rm -rf "$prev" - if [[ -e "$DEST" ]]; then - mv "$DEST" "$prev" - fi - mv "$STAGE" "$DEST" - rm -rf "$prev" - STAGE="" -} - -if [[ "${BREADWAY_SIGN_ONLY:-0}" == 1 ]]; then - [[ -n "$REPO_DIR" ]] || die "BREADWAY_SIGN_ONLY requires BREADWAY_REPO_DIR" - sign_and_index "$REPO_DIR" - exit 0 -fi - -need_key - -DEST_PARENT="$(dirname "$DEST")" -mkdir -p "$DEST_PARENT" || die "cannot create $DEST_PARENT (runner must write /srv/breadway-dl)" -STAGE="$(mktemp -d "$DEST_PARENT/.stage-XXXXXX")" -cleanup() { - if [[ -n "${STAGE:-}" && -d "${STAGE:-}" ]]; then - rm -rf "$STAGE" - fi - if [[ -n "${GNUPGHOME:-}" && "$GNUPGHOME" == *gnupg-breadway-repo* ]]; then - rm -rf "$GNUPGHOME" - fi -} -trap cleanup EXIT - -copy_local_overrides "${BREADWAY_PKG_DIR:-}" -collect_from_registry - -missing=() -for name in "${PACKAGES[@]}"; do - has_pkg_named "$name" || missing+=("$name") -done -if (( ${#missing[@]} > 0 )); then - die "missing packages after collect: ${missing[*]}" -fi - -sign_and_index_anywhere "$STAGE" - -# Do not publish helper junk if a container left any. -rm -f "$STAGE/.sign.sh" - -publish_tree - -printf 'published signed [breadway] repo -> %s\n' "$DEST" -ls -lh "$DEST" diff --git a/scripts/ci-verify-bake.sh b/scripts/ci-verify-bake.sh index e55b11e..1b01cab 100755 --- a/scripts/ci-verify-bake.sh +++ b/scripts/ci-verify-bake.sh @@ -1,20 +1,15 @@ #!/usr/bin/env bash -# Read-only checks that a builder home (and optionally a staged image) has +# 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. # -# 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 @@ -47,17 +42,6 @@ PY echo "== lockfile $LOCKFILE ==" echo " ${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional" -if grep -qE '^nvidia(-utils|-dkms|-open)?$' "$REPO/iso/packages.x86_64"; then - bad "iso/packages.x86_64 lists an nvidia driver package" -else - ok "iso/packages.x86_64 has no nvidia driver package" -fi -echo "== host tools ==" -if command -v grub-install >/dev/null 2>&1; then - ok "grub-install (uefi.grub bootmode)" -else - bad "grub-install missing — mkarchiso uefi.grub will abort (install grub on the builder)" -fi echo "== builder home $LAPTOP_HOME ==" check_exec() { @@ -122,108 +106,21 @@ else done fi -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" +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 "bakery prefix is not /usr/local in $AIROOTFS/etc/bakery/config.toml" + bad "skel unit missing: $SKEL/.config/systemd/user/$unit" fi - for b in "${REQUIRED_BINS[@]}"; do - check_exec "$AIROOTFS/usr/local/bin/$b" "image required bin $b" - done - check_exec "$AIROOTFS/usr/local/bin/bos-nvidia-setup" "image bos-nvidia-setup" - 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 - check_file "$SKEL/.config/hypr/hyprland.lua" "skel hyprland.lua" - if grep -q 'nvidia.lua' "$SKEL/.config/hypr/hyprland.lua"; then - ok "skel hyprland.lua includes nvidia.lua only if present" - else - bad "skel hyprland.lua does not mention nvidia.lua" - fi - 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 - check_file "$AIROOTFS/usr/lib/systemd/user-preset/90-bos-bakery.preset" \ - "bakery user preset" - if [[ -L "$AIROOTFS/etc/systemd/user/default.target.wants/breadd.service" ]] \ - || [[ -f "$AIROOTFS/etc/systemd/user/default.target.wants/breadd.service" ]]; then - ok "breadd.service globally enabled (etc wants)" - else - bad "breadd.service missing from /etc/systemd/user/default.target.wants" - fi - # After bake the image has /usr/local/bin/breadd and every preset unit. - # The committed airootfs only has the preset + breadd wants. - if [[ -f "$AIROOTFS/usr/lib/systemd/user-preset/90-bos-bakery.preset" ]] \ - && [[ -x "$AIROOTFS/usr/local/bin/breadd" ]]; then - while read -r verb unit; do - [[ "$verb" == enable && -n "$unit" ]] || continue - check_file "$AIROOTFS/usr/lib/systemd/user/$unit" "preset unit $unit" - if [[ -L "$AIROOTFS/etc/systemd/user/default.target.wants/$unit" ]] \ - || [[ -L "$AIROOTFS/etc/systemd/user/graphical-session.target.wants/$unit" ]]; then - ok "$unit globally enabled (etc wants)" - else - bad "$unit missing from /etc/systemd/user/*.target.wants" - fi - done < "$AIROOTFS/usr/lib/systemd/user-preset/90-bos-bakery.preset" - fi - if [[ -x "$AIROOTFS/usr/local/bin/bos-enable-bakery-user-units" ]]; then - ok "bos-enable-bakery-user-units executable" - else - bad "bos-enable-bakery-user-units missing or not executable" - fi - fi + done fi echo diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 28b5c28..ab70559 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -40,15 +40,22 @@ check "grub-btrfs present" "pacman -Qq grub-btrfs" echo "== enabled system services ==" for unit in NetworkManager.service greetd.service bluetooth.service tlp.service \ - cups.socket avahi-daemon.socket ufw.service systemd-timesyncd.service; do + cups.socket avahi-daemon.service ufw.service systemd-timesyncd.service; do check "$unit enabled" "systemctl is-enabled $unit" done check "graphical.target is default" "[ \"\$(systemctl get-default)\" = graphical.target ]" echo "== bread ecosystem on PATH ==" -for bin in bakery bread breadd bread-emit bread-module-host breadbar breadbox breadbox-sync breadcrumbs breadpad breadman; do +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" @@ -56,55 +63,15 @@ check "bos-settings installed" "command -v bos-settings" echo "== breadhelp ==" check "breadhelp installed" "command -v breadhelp" check "breadhelp content installed" \ - "[ -d /usr/local/share/breadhelp/content ] || [ -d \"\$HOME/.local/share/breadhelp/content\" ]" + "[ -d \"\$HOME/.local/share/breadhelp/content\" ] || [ -d /etc/skel/.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" -check "bos-nvidia-setup present" "command -v bos-nvidia-setup" -if pacman -Qq nvidia >/dev/null 2>&1; then - note "nvidia installed (optional proprietary path)" - check "nvidia env drop-in present" "[ -f \"\$HOME/.config/hypr/nvidia.lua\" ]" -else - check "nvidia not on the default image" "! pacman -Qq nvidia" -fi - -echo "== bakery user units (global enable) ==" -# A later useradd does not enable --user units unless they were enabled -# --global (or the user enables them). post-install + live-setup + bake -# write /etc/systemd/user/.wants/ and a preset listing the set. -check "bakery user preset present" \ - "[ -f /usr/lib/systemd/user-preset/90-bos-bakery.preset ]" -check "bos-enable-bakery-user-units present" \ - "command -v bos-enable-bakery-user-units" -check "breadd.service globally enabled" \ - "systemctl --global is-enabled breadd.service || [ -L /etc/systemd/user/default.target.wants/breadd.service ]" -if [[ -f /usr/lib/systemd/user-preset/90-bos-bakery.preset ]]; then - while read -r verb unit; do - [[ "$verb" == enable && -n "$unit" ]] || continue - [[ -f /usr/lib/systemd/user/$unit ]] || continue - check "$unit globally enabled" \ - "systemctl --global is-enabled $unit || [ -L /etc/systemd/user/default.target.wants/$unit ] || [ -L /etc/systemd/user/graphical-session.target.wants/$unit ]" - done < /usr/lib/systemd/user-preset/90-bos-bakery.preset -fi -check "skel hyprland.lua present" "[ -f /etc/skel/.config/hypr/hyprland.lua ]" -check "skel bakery installed.json present" \ - "[ -f /etc/skel/.local/state/bakery/installed.json ]" -check "skel bakery index cache present" \ - "[ -f /etc/skel/.cache/bakery/index.json ]" -check "skel has no bakery binaries" \ - "! [ -e /etc/skel/.local/bin/bakery ] && ! [ -e /etc/skel/.local/bin/breadd ]" -check "useradd SKEL is /etc/skel" \ - "grep -q '^SKEL=/etc/skel' /etc/default/useradd" echo "== default dotfiles ==" check "hyprland.lua present" "[ -f \"\$HOME/.config/hypr/hyprland.lua\" ]" -check "hyprland.lua includes nvidia.lua only if present" \ - "grep -q 'nvidia.lua' \"\$HOME/.config/hypr/hyprland.lua\"" check "binds.json present" "[ -f \"\$HOME/.config/hypr/binds.json\" ]" check "monitors.json present" "[ -f \"\$HOME/.config/hypr/monitors.json\" ]" check "settings.json present" "[ -f \"\$HOME/.config/hypr/settings.json\" ]" check "autostart.json present" "[ -f \"\$HOME/.config/hypr/autostart.json\" ]" -check "autostart includes first-boot probe" "grep -q bos-first-boot \"\$HOME/.config/hypr/autostart.json\"" check "hypr scripts/lib present" "[ -f \"\$HOME/.config/hypr/scripts/lib/json.lua\" ]" check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]" check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]"