diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..6a2f9ab --- /dev/null +++ b/.forgejo/workflows/mirror.yml @@ -0,0 +1,21 @@ +name: Mirror to GitHub + +on: + push: + branches: ['**'] + tags: ['**'] + +jobs: + mirror: + runs-on: [self-hosted, hestia] + steps: + - name: Mirror to GitHub + run: | + set -euo pipefail + git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git + cd repo.git + # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); + # --prune deletes GitHub refs that no longer exist on Forgejo. + git push --prune \ + "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bos.git" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/release-iso.yml b/.forgejo/workflows/release-iso.yml index 162fa16..862368e 100644 --- a/.forgejo/workflows/release-iso.yml +++ b/.forgejo/workflows/release-iso.yml @@ -1,21 +1,19 @@ name: Build and release ISO -# Builds the BOS ISO on the hestia self-hosted runner (native Arch container). -# Stages bakery desktop apps from the *minisign-verified* stable index at -# https://dl.breadway.dev/index.json (see iso/bread-lockfile.toml), then runs -# build-local.sh and uploads the ISO to a Forgejo release. A matching GitHub -# release is created best-effort and points at Forgejo for the download +# Builds the BOS ISO on the hestia self-hosted runner (native Arch container), +# downloads all bakery ecosystem binaries from their GitHub releases, compiles +# bread-theme from source, and uploads the resulting ISO to a Forgejo pre-release. +# A matching GitHub release is created that points to Forgejo for the download # (GitHub releases cannot host files larger than 2 GB). # # Required secrets: # RELEASE_TOKEN — Forgejo API token with write:repository scope -# MIRROR_TOKEN — GitHub personal access token with repo scope +# MIRROR_TOKEN — GitHub personal access token with repo scope (already used by mirror.yml) # GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing" # identity (releases@breadway.dev); public half is committed -# at KEYS.asc. Signs ISO SHA256SUMS here; the same secret -# signs the [breadway] repo in signed-repo.yml. No passphrase -# (CI-only key, access controlled via the Forgejo secret -# store). +# at KEYS.asc for verification. No passphrase (CI-only key, +# access controlled via the Forgejo secret store, not a +# passphrase nobody could type non-interactively anyway). on: push: @@ -30,8 +28,6 @@ jobs: release-iso: runs-on: [self-hosted, hestia] container: - # Floating tag: this environment cannot pin a reproducible digest of - # archlinux:latest. Do not invent one. image: archlinux:latest # --privileged: mkarchiso needs CAP_SYS_ADMIN for loop mounts + mknod # --network=host: gives localhost:3002 access to Forgejo (avoids the @@ -41,10 +37,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 rust - name: Determine tag and version id: vars @@ -62,17 +55,82 @@ jobs: git clone --branch "${{ steps.vars.outputs.tag }}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /bos - - name: Stage bakery ecosystem from signed stable index + - name: Download bakery ecosystem binaries run: | set -euo pipefail - cd /bos - LAPTOP_HOME=/build-home python3 scripts/ci-stage-bakery.py + mkdir -p /build-home/.local/bin \ + /build-home/.local/state/bakery \ + /build-home/.cache/bakery - - name: Verify staged bakery bake inputs + # Fetch the canonical bakery index + curl -fsSL "https://dl.breadway.dev/index.json" \ + -o /build-home/.cache/bakery/index.json + + # Download each binary from dl.breadway.dev (canonical source; github_url + # is not always published for dev/patch releases) and generate the + # installed.json that bakery expects in ~/.local/state. + python3 << 'PYEOF' + import json, urllib.request, os + + with open('/build-home/.cache/bakery/index.json') as f: + idx = json.load(f) + + BIN_DIR = '/build-home/.local/bin' + installed = {} + + for pkg_name, pkg in idx['packages'].items(): + bins = [] + for b in pkg['binaries']: + dest_name = b['name'].removesuffix('-x86_64') + dest = os.path.join(BIN_DIR, dest_name) + url = b['dl_url'] + print(f' {dest_name} <- {url}', flush=True) + urllib.request.urlretrieve(url, dest) + os.chmod(dest, 0o755) + bins.append(dest_name) + + # installed.json services field is a flat list of unit-name strings + services = [ + (s['unit'] if isinstance(s, dict) else s) + for s in pkg.get('services', []) + ] + installed[pkg_name] = { + 'name': pkg_name, + 'version': pkg['version'], + 'binaries': bins, + 'services': services, + 'installed_at': '2024-01-01T00:00:00+00:00', + } + + with open('/build-home/.local/state/bakery/installed.json', 'w') as f: + json.dump({'packages': installed}, f, indent=2) + print('installed.json written', flush=True) + PYEOF + + - name: Build bread-theme from source run: | set -euo pipefail - cd /bos - LAPTOP_HOME=/build-home bash scripts/ci-verify-bake.sh + # bread-theme is not in the bakery index; build it at the tag pinned + # in bos-settings' Cargo.toml so the CLI matches the library version + # the bos-settings package (and breadbar/breadbox/breadpad) were + # built against. bos-settings used to live at bos-settings/Cargo.toml + # inside this repo; it's since been split into its own repo + # (git.breadway.dev/Breadway/bos-settings), so fetch its Cargo.toml + # from there instead of a path that no longer exists in this + # checkout. Uses bos-settings' default branch (dev) — the branch its + # own CI actually publishes the `bos-settings` pacman package from. + REPO_OWNER="${GITHUB_REPOSITORY%%/*}" + curl -fsSL "https://git.breadway.dev/${REPO_OWNER}/bos-settings/raw/branch/dev/Cargo.toml" \ + -o /tmp/bos-settings-Cargo.toml + THEME_TAG=$(grep 'bread-theme.*tag' /tmp/bos-settings-Cargo.toml \ + | grep -oP '"v[^"]+"' | tr -d '"') + echo "Building bread-theme @ $THEME_TAG" + git clone --branch "$THEME_TAG" --depth 1 \ + https://github.com/Breadway/bread-ecosystem /bread-ecosystem + cd /bread-ecosystem + cargo build --release -p bread-theme + install -m 755 target/release/bread-theme /build-home/.local/bin/bread-theme + echo "bread-theme built OK" - name: Build ISO run: | @@ -186,21 +244,6 @@ jobs: gh release create "${TAG}" \ --repo "Breadway/bos" \ --title "BOS ${TAG}" \ + \ --notes-file /tmp/gh-release-notes.md \ - || echo "skip: GitHub release failed (MIRROR_TOKEN historically broken)" - - # `stable` is a marker branch only — CI fast-forwards it to whatever - # commit the latest real (non-RC) release tag points at. Never merged - # into by hand, so unlike the old dev/beta/main model it can't rot: - # nobody has to remember to move it, a bot always does. Lets you - # `git diff stable..main` before a build to see what's new since the - # last release, without a human-maintained promotion step. - - name: Fast-forward stable branch to this tag - if: ${{ !contains(steps.vars.outputs.tag, '-rc.') }} - env: - FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }} - run: | - set -euo pipefail - cd /bos - git push "https://oauth2:${FORGEJO_TOKEN}@git.breadway.dev/${GITHUB_REPOSITORY}.git" \ - "HEAD:refs/heads/stable" --force + 2>/dev/null || echo "GitHub release already exists — skipping" diff --git a/.forgejo/workflows/signed-repo.yml b/.forgejo/workflows/signed-repo.yml deleted file mode 100644 index 3ef8a65..0000000 --- a/.forgejo/workflows/signed-repo.yml +++ /dev/null @@ -1,52 +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 - 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/.gitignore b/.gitignore index 6e38d88..25b3b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,10 +42,3 @@ logs/ # Wallpaper source drop (baked copy lives in airootfs/usr/share/backgrounds) /Bread Background.png - -# Local hygiene notes (not for commit) -CLAUDE.md - -# Python -__pycache__/ -*.pyc diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 8019d5e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,58 +0,0 @@ -# AGENTS.md — Repo hygiene - -Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. Product shape is [README.md](README.md). - -## What this repo is - -ISO + Calamares + skel. **No Cargo workspace. No `bos-settings/` member.** -bos-settings (Tauri 2 + Svelte) and breadhelp are standalone bakery -products. breadlock is the only bread\* pacman package. - -Live skel is `iso/airootfs/etc/skel`. `dotfiles/` is stale. - -## Branch model - -Single-trunk: - -- `main` — integration + release trunk. Land work via short-lived - `feature/*` / `fix/*` branches, then merge. -- `stable` — marker only. CI fast-forwards it to the latest non-RC release - tag. Never merge into it by hand. - -There is no `dev` integration branch. - -## Remotes - -- `origin` — Forgejo (`ssh://git@100.66.238.26:2222/Breadway/bos.git`) — authoritative. -- `github` — GitHub (`https://github.com/Breadway/bos.git`) mirror. Push - origin (and github when mirroring). - -`origin` is **not** GitHub. - -## CI - -- `.forgejo/workflows/*.yml` trigger on `push: tags: ['v*']` (or - path-scoped packaging triggers) — ordinary pushes to `main` run nothing - except those path filters. Tag a release to build the ISO. -- `stable` is moved by the release-iso workflow, not by humans. -- No build/lint/test CI runs on ordinary commits or PRs — test locally - before merging to `main`. - -## Cleanup - -- Delete feature/fix branches (local + remote) once merged. Check with - `git branch --merged main`. -- Don't let merged branches accumulate. - -## Don't - -- Don't embed credentials in remote URLs — SSH or a credential helper only. -- Don't leave the default branch pointed at a feature branch on - GitHub/Forgejo. -- Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs - work does not require it. -- Don't tell users to `snapper rollback` blindly; GRUB pins - `rootflags=subvol=@`. Recovery is grub-btrfs reboot. Bakery desktop - apps on BOS are system-prefix `/usr/local` (`/etc/bakery/config.toml`); - snapper `@` snapshots include them. Do not move those bits back to - `~/.local` on the image (hermes / default bakery stay user-layout). diff --git a/DESIGN.md b/DESIGN.md index 31b22f8..d6d6585 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,26 +1,4 @@ -# BOS — historical design plan - -## Current architecture - -**Read [README.md](README.md) for how this repo actually ships.** This file -is the original plan. Several sections below are historical and must not be -taken as current: - -| Plan said | What the tree does now | -|-----------|------------------------| -| Cargo workspace with a `bos-settings/` member | This repo is ISO + Calamares + skel only. No Cargo workspace. | -| `bos-settings` as an in-tree GTK4 app | Standalone bakery product, **Tauri 2 + Svelte**. | -| bakery install in Calamares post-install | bakery binaries + breadhelp content are **baked into `/etc/skel` at ISO build time** from `iso/bread-lockfile.toml`. Missing bins fail the bake. | -| `dotfiles/` is the live skel | Live defaults are `iso/airootfs/etc/skel`. `dotfiles/` is stale. | -| A/B root swapping | **Future.** Today: btrfs + snapper + **grub-btrfs**. GRUB pins `rootflags=subvol=@`, so `snapper rollback` is not the user-facing recovery path. | -| Work on `dev`; origin = GitHub | Single-trunk `main`; `stable` is a CI marker. `origin` = Forgejo, `github` = GitHub. | -| `[breadway]` provides bakery/breadbar/bos-settings | `[breadway]` is breadlock + AUR republishes. Desktop apps are bakery. **Not shipped:** breadcast, breadarr. | -| NVIDIA / A/B / Secure Boot / LUKS2 | NVIDIA proprietary is **unsupported**. A/B root swapping is **not implemented**. Secure Boot is **Setup Mode only** (self-signed `sbctl`). Disk encryption is **LUKS1** because GRUB cannot unlock LUKS2+Argon2id. | -| `SigLevel = Required` on `[breadway]` | **No.** Forgejo's Arch registry has no pacman-compatible db signatures. `SigLevel = Never` is TLS only; flipping Required without a signed db breaks installs. `KEYS.asc` signs ISO SHA256SUMS, not the pacman repo. | - ---- - -# Original plan (kept for history) +# BOS — Bread Operating System Plan ## Context @@ -29,25 +7,51 @@ The bread ecosystem (bread, breadbar, breadbox, breadcrumbs, breadpad/breadman, Goals: - **Install and be done**: Calamares GUI installer → reboot → working Hyprland + full bread stack - **Rollback safety**: Btrfs subvolumes + snapper + snap-pac; every pacman transaction is snapshotted -- **Unified config**: `bos-settings` surfaces all app configs + snapshot management + bakery updates +- **Unified config**: `bos-settings` GTK4 app surfaces all app configs + snapshot management + bakery updates - **Future-compatible**: Btrfs layout is designed to allow A/B partition migration later (SteamOS model) --- ## Repo Structure -Single new repo: `Breadway/bos` — *planned as* a Cargo workspace. **That is -not what landed**; see Current architecture. +Single new repo: `Breadway/bos` — a Cargo workspace. ``` bos/ -├── Cargo.toml # Workspace (members: [bos-settings]) — NOT in tree -├── bos-settings/ # planned GTK4 app — now its own bakery repo -├── iso/ # archiso profile (this is the repo) +├── Cargo.toml # Workspace (members: [bos-settings]) +├── bos-settings/ # GTK4 unified settings app +│ ├── Cargo.toml +│ └── src/ +│ ├── main.rs +│ ├── state.rs +│ ├── theme.rs +│ ├── ui/ +│ │ ├── window.rs # Sidebar + content shell (port breadman pattern) +│ │ ├── sidebar.rs +│ │ └── views/ +│ │ ├── bread.rs +│ │ ├── breadbar.rs +│ │ ├── breadbox.rs +│ │ ├── breadcrumbs.rs +│ │ ├── breadpad.rs +│ │ ├── snapshots.rs +│ │ ├── packages.rs +│ │ └── hyprland.rs +│ └── config/ +│ └── mod.rs # Per-app config loaders +├── iso/ # archiso profile │ ├── profiledef.sh -│ ├── packages.x86_64 -│ └── airootfs/ -└── dotfiles/ # planned install-time configs — NOT the live skel +│ ├── packages.x86_64 # Live ISO + installed system package list +│ ├── airootfs/ # Files overlaid onto live ISO root +│ │ └── etc/ +│ │ ├── calamares/ # Calamares YAML configuration +│ │ └── skel/ # Default user dotfiles +└── dotfiles/ # Default configs deployed at install time + ├── hyprland/ # hyprland.conf, keybinds, autostart + ├── bread/ # breadd.toml, init.lua, devices.lua + ├── breadbar/ # (no config needed; zero-config by default) + ├── breadbox/ # config.toml with default context priorities + └── breadcrumbs/ # breadcrumbs.toml with default home profile ``` --- @@ -66,11 +70,7 @@ bos/ Mount options: `noatime,compress=zstd,space_cache=v2` on all subvolumes. -**A/B compatibility note (future):** The `@` subvolume is self-contained and -could be swapped atomically. This is a design property for a later upgrade -path. It is **not** implemented. Recovery today is reboot into a grub-btrfs -snapshot; GRUB's `rootflags=subvol=@` means a raw `snapper rollback` is the -wrong instruction to give users. +**A/B compatibility note:** The `@` subvolume is self-contained and can be swapped atomically — this is the design property needed for a future A/B upgrade path. The layout does not need to change to adopt it. ### Snapshot tooling (installed + configured during post-install) @@ -96,79 +96,100 @@ No user-facing CLI needed for this component — `bos-settings` is the interface ### archiso profile (`iso/`) - Derives from `/usr/share/archiso/configs/releng/` (the standard baseline) -- `packages.x86_64` is the live + installed pacman set (Hyprland, Calamares, - breadlock, WebKitGTK 4.1 for Tauri bos-settings, …). bakery apps are not - listed here. -- `airootfs/etc/skel/` contains the default user configs (this is the live - skel — not `dotfiles/`). +- `packages.x86_64` includes: base, linux, grub, btrfs-progs, snapper, snap-pac, grub-btrfs, hyprland, pipewire, wireplumber, networkmanager, gtk4, gtk4-layer-shell, iw, librsvg, libpulse, bluez, bluez-utils, calamares, calamares-qt6 +- `airootfs/etc/skel/` contains the default dotfiles (symlinked from `dotfiles/`) - Live session autologs into a `liveuser` and launches Calamares automatically ### Calamares modules (in order) -The historical list below included a post-install `bakery install` and -Calamares `bootloader`/`grubcfg` installing GRUB. What shipped instead: -binaries are already in skel; `post-install.sh` runs `grub-install` + -`grub-mkconfig` (Calamares' bootloader modules leave the ESP empty here). - 1. **welcome** — system checks (RAM ≥ 2GB, internet, disk space) 2. **locale** — timezone + locale selection 3. **keyboard** — layout selection 4. **partition** — custom `btrfs` mode: creates EFI partition + single btrfs pool with the subvolume layout above 5. **users** — create main user, set password 6. **packages** — install package list (reuses `packages.x86_64`) -7. **bootloader** — *planned*; actual GRUB install is in `post-install.sh` -8. **shellprocess (post-install)** — snapper, services, copy skel; does **not** run bakery +7. **bootloader** — install GRUB to EFI, `grub-mkconfig` with grub-btrfs hook +8. **shellprocess (post-install)** — runs `iso/post-install.sh`: + - Configures snapper root config + - Enables services: `NetworkManager`, `bluetooth`, `breadd` (user), `breadbox-sync` (user) + - Runs `bakery install bread breadbar breadbox breadcrumbs breadpad` (or `bakery install --all`) + - Copies `dotfiles/` into `/home/$USER/.config/` (skips any file that already exists) 9. **finished** — reboot prompt --- -## Component 3: `bos-settings` (planned as GTK4) - -### Tech choices (original) +## Component 3: `bos-settings` GTK4 App +### Tech choices - **gtk4-rs** (v0.11, v4_12 feature), no relm4 — plain GTK4 following breadman's pattern - -**What shipped:** Tauri 2 + Svelte in its own repo -(`git.breadway.dev/Breadway/bos-settings`), distributed by bakery. This -repo does not build it. +- **bread-theme** for palette + CSS (git dep: `github.com/Breadway/bread-ecosystem`) +- Reads/writes each tool's own config file directly (no unified intermediate config) +- Window: 960×640, sidebar 190px, `gtk4::Stack` for view switching — identical structure to breadman ### Sidebar sections + views -The panel list is still roughly accurate; see README. Snapshots recovery -should send users through **grub-btrfs reboot**, not `snapper rollback N`. +| Section | View | What it does | +|---------|------|--------------| +| **Apps** | bread | Edit `~/.config/bread/breadd.toml` | +| | breadbar | Edit `~/.config/breadbar/` (style.css override, no TOML needed) | +| | breadbox | Edit `~/.config/breadbox/config.toml` (context priority lists) | +| | breadcrumbs | Edit `~/.config/breadcrumbs/breadcrumbs.toml` (profiles, networks) | +| | breadpad | Edit `~/.config/breadpad/breadpad.toml` (model, reminders, calendar) | +| **System** | Snapshots | `snapper list` output; rollback button calls `snapper rollback N` | +| | Packages | `bakery list --installed`; update buttons call `bakery update ` | +| | Hyprland | "Open config in editor" + monitor list from `bread.state.monitors()` | + +### Config loading pattern + +Each view has a dedicated `load_config(path) -> Result` and `save_config(path, T) -> Result<()>` using `toml` crate. Config structs mirror each app's existing types (no duplication — import the `*-shared` crate where it exists, e.g. `breadpad-shared`). For apps without a shared crate (breadbox, breadcrumbs), define minimal local structs. + +### Snapshots view specifics + +- On open: runs `snapper list --output-cols number,date,description,pre-post` via `std::process::Command`, parses into table rows +- Rollback: confirmation dialog → `snapper rollback ` → notify user to reboot +- Delete: `snapper delete ` +- No write access to `/` needed for list/rollback since snapper is configured with `ALLOW_USERS` for the main user + +### Packages view specifics + +- On open: reads `~/.local/state/bakery/installed.json` directly (no network) +- "Check for updates": runs `bakery list` (triggers index refresh), compares versions +- "Update all": runs `bakery update --all` in a subprocess, streams stdout to a log TextView ### Distribution -`bos-settings` has its own `bakery.toml` and is installable via -`bakery install bos-settings` on any Arch/Hyprland system, not only as part -of a BOS install. +`bos-settings` gets a `bakery.toml` and is added to the `bread-ecosystem` registry — installable standalone on any Arch/Hyprland system via `bakery install bos-settings`, not only as part of a BOS install. --- ## Component 4: Default Dotfiles -Minimal but functional defaults. These live in `iso/airootfs/etc/skel` -(`hyprland.lua` + JSON binds, not `dotfiles/hyprland/*.conf`). +Minimal but functional defaults deployed at install time. These are opinionated starting points, not locked configs — users edit freely after install. -Zero-config bakery apps survive with no extra skel files. breadcrumbs -networks are user-filled after install — do not invent a full -`breadcrumbs.toml` in-tree. +| File | Key content | +|------|-------------| +| `dotfiles/hyprland/hyprland.conf` | Monitor auto-detect, default keybinds, `exec-once` for breadd/breadbar/breadbox-sync | +| `dotfiles/hyprland/keybinds.conf` | `$mod+Space` → breadbox, `$mod+N` → breadpad, `$mod+M` → breadman, `$mod+S` → bos-settings | +| `dotfiles/bread/breadd.toml` | All adapters enabled, log_level=info | +| `dotfiles/bread/init.lua` | Minimal: activates "default" profile on startup | +| `dotfiles/breadbox/config.toml` | Single default context with common apps | +| `dotfiles/breadcrumbs/breadcrumbs.toml` | Placeholder home profile (user fills in SSIDs) | --- ## Build Order -Historical. The ISO profile + skel + Calamares path is what this repo -iterates on. bos-settings is developed in its own repo. +1. **Dotfiles** — write default configs; these unblock installer testing immediately +2. **Btrfs + snapper config** — write `post-install.sh`; test in a VM with `archiso` livecdbase +3. **ISO profile** — archiso profiledef + package list + Calamares YAML; iterate in a VM +4. **bos-settings** — start with Snapshots and Packages views (highest value, no app-specific config parsing needed), then add per-app views one at a time --- ## Verification -- **ISO**: `sudo ./build-local.sh` (not a raw `mkarchiso iso/` — the bake - step is required). Boot in QEMU; complete install; confirm bakery bins and - `~/.local/share/breadhelp/content`. +- **ISO**: Build with `mkarchiso -v -w /tmp/bos-work -o /tmp/bos-out iso/`; boot in QEMU (`qemu-system-x86_64 -cdrom bos.iso -m 4G -enable-kvm`); complete install; reboot into installed system; confirm all services running and bakery packages present - **btrfs layout**: `btrfs subvolume list /` after install; confirm `@`, `@home`, `@snapshots`, `@log`, `@cache` exist - **snapper**: `snapper list`; run `pacman -Syu` and confirm two new snapshots appear - **grub-btrfs**: Reboot and confirm snapshot submenu in GRUB -- **bos-settings**: built and tested in the bos-settings repo, not here +- **bos-settings**: `cargo build --release`; launch; confirm each view loads its config file; edit a value, save, re-open and confirm persistence; test rollback button in Snapshots view diff --git a/README.md b/README.md index 1a833b6..7eb401b 100644 --- a/README.md +++ b/README.md @@ -5,138 +5,74 @@ ecosystem](https://git.breadway.dev/Breadway) preconfigured. One Calamares insta produces a themed, bootable Wayland desktop — no manual Arch bootstrap, no wiring up dotfiles, no per-tool bakery installs. -> This file is the product as the tree ships it. [DESIGN.md](DESIGN.md) is the -> original plan, kept as history — several of its sections (in-tree GTK -> bos-settings, bakery-at-post-install, A/B as if it were current) are not -> how the ISO works today. +> Design rationale and the btrfs/A-B roadmap live in [DESIGN.md](DESIGN.md). +> This file is the practical overview: what's in the image, how to build it, +> and how to test it. ## What you get - **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated keybinds, snappy animations, blur, and pywal-driven colours on a black base. -- **bread ecosystem**, baked into `/usr/local` from bakery-managed binaries - (no network needed at install time; per-user bakery state is seeded in - `/etc/skel`): the `bread`/`breadd` automation daemon - (`bread-emit` / `bread-module-host` when the stable bread release publishes - them), `breadbar` (status bar + notifications), `breadbox` (launcher), - `breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles), - `breadpad`/`breadman` (notes), `breadpaper` (wallpaper + theme), - `breadsearch` (system search), `breadmon` (monitor layout TUI), - `breadshot` (screenshots), `bread-theme` (the shared palette engine), - `breadhelp` (onboarding + cheatsheet), `bos-settings` (control panel), - and the `bakery` package manager. Most of those apps are zero-config on - first boot; breadcrumbs networks are user-filled after install. See - [below](#the-bread-ecosystem). -- **breadlock** (lock screen + greeter) is the one bread\* app that ships as - **pacman**, not bakery — it needs a root-owned PAM service. -- **bos-settings**: a **Tauri 2 + Svelte** control panel (standalone bakery - product, not a member of this repo). Configures every bread\* app - non-destructively, plus snapshots, bakery/pacman updates, and day-to-day - machine administration. -- **Login**: greetd + breadgreet (under `cage`) → Hyprland session. +- **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries + (no network needed at install time): the `bread`/`breadd` automation daemon, + `breadbar` (status bar + notifications), `breadbox` (launcher), `breadclip` + (clipboard history), `breadcrumbs` (Wi-Fi profiles), `breadpad`/`breadman` + (notes), `breadpaper` (wallpaper + theme), `breadsearch` (system search), + `breadmon` (monitor layout TUI), `breadshot` (screenshots), `bread-theme` + (the shared palette engine), and the `bakery` package manager. `breadlock` + (lock screen + greeter) ships as its own pacman package alongside + `bos-settings`, not through bakery. See [below](#the-bread-ecosystem) for + what each one actually does. +- **bos-settings**: a GTK4 control panel that configures every bread\* app's + config from a GUI (non-destructively), plus snapshot rollback and bakery + updates. See below. +- **Login**: greetd + breadgreet (bread-ecosystem's own greeter, under `cage`) + → Hyprland session. - **Boot splash**: Plymouth `bos` theme (logo + spinner, black background). - **Theming**: global dark across GTK3 (Adwaita-dark), GTK4/libadwaita (`color-scheme: prefer-dark`), and Qt (qt5ct/qt6ct Fusion dark); Papirus-Dark icons; Bibata cursor. - **Apps**: kitty, nautilus (+ gvfs), Zen browser, VLC, loupe, gnome-text-editor, gnome-calculator, file-roller, with file associations wired in `mimeapps.list`. - `yay` ships for AUR access beyond bakery + `[breadway]`. + `yay` ships for AUR access beyond bakery's bread ecosystem + `[breadway]`. - **Hardware**: pipewire audio, NetworkManager, BlueZ + blueman, CUPS printing with avahi mDNS discovery, TLP power management, fwupd firmware updates. - Mesa only — **NVIDIA proprietary drivers are not included** and NVIDIA is - unsupported out of the box (see [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 - [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 - `sbctl` — not a Microsoft-signed shim; enrollment is skipped unless the - firmware is already in Setup Mode. - -## What ships vs what does not - -| Channel | What | -|---------|------| -| **Bakery, required** | `bakery`, `bread` / `breadd`, `breadbar`, `breadbox` / `breadbox-sync`, `breadcrumbs`, `breadpad` / `breadman`, `breadpaper`, `bread-theme`, `breadmon`, `breadsearch` / `breadmill`, `breadclip` / `breadclipd`, `breadshot`, `bos-settings`, `breadhelp` (+ breadhelp content under `/usr/local/share/breadhelp/`) | -| **Bakery, optional** | `bread-emit`, `bread-module-host` — baked when the verified stable index publishes them; skipped (not a failed bake) until bread ships them | -| **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) | -| **Not shipped** | `breadcast`, `breadarr` | - -The baked name list is [`iso/bread-lockfile.toml`](iso/bread-lockfile.toml) -(plus optional `[versions]` / `[[pin]]` so CI fetches -`https://dl.breadway.dev///...`). `build-local.sh` fails if any -**required** binary is missing on the builder. + pacman transaction; zram swap; ufw firewall (deny-incoming, mDNS allowed). +- **Security**: full-disk encryption (LUKS, via Calamares' built-in support — + cryptsetup + the matching mkinitcpio/GRUB wiring ship so an encrypted + install actually boots) and self-signed Secure Boot (via `sbctl`, enrolled + automatically at install time when the firmware is in Setup Mode). ## Repo layout -This is an **ISO + Calamares + skel** repo. There is no Cargo workspace and -no `bos-settings/` member — bos-settings and breadhelp live in their own -repos and arrive via bakery. - ``` bos/ +├── Cargo.toml # workspace (members: bos-settings) +├── bos-settings/ # GTK4 unified settings app (Rust) +│ └── src/ +│ ├── config/mod.rs # non-destructive toml_edit config layer +│ └── ui/{widgets,window,sidebar}.rs, ui/views/*.rs ├── iso/ # archiso profile -│ ├── bread-lockfile.toml # bakery bins + optional version pins │ ├── profiledef.sh -│ ├── packages.x86_64 # live + installed pacman set +│ ├── packages.x86_64 # live + installed package set │ └── airootfs/ # files overlaid onto the image │ └── etc/ -│ ├── skel/ # live user defaults (hypr, kitty, gtk, …) +│ ├── skel/ # default user dotfiles (hypr, kitty, gtk, …) │ └── calamares/ # installer config + post-install.sh ├── packaging/ # in-house PKGBUILDs for AUR-only deps +│ ├── arch/ # bos-settings │ ├── calamares/ -│ ├── bibata/ -│ ├── powerlevel10k/ -│ └── yay-bin/ -├── dotfiles/ # STALE — not the live skel; see its README -├── scripts/ -│ ├── ci-stage-bakery.py # CI: minisign-verified index → $LAPTOP_HOME -│ ├── ci-verify-bake.sh # CI: read-only checks before mkarchiso -│ ├── 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 +│ └── bibata/ +├── .forgejo/workflows/ # CI: build + publish packages to [breadway] ├── build-local.sh # native ISO build for this machine -├── README.md -└── DESIGN.md # historical plan +└── DESIGN.md ``` -Live binds are `iso/airootfs/etc/skel/.config/hypr/binds.json` (`Super+L` → -`loginctl lock-session`, breadshot on `Super+Shift+S/C/P`, `Super+U` -breadpad). Do not treat `dotfiles/hypr/keybinds.conf` as current. - -## Branches and remotes - -Single-trunk: work on **`main`** via short-lived `feature/*` / `fix/*` -branches. **`stable`** is a marker branch CI fast-forwards to the latest -non-RC release tag — do not land work there by hand. - -Dual remotes: - -- **`origin`** — Forgejo (`ssh://git@100.66.238.26:2222/Breadway/bos.git`), - authoritative -- **`github`** — GitHub (`https://github.com/Breadway/bos.git`) mirror - -Push `origin` (and `github` when mirroring). Do not treat origin as GitHub. - ## Building the ISO -`build-local.sh` builds the image natively (no container) and 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 into `/etc/skel`: ```sh sudo ./build-local.sh # release-quality (xz squashfs) @@ -144,24 +80,16 @@ sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration (zstd squashfs) ``` The ISO lands in `out/bos--x86_64.iso`. The script pins -`SOURCE_DATE_EPOCH` (reproducible UUIDs), rewrites the `[breadway]` repo URL -to the Tailscale-reachable Forgejo registry for the build, and **exits -non-zero** if any **required** lockfile binary (or breadhelp content) is -missing. Optional bins are skipped with a warning. - -CI stages the builder from the **minisign-verified** stable bakery index -(`index.json` + `index.json.minisig`) and prefers lockfile `[versions]` -URLs (`https://dl.breadway.dev///...`) when set, so two bakes -of the same commit fetch the same bits. Local builds still snapshot the -builder. +`SOURCE_DATE_EPOCH` (reproducible UUIDs) and rewrites the `[breadway]` repo URL +to the Tailscale-reachable Forgejo registry for the build. ### Why some packages are in-house `calamares`, `zen-browser-bin`, `bibata-cursor-theme`, and `yay-bin` are -AUR-only. BOS keeps a PKGBUILD for each under `packaging/` and republishes -the built package to the `[breadway]` repo via a Forgejo Actions workflow -(built on the hestia self-hosted runner, published with a scoped registry -token). `[breadway]` is **not** where bakery/breadbar/bos-settings live. +AUR-only. BOS keeps a PKGBUILD for each under `packaging/` and republishes the +built package to the `[breadway]` repo via a Forgejo Actions workflow (built +on the hestia self-hosted runner, published with a scoped registry token). +`bos-settings` itself publishes the same way on a `v*` tag. ### Verifying a release @@ -174,11 +102,7 @@ dedicated release-signing key (not reused from anything else): 5620 3B86 A110 695A E7F3 1093 4AF3 323D 678E B5E2 ``` -The public half is committed at [`KEYS.asc`](KEYS.asc). That key signs -**ISO checksums only** — it does not sign the `[breadway]` pacman repo -(Forgejo's Arch registry has no pacman-compatible db signatures; that -section stays `SigLevel = Never` until a signed repo exists — see -[docs/signed-repo.md](docs/signed-repo.md)). To verify a download: +The public half is committed at [`KEYS.asc`](KEYS.asc). To verify a download: ```sh gpg --import KEYS.asc @@ -200,51 +124,19 @@ It uses KVM + `-cpu host`, 8 GiB / 8 vCPU, and `virtio-vga-gl` with Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to avoid memory pressure. -Post-install, `scripts/smoke-test.sh` (run as the installed user) checks -subvolumes, services, bakery bins 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. - ## bos-settings -Standalone bakery product: **Tauri 2 + Svelte**, not GTK4, and not built -from this repo. Install/update with `bakery`; the ISO just bakes whatever -binary the builder has. +A GTK4 settings app aiming for GNOME-Settings-style parity: not just editing +config files, but live system state and control, so day-to-day machine +administration doesn't require a terminal. -It aims for GNOME-Settings-style parity: live system state and control, so -day-to-day administration doesn't require a terminal. Bread-ecosystem -configs are edited **non-destructively** (comments and unmodeled keys stay). -Panels with a daemon (bread, breadbox, breadcrumbs, breadsearch, breadclip) -also get live systemd status + Start/Stop/Restart/Logs. +Bread-ecosystem TOML configs are edited **non-destructively**: `toml_edit` +parses the file, changes only the keys a view exposes, and writes it back — +preserving comments and any keys the UI doesn't model (calendar passwords, +saved-network passwords, model paths). Panels with a daemon behind them +(bread, breadbox, breadcrumbs, breadsearch, breadclip) also get live +systemd status + Start/Stop/Restart/Logs via a shared `service_control` +widget, not just the config file. | Panel | What it does | |-------|--------------| @@ -267,29 +159,32 @@ 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. +Build standalone: + +```sh +cargo build --release -p bos-settings +cargo test -p bos-settings # includes config round-trip tests +``` ## 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 -directly. +corresponding **bos-settings** panel for configuration; this table is about +*using* the app directly. **Desktop shell** | Tool | Role | Launch | |------|------|--------| -| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor/power/network signals into events dispatched to Lua modules (`~/.config/bread/`). `bread-emit` is the fire-and-forget helper hooks/CLIs use; `bread-module-host` is the sandboxed out-of-process module runtime breadd spawns. Both extra bins are **optional** on the ISO until a stable bread release publishes them. | runs at login (`breadd.service`) | +| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor/power/network signals into events dispatched to Lua modules (`~/.config/bread/`). Everything else in the ecosystem can subscribe to its events. | runs at login (`breadd.service`) | | `breadbar` | Top status bar: workspaces, clock, system stats, tray, **and** the notification daemon — one process, not two | runs at login | | `breadbox` | Application launcher (fuzzy search, per-context results via `breadbox-sync`) | `SUPER+Space` | -| `breadlock` | Idle lock screen. Also provides `breadgreet`, the login greeter hosted under `cage` via greetd — same project, two binaries, one visual identity from login to lock. **pacman**, not bakery. | `SUPER+L` (via `loginctl lock-session`, picked up by `hypridle`); `breadgreet` runs automatically at boot | +| `breadlock` | Idle lock screen. Also provides `breadgreet`, the login greeter hosted under `cage` via greetd — same project, two binaries, one visual identity from login to lock | `SUPER+L` (via `loginctl lock-session`, picked up by `hypridle`); `breadgreet` runs automatically at boot | | `bread-theme` | The shared palette engine every bread app renders through: fixed dark base colors, with only the accent slots following the current wallpaper's pywal palette. `bread-theme generate` regenerates the stylesheet; hyprland.lua calls it automatically on wallpaper change. | invoked automatically, rarely run by hand | **Productivity** @@ -300,7 +195,6 @@ directly. | `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` | | `breadclip` | Clipboard history. `breadclipd` is the background daemon that actually records history; `breadclip` is the GTK4 popup that browses it | `SUPER+V` / `SUPER+Shift+V` | | `breadsearch` | Semantic system-wide search (indexes files/notes, embeds locally — CPU/ROCm/CUDA backend configurable). `breadmill` is its indexing daemon. | via breadbox, or BOS Settings → File Search | -| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `/usr/local/share/breadhelp/content` (bakery `content.tar.gz`, baked into the image). | `SUPER+/` | **System** @@ -316,7 +210,7 @@ directly. | Tool | Role | Launch | |------|------|--------| | `bakery` | CLI package manager for the whole ecosystem — install/update/list, tracks installed binaries + versions independently of pacman | `bakery` | -| `bos-settings` | Unified Tauri 2 + Svelte control panel: live system state + control (network, power, firewall, users, packages, firmware, AUR, snapshots) plus non-destructive config editing for every app above | `SUPER+,` | +| `bos-settings` | Unified GTK4 control panel: live system state + control (network, power, firewall, users, packages, firmware, AUR, snapshots) plus non-destructive config editing for every app above | `SUPER+,` | ## Keyboard shortcuts @@ -354,13 +248,9 @@ cheatsheet in-session; first boot shows a short welcome (once). ## Known limitations -See [docs/hardware.md](docs/hardware.md) (GPUs, NVIDIA, recovery) and -[docs/signed-repo.md](docs/signed-repo.md) (`[breadway]` stays unsigned -until `dl.breadway.dev/arch` exists). - - **GPUs**: ships the generic Mesa stack — AMD and Intel work out of the box. - NVIDIA is **unsupported** (no proprietary driver, no NVIDIA firmware). See - [docs/hardware.md](docs/hardware.md). + The **NVIDIA proprietary driver is not included**; NVIDIA users must install + `nvidia`/`nvidia-utils` and set the usual Hyprland env vars after install. - **Virtual machines**: Hyprland needs GPU acceleration to be smooth. Use `virtio-vga-gl` + `-display gtk,gl=on` (virgl); plain software rendering is noticeably laggy. @@ -379,41 +269,26 @@ until `dl.breadway.dev/arch` exists). BOS ships the matching `cryptsetup`/mkinitcpio/GRUB wiring so an encrypted install actually boots (LUKS1, since GRUB doesn't support LUKS2 + Argon2id). - **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 = Never` until a signed repo is - stood up at `dl.breadway.dev/arch`. See [docs/signed-repo.md](docs/signed-repo.md). + btrfs subvolume layout the installer creates. ## Recovery -**An update broke something (system still boots):** reboot → **GRUB -“snapshots” submenu** (grub-btrfs), then boot that entry. - -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). - -A/B root swapping (SteamOS-style) is a **future** idea in DESIGN.md — it is -not shipped. +**An update broke something (system still boots):** open BOS Settings → +Snapshots and roll back, or pick a pre-update snapshot from the **GRUB +“snapshots” submenu** at boot, then run `snapper rollback` from the booted +snapshot. **The system won't boot (broken GRUB / lost EFI entry):** 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 e360d42..942476a 100755 --- a/build-local.sh +++ b/build-local.sh @@ -41,402 +41,89 @@ 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 ------ -# 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. -# -# CI should prefer the stable bakery index when populating the builder home. -# Local builds still snapshot the builder. required_bins fail the bake if -# missing; optional_bins are skipped with a warning (a hollow ISO is worse -# than a failed build). A flat `bins` list is treated as all-required. -LOCKFILE="$REPO/iso/bread-lockfile.toml" -if [[ ! -f "$LOCKFILE" ]]; then - echo "ERROR: bakery lockfile missing: $LOCKFILE" >&2 - exit 1 -fi -eval "$(python3 - "$LOCKFILE" <<'PY' -import sys, tomllib -path = sys.argv[1] -with open(path, "rb") as f: - data = tomllib.load(f) -required = data.get("required_bins") -optional = data.get("optional_bins") or [] -if required is None: - required = data.get("bins") or data.get("binaries") -if not isinstance(required, list) or not required: - sys.exit(f"{path}: missing non-empty required_bins (or bins) list") -if not isinstance(optional, list): - sys.exit(f"{path}: optional_bins must be a list") -blocked = {"breadcast", "breadarr"} -for label, names in (("required_bins", required), ("optional_bins", optional)): - for b in names: - if not isinstance(b, str) or not b or "/" in b or b in (".", ".."): - sys.exit(f"{path}: invalid {label} name {b!r}") - if b in blocked: - sys.exit(f"{path}: {b} is not shipped on the ISO") -def emit(name, values): - print(f"{name}=(") - for v in values: - print(f" {v!r}") - print(")") -emit("REQUIRED_BINS", required) -emit("OPTIONAL_BINS", optional) -PY -)" -if [[ ${#REQUIRED_BINS[@]} -eq 0 ]]; then - echo "ERROR: $LOCKFILE produced an empty required bins list" >&2 - exit 1 -fi - +# --- Bake this laptop's bakery-installed bread ecosystem into /etc/skel ------- +# The bread apps are managed by bakery (which fetches release binaries from +# GitHub), not pacman. bakery needs DNS at install time, which the live/installed +# image doesn't have — so instead of running bakery on the target, we copy the +# exact binaries + bakery manifest this laptop already has into skel. Every user +# created from skel (the live user and the installed user) then gets the same +# versions `bakery list` reports here, fully offline. Copied at build time so the +# binaries never bloat the git repo and always track the current bakery state. +BREAD_BINS=(bakery bread breadd breadman breadbar breadbox breadbox-sync breadcrumbs breadpad breadpaper bread-theme breadmon breadsearch breadmill breadclip breadclipd breadshot) LAPTOP_HOME="${LAPTOP_HOME:-$(getent passwd "${SUDO_USER:-$USER}" | cut -d: -f6)}" BAKERY_BIN="$LAPTOP_HOME/.local/bin" BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery" BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery" -BAKERY_SHARE="$LAPTOP_HOME/.local/share" -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 - if [[ ! -x "$BAKERY_BIN/$b" ]]; then - missing+=("$BAKERY_BIN/$b") - fi -done -if [[ ${#missing[@]} -gt 0 ]]; then - echo "ERROR: bakery lockfile requires binaries that are missing on the builder:" >&2 - printf ' %s\n' "${missing[@]}" >&2 - echo "Install them with bakery (or stage them under $BAKERY_BIN) before baking." >&2 - echo "A hollow ISO is worse than a failed build." >&2 - exit 1 -fi - -BREAD_BINS=("${REQUIRED_BINS[@]}") -for b in "${OPTIONAL_BINS[@]}"; do - if [[ -x "$BAKERY_BIN/$b" ]]; then - BREAD_BINS+=("$b") - else - echo "WARN: optional lockfile bin missing, skipping: $BAKERY_BIN/$b" >&2 - fi -done - -install -d -m 0755 "$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 -# appear installed when their binaries were deliberately left out). -python3 - "$BAKERY_STATE/installed.json" "$SKEL/.local/state/bakery/installed.json" "${BREAD_BINS[@]}" <<'PY' -import json, sys -src, dest, *bins = sys.argv[1:] -wanted = set(bins) -with open(src) as f: - data = json.load(f) -pkgs = data.get("packages", data) -if not isinstance(pkgs, dict): - sys.exit(f"{src}: expected packages object") -kept = {} -for name, pkg in pkgs.items(): - pbins = pkg.get("binaries") or [] - if name in wanted or any(b in wanted for b in pbins): - kept[name] = pkg -out = {"packages": kept} -if "track" in data: - out["track"] = data["track"] -with open(dest, "w") as f: - json.dump(out, f, indent=2) - f.write("\n") -print("installed.json packages:", ", ".join(sorted(kept)) or "(none)") -PY - +install -m 0644 "$BAKERY_STATE/installed.json" "$SKEL/.local/state/bakery/installed.json" # bakery fetches its package index from dl.breadway.dev (then a GitHub fallback), # but falls back to a cached index when both are unreachable. With no network/DNS # in the live/installed image, even `bakery list` errors unless that cache exists, # so bake it in too — then bakery works fully offline (list/info from cache; # install/update still need network, as expected). -if [[ ! -f "$BAKERY_CACHE/index.json" ]]; then - echo "ERROR: bakery index cache missing: $BAKERY_CACHE/index.json" >&2 - exit 1 -fi install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json" -echo "baked bins: $(ls "$IMAGE_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 ===" -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 "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" - -python3 - "$BAKERY_CACHE/index.json" "$BAKERY_SHARE" "$IMAGE_SHARE" "${BREAD_BINS[@]}" <<'PY' -import json, os, shutil, sys -index_path, src_share, dest_share, *bins = sys.argv[1:] -wanted = set(bins) -try: - with open(index_path) as f: - idx = json.load(f) - packages = idx.get("packages", {}) -except (OSError, json.JSONDecodeError): - packages = {} - -# Package names we ship: lockfile bin names plus index packages that -# publish at least one of those bins. -pkg_names = set(wanted) -for name, pkg in packages.items(): - pbins = [] - for b in pkg.get("binaries") or []: - n = b["name"] if isinstance(b, dict) else b - pbins.append(str(n).removesuffix("-x86_64")) - if name in wanted or any(b in wanted for b in pbins): - pkg_names.add(name) - -os.makedirs(os.path.join(dest_share, "applications"), exist_ok=True) -os.makedirs(os.path.join(dest_share, "licenses"), exist_ok=True) - -for name in sorted(pkg_names): - pkg = packages.get(name) or {} - if pkg.get("data_archive"): - src = os.path.join(src_share, name) - dest = os.path.join(dest_share, name) - if name == "breadhelp": - continue # already copied above, required - if os.path.isdir(src): - if os.path.exists(dest): - shutil.rmtree(dest) - shutil.copytree(src, dest, symlinks=True) - print(f" baked data dir {dest}") - else: - sys.exit(f"ERROR: bakery data_archive for {name} missing at {src}") - - desktop_src = os.path.join(src_share, "applications", f"{name}.desktop") - desktop_dest = os.path.join(dest_share, "applications", f"{name}.desktop") - if os.path.isfile(desktop_src) and not os.path.isfile(desktop_dest): - shutil.copy2(desktop_src, desktop_dest) - print(f" baked desktop {desktop_dest}") - - lic_src = os.path.join(src_share, "licenses", name) - lic_dest = os.path.join(dest_share, "licenses", name) - if os.path.isdir(lic_src) and not os.path.isdir(lic_dest): - shutil.copytree(lic_src, lic_dest, symlinks=True) - print(f" baked license {lic_dest}") -PY +echo "baked: $(ls "$SKEL/.local/bin")" # --- Bake systemd user services for bakery-managed bread packages ----------- # Historically only breadd.service was hand-committed to skel; every other # bakery package's service (breadbox-sync, breadmill, breadclipd, ...) was # silently left out, so those daemons never start on a fresh install/live # boot until the user re-runs `bakery install` (which needs network). -# 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 ===" +# Generalize from the same source of truth as the binary bake above: read +# the services this laptop's bakery actually installed, copy each unit file +# into skel with ExecStart rewritten from this laptop's literal home path to +# the portable `%h` specifier, and recreate whichever *.target.wants enable +# symlink bakery created locally. Units already committed by hand (breadd.service +# carries a RuntimeDirectoryPreserve=yes fix not yet upstreamed — see bread-release-build +# notes) are left alone rather than overwritten. +echo "=== baking bakery service units into skel ===" SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user" SKEL_SYSTEMD="$SKEL/.config/systemd/user" -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) -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 -} +mapfile -t SERVICE_UNITS < <(python3 -c " +import json +with open('$BAKERY_STATE/installed.json') as f: + d = json.load(f) +for pkg in d.get('packages', d).values(): + for s in pkg.get('services', []): + print(s) +") 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 " warning: $unit not found at $src, skipping" + continue 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 deleted file mode 100644 index cf6ebbe..0000000 --- a/docs/hardware.md +++ /dev/null @@ -1,46 +0,0 @@ -# Hardware and recovery - -## GPUs - -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. - -## 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=@`). - -`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 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. diff --git a/docs/signed-repo.md b/docs/signed-repo.md deleted file mode 100644 index 224476e..0000000 --- a/docs/signed-repo.md +++ /dev/null @@ -1,153 +0,0 @@ -# Signed `[breadway]` repo - -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`** and, once published, the -`dl.breadway.dev/arch` database. It is **not** imported as a pacman repo key -on the ISO yet. 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. - -The signed repo belongs at `https://dl.breadway.dev/arch`, not on Forgejo's -registry. Forgejo publishing stays as it is (`package.yml` / packaging -workflows PUT unsigned `.pkg.tar.zst` so existing Never installs keep -working). - -## 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) - (`5620 3B86 A110 695A E7F3 1093 4AF3 323D 678E B5E2`, - `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`): - -``` -https://dl.breadway.dev/arch/x86_64/ - breadlock--1-x86_64.pkg.tar.zst - breadlock--1-x86_64.pkg.tar.zst.sig - breadway.db - breadway.db.sig - breadway.files - 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). 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`: - -```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 -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`): - -```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"}' -``` - -It also runs after the in-repo AUR republish workflows complete -(`calamares` / `bibata` / `powerlevel10k` / `yay-bin`). 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. - -## After the signed repo exists - -Only after `https://dl.breadway.dev/arch/x86_64/breadway.db.sig` HEADs 200 -and the verify commands above succeed: - -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`. This tree does -**not** change either file. diff --git a/dotfiles/README.md b/dotfiles/README.md deleted file mode 100644 index 8e61cc6..0000000 --- a/dotfiles/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# `dotfiles/` is not the live skel - -These files are a leftover from an earlier design (Hyprland `.conf` binds). -They are **not** copied into the ISO or the installed system. - -`dotfiles/hypr/keybinds.conf` still mentions `grimblast`. That is not the -screenshot tool BOS ships — live binds use **breadshot** -(`iso/airootfs/etc/skel/.config/hypr/binds.json`). Do not treat grimblast -as current. - -Live user defaults live in [`iso/airootfs/etc/skel`](../iso/airootfs/etc/skel) -(`hyprland.lua` + `binds.json`, breadlock/`loginctl lock-session`, breadshot, -breadpad, …). Edit that tree. diff --git a/dotfiles/hypr/keybinds.conf b/dotfiles/hypr/keybinds.conf index a857962..7cf8cdd 100644 --- a/dotfiles/hypr/keybinds.conf +++ b/dotfiles/hypr/keybinds.conf @@ -1,7 +1,3 @@ -# STALE — not the live Hyprland binds. Not copied into the ISO. -# Screenshots are breadshot (see iso/airootfs/etc/skel/.config/hypr/binds.json), -# not grimblast. Do not copy from this file. - $mod = SUPER # App launchers diff --git a/iso/airootfs/etc/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 b8d4736..7899e67 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,40 +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 -# and reaps newly-orphaned KF6/Qt6 deps. Each name is independent so one -# missing package cannot abort the rest. Offline-safe: never touches the -# network. qt6-declarative is NOT reaped — qt6-wayland still needs it. -LIVE_ONLY_PKGS=( - calamares - squashfs-tools - mkinitcpio-archiso - mkinitcpio-nfs-utils - memtest86+ - memtest86+-efi - edk2-shell - syslinux -) -for pkg in "${LIVE_ONLY_PKGS[@]}"; do - pacman -Qq "$pkg" &>/dev/null || continue - pacman -Rs --noconfirm "$pkg" &>/dev/null \ - || echo "WARN: could not remove live-only package $pkg" -done -while orphans="$(pacman -Qtdq 2>/dev/null)" && [[ -n "$orphans" ]]; do - # shellcheck disable=SC2086 - pacman -Rs --noconfirm $orphans &>/dev/null || break -done - # Root used a passwordless entry on the live medium; lock it (sudo model). passwd -l root || true @@ -73,11 +41,8 @@ passwd -l root || true # over to the target (unpackfs may skip it / perms differ), leaving the installed # system unable to verify package signatures — the first `pacman -Syu` then dies # with "keyring is not writable / required key missing". Initialise it here so a -# fresh install can update out of the box. archlinux-keyring is already present -# and is the only keyring populated — it verifies official Arch packages. -# [breadway] stays SigLevel=Never (Forgejo does not serve pacman-compatible -# db signatures). Do not import KEYS.asc here: that key signs ISO SHA256SUMS, -# not the pacman repo; treating it as a repo key would be a lie. +# fresh install can update out of the box. archlinux-keyring is already present; +# [breadway] is SigLevel=Never so it needs no key. # --------------------------------------------------------------------------- if command -v pacman-key &>/dev/null; then pacman-key --init || echo "WARN: pacman-key --init failed" @@ -376,39 +341,15 @@ fi # greetd — graphical login (shipped disabled; live uses tty autologin) # grub-btrfsd — regenerates GRUB snapshot entries (the unit is grub-btrfsd.service, # NOT grub-btrfs.path, which no longer exists) -# avahi-daemon.service → avahi-daemon.socket: the package ships both; socket -# activation still answers nss-mdns and CUPS discovery but stays off the idle -# RSS until something asks. The host stops announcing itself over mDNS until -# the socket is first touched. # --------------------------------------------------------------------------- for unit in NetworkManager.service bluetooth.service systemd-timesyncd.service \ tlp.service greetd.service snapper-cleanup.timer grub-btrfsd.service \ - fstrim.timer cups.socket avahi-daemon.socket ufw.service \ + fstrim.timer cups.socket avahi-daemon.service ufw.service \ fwupd-refresh.timer reflector.timer; do systemctl enable "$unit" || echo "WARN: failed to enable $unit" done systemctl set-default graphical.target || echo "WARN: set-default graphical failed" -# Arch's 90-systemd.preset enables systemd-homed / userdbd / nsresourced. -# BOS creates classic /etc/passwd accounts and never calls homectl. Mask -# (not disable) so preset-all or a systemd upgrade cannot re-enable them. -for unit in systemd-homed.service systemd-homed-activate.service \ - systemd-userdbd.service systemd-userdbd.socket \ - systemd-nsresourced.service systemd-nsresourced.socket; do - systemctl mask "$unit" || echo "WARN: failed to mask $unit" -done - -# journald defaults SystemMaxUse to 10% of the filesystem holding /var/log. -# /var/log is the @log subvolume of the root pool, so that ceiling is tens -# of GB. Cap it; less history for postmortems. -install -d -m 0755 /etc/systemd/journald.conf.d -cat >/etc/systemd/journald.conf.d/90-bos-journal.conf <<'JOURNALEOF' -[Journal] -SystemMaxUse=256M -SystemMaxFileSize=32M -RuntimeMaxUse=32M -JOURNALEOF - # --------------------------------------------------------------------------- # mDNS resolution (nss-mdns): insert mdns_minimal into the hosts: line so the # resolver answers *.local (network printers, other hosts) via avahi. Idempotent. @@ -430,21 +371,12 @@ if command -v ufw &>/dev/null; then ufw --force enable || echo "WARN: ufw enable failed" 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 -# 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 +# The bread ecosystem (bakery + bread, breadbar, breadbox, breadcrumbs, breadpad) +# is bakery-managed, not pacman: the binaries and bakery manifest live in +# /etc/skel/.local (baked in at ISO build time) and are copied into the user's +# home below, so the install works fully offline with no DNS for bakery/GitHub. +# bos-settings and breadhelp are the only pacman bread packages and were +# installed by unpackfs. # --------------------------------------------------------------------------- # 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/os-release b/iso/airootfs/etc/os-release index f6b380c..4f28e07 100644 --- a/iso/airootfs/etc/os-release +++ b/iso/airootfs/etc/os-release @@ -5,7 +5,7 @@ ID_LIKE=arch BUILD_ID=rolling ANSI_COLOR="38;2;23;147;209" HOME_URL="https://breadway.dev" -DOCUMENTATION_URL="https://git.breadway.dev/Breadway/bos" -SUPPORT_URL="https://git.breadway.dev/Breadway/bos/issues" +DOCUMENTATION_URL="https://wiki.archlinux.org/" +SUPPORT_URL="https://bbs.archlinux.org/" BUG_REPORT_URL="https://git.breadway.dev/Breadway/bos/issues" -PRIVACY_POLICY_URL="https://breadway.dev" +PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/" diff --git a/iso/airootfs/etc/pacman.conf b/iso/airootfs/etc/pacman.conf index 4e4435e..20c5242 100644 --- a/iso/airootfs/etc/pacman.conf +++ b/iso/airootfs/etc/pacman.conf @@ -26,21 +26,17 @@ Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist # ----------------------------------------------------------------------- -# Breadway custom repo — breadlock plus AUR republishes the ISO needs -# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin, -# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp -# are NOT here; they are bakery-baked into /usr/local at ISO build time. +# Breadway custom repo — provides: bakery and the bread ecosystem packages +# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings). +# (calamares comes from the official extra repo, not here.) # # Packages are published to the Forgejo Arch registry (group "os") by the -# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). +# .forgejo/workflows/package.yml workflow in each repo, on tag push. # -# Forgejo's Arch package registry does not serve pacman-compatible db -# signatures. SigLevel = Never is TLS-only integrity: the connection is -# HTTPS (or rewritten to hestia's localhost:3002 in CI). breadlock (PAM) -# rides this repo. Do NOT flip to SigLevel = Required unless a signed db -# has been verified to work — Required without signatures breaks the ISO -# and every install that uses [breadway]. KEYS.asc is the ISO SHA256SUMS -# signing key, not a pacman repo key. +# Forgejo signs the repo db with a key pacman can't look up, so TrustAll +# fails. SigLevel = Never skips verification (acceptable for this private +# repo over TLS). Future improvement: import Forgejo's signing key and +# switch to SigLevel = Required for full package verification. # ----------------------------------------------------------------------- # The section name must match Forgejo's served db filename # ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. diff --git a/iso/airootfs/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/hypr/autostart.json b/iso/airootfs/etc/skel/.config/hypr/autostart.json index be2b4ac..e18222b 100644 --- a/iso/airootfs/etc/skel/.config/hypr/autostart.json +++ b/iso/airootfs/etc/skel/.config/hypr/autostart.json @@ -3,14 +3,6 @@ { "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 }, - { "command": "bash -c 'command -v breadpad >/dev/null && exec breadpad listen'", "label": "Capture command bus (breadpad listen)", "enabled": true } + { "command": "breadhelp --autostart", "label": "BOS Help (first-run onboarding)", "enabled": true } ] } diff --git a/iso/airootfs/etc/skel/.config/hypr/binds.json b/iso/airootfs/etc/skel/.config/hypr/binds.json index 67f19f4..4b959ce 100644 --- a/iso/airootfs/etc/skel/.config/hypr/binds.json +++ b/iso/airootfs/etc/skel/.config/hypr/binds.json @@ -18,7 +18,6 @@ { "action": "exec", "command": "breadclip", "key": "V", "label": "Clipboard history (breadclip)", "category": "apps", "demo_cmd": "breadclip" }, { "action": "exec", "command": "breadclip", "key": "V", "mods": ["SUPER", "SHIFT"], "label": "Clipboard history (breadclip) — same as SUPER + V", "category": "apps" }, - { "action": "exec", "command": "breadbar --history", "key": "N", "mods": ["SUPER", "SHIFT"], "label": "Notification history (breadbar)", "category": "apps", "demo_cmd": "breadbar --history" }, { "action": "layout", "layout": "togglesplit", "key": "T", "label": "Toggle split direction", "category": "windows" }, { "action": "focus_last", "key": "Tab", "label": "Focus last window", "category": "windows" }, diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua index f03e5b5..233da16 100644 --- a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua @@ -77,17 +77,6 @@ hl.env("SDL_VIDEODRIVER", "wayland") hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") hl.env("_JAVA_AWT_WM_NONREPARENTING", "1") --- 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. @@ -105,22 +94,14 @@ pcall(function() bindings = binds.bindings, }) end) --- 3-finger horizontal trackpad swipe → workspace switch (1:1 gesture) -hl.gesture({ - fingers = 3, - direction = "horizontal", - action = "workspace", -}) -- --------------------------------------------------------------------------- -- Autostart. Core bootstrap sequence (polkit agent, dark theme, wallpaper -- daemon, breadd's Wayland-env fix, breadclipd) stays hardcoded here — it's -- timing/order-sensitive infrastructure, not something a settings UI should -- expose for a user to disable or reorder. The extra, genuinely toggleable --- apps (breadbar, hypridle, bos-netcheck, breadhelp, breadpaper/breadshot --- listen) come from autostart.json via scripts/system/autostart.lua, --- appended after. listen is wrapped with `command -v` so a missing --- binary does not brick login (Hyprland exec is already fire-and-forget). +-- apps (breadbar, hypridle, bos-netcheck, breadhelp) come from +-- autostart.json via scripts/system/autostart.lua, appended after. -- (bos-live-setup appends the live-installer launch below this on the ISO.) -- --------------------------------------------------------------------------- hl.on("hyprland.start", function() @@ -135,12 +116,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 @@ -154,9 +132,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", @@ -177,8 +154,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) @@ -186,20 +162,7 @@ hl.on("hyprland.start", function() -- autostart.json/its loader broke — fall back to the same apps BOS -- has always started, so a bad JSON edit degrades to "normal -- desktop" rather than "no bar, no idle lock, no onboarding". - extra = { - "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'", - "bash -c 'command -v breadpad >/dev/null && exec breadpad listen'", - } + extra = { "breadbar", "hypridle", "bos-netcheck", "breadhelp --autostart" } end for _, cmd in ipairs(extra) do hl.dispatch(hl.dsp.exec_cmd(cmd)) 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..7fb119d 100644 --- a/iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua +++ b/iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua @@ -12,21 +12,11 @@ -- because the valid result happens to be empty. local json = dofile(os.getenv("HOME") .. "/.config/hypr/scripts/lib/json.lua") --- breadpaper/breadshot `listen` is wrapped so a missing binary (stable --- does not ship the command-bus verb yet) cannot take down the session. 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 }, - { command = "bash -c 'command -v breadpad >/dev/null && exec breadpad listen'", enabled = true }, } return function() diff --git a/iso/airootfs/etc/skel/.config/hypr/scripts/ui/settings.lua b/iso/airootfs/etc/skel/.config/hypr/scripts/ui/settings.lua index 8d3a829..f9cd963 100644 --- a/iso/airootfs/etc/skel/.config/hypr/scripts/ui/settings.lua +++ b/iso/airootfs/etc/skel/.config/hypr/scripts/ui/settings.lua @@ -112,6 +112,19 @@ local function build_hl_config(v) dwindle = { preserve_split = true }, animations = { enabled = true }, misc = { disable_hyprland_logo = true, disable_splash_rendering = true }, + -- 3-finger touchpad swipe switches workspaces (touchscreen/touchpad + -- gesture, native to Hyprland — no plugin needed). + gestures = { + workspace_swipe = true, + workspace_swipe_fingers = 3, + workspace_swipe_distance = 300, + workspace_swipe_invert = true, + workspace_swipe_min_speed_to_force = 30, + workspace_swipe_cancel_ratio = 0.5, + workspace_swipe_create_new = true, + workspace_swipe_direction_lock = true, + workspace_swipe_forever = false, + }, } end 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/.local/share/applications/bos-settings.desktop b/iso/airootfs/etc/skel/.local/share/applications/bos-settings.desktop deleted file mode 100644 index 5385adb..0000000 --- a/iso/airootfs/etc/skel/.local/share/applications/bos-settings.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=BOS Settings -Comment=System settings for Bread OS -Exec=bos-settings -Icon=preferences-system -Terminal=false -Type=Application -Categories=Settings;System; -StartupWMClass=bos-settings diff --git a/iso/airootfs/etc/skel/.local/share/applications/breadhelp.desktop b/iso/airootfs/etc/skel/.local/share/applications/breadhelp.desktop deleted file mode 100644 index c41be36..0000000 --- a/iso/airootfs/etc/skel/.local/share/applications/breadhelp.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Name=BOS Help -Comment=Your personal guide to the Bread desktop -Exec=breadhelp -Icon=help-browser -Terminal=false -Type=Application -Categories=Help;System; -StartupWMClass=com.breadway.breadhelp 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/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..53ac3c1 100644 --- a/iso/airootfs/usr/local/bin/bos-update +++ b/iso/airootfs/usr/local/bin/bos-update @@ -2,41 +2,19 @@ # bos-update — update all of BOS in one go. # # BOS packages come from two channels, so a full update touches both: -# 1. pacman — Arch base/desktop + the [breadway] repo (breadlock + AUR -# republishes: calamares, zen-browser-bin, bibata, yay-bin, -# powerlevel10k). [breadway] does NOT provide bos-settings -# or other bakery desktop apps. Every transaction is -# snapshotted by snap-pac; recover via the GRUB "snapshots" -# submenu (grub-btrfs), not `snapper rollback`. -# 2. bakery — the bread ecosystem apps in /usr/local (whatever `bakery list` -# reports as installed — bakery, bread, breadbar, breadbox, -# breadcrumbs, breadpad, breadman, bread-theme, breadpaper, -# breadmon, breadsearch, breadclip, breadshot, bos-settings, -# breadhelp, ...). Those bits live on @ and ride snapper -# root snapshots; recover via grub-btrfs, not `snapper rollback`. +# 1. pacman — Arch base/desktop + the [breadway] repo (bos-settings, etc.). +# Every transaction is snapshotted by snap-pac, so you can roll +# back from the GRUB "snapshots" submenu or BOS Settings. +# 2. bakery — the bread ecosystem apps in ~/.local/bin (whatever `bakery list` +# reports as installed — bread, breadbar, breadbox, breadcrumbs, +# breadpad, breadman, bread-theme, breadpaper, breadmon, +# breadsearch, breadclip, breadshot, ...). # # Best-effort: a failure in one channel doesn't abort the other. set -uo pipefail 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. -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 \ - -d "bos-update (pre bakery)" \ - || echo "WARN: snapper home pre snapshot failed" - fi - snapper -c root create -t pre -c number \ - -d "bos-update (pre bakery)" \ - || echo "WARN: snapper pre snapshot failed" -fi - bold "==> System packages (pacman -Syu)" if command -v pacman >/dev/null; then sudo pacman -Syu || echo "WARN: pacman update failed" @@ -47,22 +25,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 deleted file mode 100644 index 8e21593..0000000 --- a/iso/bread-lockfile.toml +++ /dev/null @@ -1,63 +0,0 @@ -# Bakery binaries baked into the live/installed image at /usr/local. -# -# build-local.sh and CI (scripts/ci-stage-bakery.py) read this file. A missing -# *required* binary fails the bake: a hollow ISO is worse than a failed build. -# 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. -# -# A flat `bins` list is still accepted and treated as required_bins. -# -# CI populates the builder from the minisign-verified stable bakery index -# (https://dl.breadway.dev/index.json). Optional [versions] (or [[pin]] -# tables with package + version) pin bakery package versions so two ISO -# bakes of the same git commit fetch the same bits: -# https://dl.breadway.dev///... -# Bump pins after new bakery stables land. Local builds still snapshot -# whatever is installed on the builder. -# -# Not shipped (even if they appear in the index): breadcast, breadarr. -# breadlock is pacman (see packages.x86_64), not bakery. - -required_bins = [ - "bakery", - "bread", - "breadd", - "bread-emit", - "bread-module-host", - "breadman", - "breadbar", - "breadbox", - "breadbox-sync", - "breadcrumbs", - "breadpad", - "breadpaper", - "bread-theme", - "breadmon", - "breadsearch", - "breadmill", - "breadclip", - "breadclipd", - "breadshot", - "bos-settings", - "breadhelp", -] - -# Package name → version. Must exist at dl.breadway.dev/// and -# should match the signed index so CI can verify sha256. -# [[pin]] { package, version } is accepted as well and merged (conflict = bake error). -[versions] -bakery = "0.7.4" -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" diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index ff90edf..38b4488 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -1,26 +1,9 @@ # Base system base +base-devel linux -# linux-firmware metapackage pulls every mandatory vendor blob (incl. nvidia). -# List the subpackages we actually need so nvidia (~103 MiB, nouveau-only — -# BOS ships no NVIDIA driver) can stay off the image. Turing+ nouveau needs -# the GSP blobs; reinstall linux-firmware-nvidia when lspci sees NVIDIA. -# linux-firmware -linux-firmware-amdgpu -linux-firmware-atheros -linux-firmware-broadcom -linux-firmware-cirrus -linux-firmware-intel -linux-firmware-mediatek -linux-firmware-realtek -linux-firmware-radeon -linux-firmware-other -# linux-firmware-nvidia -# base-devel + linux-headers are for AUR/DKMS builds. yay needs base-devel, -# but those builds need network anyway — pacman -S base-devel at that point. -# linux-headers is DKMS-only and BOS ships no DKMS packages. -# base-devel -# linux-headers +linux-firmware +linux-headers # CPU microcode — applied early by GRUB on the installed system (picked up by # the bootloader module). amd-ucode for the dev laptop's Ryzen; intel-ucode for # Intel targets. bos-copy-kernel also stages these into the live target /boot. @@ -61,7 +44,7 @@ sbctl squashfs-tools # rsync: unpackfs copies the unpacked rootfs onto the target with rsync. rsync -# Live-ISO boot (archiso bootmodes: bios.syslinux + uefi.grub) +# Live-ISO boot (archiso bootmodes: bios.syslinux + uefi.systemd-boot) # mkinitcpio-archiso provides the initramfs hooks that find and mount # airootfs.sfs and switch root into it — without it the live ISO drops # to emergency mode on boot. @@ -78,8 +61,6 @@ snapper snap-pac grub-btrfs inotify-tools -# Home backup (Settings → Backup). Snapper is root (`@`) only; restic covers $HOME. -restic # Wayland / Hyprland hyprland @@ -118,17 +99,11 @@ bluez-utils # blueman: GUI Bluetooth manager (pair/connect devices; breadbar shows status only). blueman -# GTK4 runtime (breadbar, breadbox, breadclip, breadhelp, and other bakery apps) +# GTK4 runtime gtk4 gtk4-layer-shell librsvg libpulse -hicolor-icon-theme -# Tauri 2 runtime for bakery-baked bos-settings. Arch's WebKitGTK 4.1 package -# is webkit2gtk-4.1 (libwebkit2gtk-4.1.so); libsoup3 and JavaScriptCore 4.1 -# are pulled in as its dependencies. xdg-desktop-portal comes from the -# Hyprland/GTK portal packages listed above. -webkit2gtk-4.1 # GTK3 dark theme (Adwaita-dark); without this package the gtk-theme-name in # skel settings.ini silently falls back to the light theme for GTK3 apps. gnome-themes-extra @@ -150,9 +125,7 @@ wayland-protocols # Fonts noto-fonts -# noto-fonts-cjk is ~299 MiB installed / ~196 MiB on the ISO and only useful -# to CJK-locale users. Install on first run for zh/ja/ko. -# noto-fonts-cjk +noto-fonts-cjk noto-fonts-emoji ttf-jetbrains-mono # Nerd font variant — icons in terminal tools (eza --icons, fastfetch, yazi) @@ -178,12 +151,13 @@ file-roller # GUI applications a general desktop is expected to have out of the box. # gnome-text-editor: graphical editor (terminal editors aside); gnome-calculator: -# calculator; loupe: Wayland-native image viewer (default for image files). -# PDF is handled by Zen (skel mimeapps.list maps application/pdf to zen.desktop); -# zathura+zathura-pdf-mupdf would pull libmupdf (~56 MiB) as a never-default viewer. +# calculator; loupe: Wayland-native image viewer (default for image files); +# zathura(+pdf-mupdf): lightweight Wayland PDF viewer (BOS had no PDF reader). gnome-text-editor gnome-calculator loupe +zathura +zathura-pdf-mupdf # Media player — BOS ships gstreamer codecs but otherwise has no player app. vlc # Web browser (served from the [Breadway] repo; AUR zen-browser-bin republished @@ -204,16 +178,19 @@ yay-bin # Bread ecosystem. # -# breadlock is the only bread* pacman package here (it needs a root-owned -# /etc/pam.d/breadlock). Everything else — bakery, bread/breadd/bread-emit/ -# bread-module-host, breadbar, breadbox, breadcrumbs, breadpad, breadpaper, -# bread-theme, breadmon, breadsearch, breadclip, breadshot, bos-settings, -# breadhelp — is bakery-managed and baked into /usr/local at ISO build -# time from iso/bread-lockfile.toml (see build-local.sh). breadcast and -# breadarr are not shipped. bos-settings/breadhelp desktop entries are -# also committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime -# deps stay listed even though no bread package depends on them via pacman -# (gtk4, gtk4-layer-shell, webkit2gtk-4.1, iw, libpulse, librsvg, …). +# The bread apps themselves (bakery, bread, breadbar, breadbox, breadcrumbs, +# breadpad) are NOT pacman packages here — they are bakery-managed binaries +# baked into /etc/skel/.local/bin at build time (see build-local.sh), so every +# user gets the exact versions from this laptop's bakery install with no +# network/DNS needed at install or runtime. Their runtime system deps are pulled +# in elsewhere in this list (gtk4, gtk4-layer-shell, iw, libpulse, librsvg, +# networkmanager, openssl, zlib, systemd-libs) — keep those even though no bread +# package depends on them. +# +# bos-settings and breadhelp are BOS-specific pacman packages (not part of the +# bakery index), so they stay here, served from the [breadway] repo. +bos-settings +breadhelp # Input / screen utilities brightnessctl @@ -346,3 +323,6 @@ qt6ct # hyprland.lua) needs these or Qt apps fall back to (blurry) XWayland. qt5-wayland qt6-wayland + +# Dev tools (for bos-settings standalone install) +rustup diff --git a/iso/pacman.conf b/iso/pacman.conf index be3d52f..20c5242 100644 --- a/iso/pacman.conf +++ b/iso/pacman.conf @@ -9,23 +9,6 @@ Architecture = auto CheckSpace ParallelDownloads = 5 -# Optional NoExtract size levers — left disabled. This file is both the ISO -# build config AND the installed system's pacman.conf, so enabling any line -# also stops future pacman -Syu from restoring those files. -# Measured against the 844-package closure (xz squashfs, profiledef.sh opts): -# usr/share/locale (non-en) 405.0 MiB raw -> 92.28 MiB ISO -# usr/share/doc 130.5 MiB raw -> 26.54 MiB ISO -# usr/share/man 41.5 MiB raw -> 38.77 MiB ISO -# usr/share/info 12.9 MiB raw -> 11.12 MiB ISO -# usr/share/gtk-doc 16.0 MiB raw -> 1.18 MiB ISO -# usr/include 193.6 MiB raw -> 25.26 MiB ISO -# Non-en locales make every GUI English-only until the package is reinstalled -# without this NoExtract; dropping man/info means `man` returns nothing. -#NoExtract = usr/share/locale/* !usr/share/locale/en* !usr/share/locale/locale.alias -#NoExtract = usr/share/doc/* usr/share/gtk-doc/* usr/share/info/* -#NoExtract = usr/share/man/* -#NoExtract = usr/include/* - Color VerbosePkgLists ILoveCandy @@ -43,21 +26,17 @@ Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist # ----------------------------------------------------------------------- -# Breadway custom repo — breadlock plus AUR republishes the ISO needs -# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin, -# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp -# are NOT here; they are bakery-baked into /usr/local at ISO build time. +# Breadway custom repo — provides: bakery and the bread ecosystem packages +# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings). +# (calamares comes from the official extra repo, not here.) # # Packages are published to the Forgejo Arch registry (group "os") by the -# .forgejo/workflows/*.yml workflows in this repo (and breadlock's). +# .forgejo/workflows/package.yml workflow in each repo, on tag push. # -# Forgejo's Arch package registry does not serve pacman-compatible db -# signatures. SigLevel = Never is TLS-only integrity: the connection is -# HTTPS (or rewritten to hestia's localhost:3002 in CI). breadlock (PAM) -# rides this repo. Do NOT flip to SigLevel = Required unless a signed db -# has been verified to work — Required without signatures breaks the ISO -# and every install that uses [breadway]. KEYS.asc is the ISO SHA256SUMS -# signing key, not a pacman repo key. +# Forgejo signs the repo db with a key pacman can't look up, so TrustAll +# fails. SigLevel = Never skips verification (acceptable for this private +# repo over TLS). Future improvement: import Forgejo's signing key and +# switch to SigLevel = Required for full package verification. # ----------------------------------------------------------------------- # The section name must match Forgejo's served db filename # ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. diff --git a/iso/profiledef.sh b/iso/profiledef.sh index 462f74e..aa92318 100644 --- a/iso/profiledef.sh +++ b/iso/profiledef.sh @@ -8,12 +8,7 @@ iso_application="Bread Operating System" iso_version="$(date +%Y.%m.%d)" install_dir="arch" buildmodes=('iso') -# systemd-boot can only read files from the ESP it was launched from, so -# mkarchiso's _make_bootmode_uefi.systemd-boot copies vmlinuz + initramfs -# INTO the FAT efiboot.img on top of the copy already on ISO9660 (~244 MiB -# duplicate). uefi.grub's ESP is only EFI + shell*.efi — GRUB reads ISO9660 -# directly. iso/grub/{grub,loopback}.cfg are already BOS-branded. -bootmodes=('bios.syslinux' 'uefi.grub') +bootmodes=('bios.syslinux' 'uefi.systemd-boot') arch="x86_64" pacman_conf="pacman.conf" airootfs_image_type="squashfs" @@ -29,8 +24,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 ce80f7e..ccdd544 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -1,19 +1,39 @@ 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`) — 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//**`. +`breadhelp/PKGBUILD` builds and installs `breadhelp` from source — BOS's +onboarding + help center, and the only first-party pacman package still built +from this repo. -Every bread-ecosystem app (bakery, bread, breadbar, breadbox, breadcrumbs, -breadpad, breadpaper, breadmon, breadsearch, breadclip, breadshot, -bos-settings, breadhelp, ...) is bakery-managed, not pacman-packaged — see -`iso/bread-lockfile.toml` (`required_bins` + `optional_bins`), which -`build-local.sh` uses as the name list when baking this machine's bakery -install into the ISO's `/etc/skel`. -`breadlock` is the sole deliberate exception (it needs a root-owned -`/etc/pam.d/breadlock` PAM service file, which bakery — by design — has no -privileged-install path for) and stays on pacman only; see -`bread-ecosystem/docs/release-channels.md` for the full policy. +`bos-settings` is also pacman-packaged and served from the same [breadway] +repo, but its source lives in its own repo now (`~/Projects/bos-settings`, +`git.breadway.dev/Breadway/bos-settings`) so a bos-settings release doesn't require +a BOS ISO release. + +Everything else the bread ecosystem ships (breadbar, breadbox, breadpad, ...) +is bakery-managed, not pacman-packaged — see `build-local.sh`. + +## Local build + +```bash +cd breadhelp && makepkg -si +``` + +## Before publishing to [breadway] repo + +CI (`.forgejo/workflows/package.yml`) handles this on tag push (`vX.Y.Z`): it +bumps `pkgver`, archives the repo into the expected tarball name, and runs +`makepkg`. To do it by hand instead: + +1. Tag a release on GitHub (`vX.Y.Z`). +2. Update `pkgver` in `breadhelp/PKGBUILD` to match the tag. +3. Update `source` to the release tarball URL. +4. Run `updpkgsums` (or manually set `sha256sums`). + +## Runtime dependencies + +| Package | Required | Notes | +|---------|----------|-------| +| `gtk4` | yes | UI toolkit | +| `glib2` | yes | always | +| `snapper` | optional | create-backup one-click fix | diff --git a/packaging/bibata/PKGBUILD b/packaging/bibata/PKGBUILD index b713436..b49e382 100644 --- a/packaging/bibata/PKGBUILD +++ b/packaging/bibata/PKGBUILD @@ -16,13 +16,7 @@ options=('!strip') source=("${pkgname%-bin}-$pkgver.tar.xz::$url/releases/download/v$pkgver/Bibata.tar.xz") sha256sums=('172e33c4ae415278384dcecc7d1a9b7a024266bc944bc751fd86532be1cc6251') -# Upstream tarball has all 12 variants (~322 MiB). BOS only ever selects -# Bibata-Modern-Ice (hyprland.lua XCURSOR_THEME, gsettings, gtk settings.ini). -# Ship that plus its -Right sibling. -_variants=(Bibata-Modern-Ice Bibata-Modern-Ice-Right) package() { install -d "$pkgdir/usr/share/icons" - for v in "${_variants[@]}"; do - cp -r "$v" "$pkgdir/usr/share/icons/" - done + cp -r Bibata* "$pkgdir/usr/share/icons" } diff --git a/scripts/ci-publish-signed-repo.sh b/scripts/ci-publish-signed-repo.sh deleted file mode 100755 index f063ce2..0000000 --- a/scripts/ci-publish-signed-repo.sh +++ /dev/null @@ -1,307 +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 -) - -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-stage-bakery.py b/scripts/ci-stage-bakery.py deleted file mode 100755 index 20dbfb1..0000000 --- a/scripts/ci-stage-bakery.py +++ /dev/null @@ -1,497 +0,0 @@ -#!/usr/bin/env python3 -"""Stage bakery artifacts from the verified stable index into $LAPTOP_HOME. - -Used by .forgejo/workflows/release-iso.yml so the ISO bake does not invent -binaries, fake installed.json, or cargo-build bread-theme. Never downloads -breadcast or breadarr. -""" -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import shutil -import subprocess -import sys -import tarfile -import tempfile -import tomllib -import urllib.error -import urllib.request -from datetime import datetime, timezone -from pathlib import Path -from urllib.parse import urljoin, urlparse - -INDEX_URL = "https://dl.breadway.dev/index.json" -DL_ORIGIN = "https://dl.breadway.dev" -# Same key as bread-ecosystem/scripts/get.sh and bakery/src/manifest.rs. -MINISIGN_PUBKEY = "RWTBR8w/IJ+jaylOv80b52DzekKbSR2CvOVGvzB0ipGBaMhJPAOiEWq8" -BLOCKED = frozenset({"breadcast", "breadarr"}) -ARCH_SUFFIXES = ("-x86_64", "-aarch64", "-arm64", "-armv7") - - -def die(msg: str) -> None: - print(f"ERROR: {msg}", file=sys.stderr) - raise SystemExit(1) - - -def dest_name(name: str) -> str: - for suf in ARCH_SUFFIXES: - if name.endswith(suf): - return name[: -len(suf)] - return name - - -def valid_name(name: str) -> bool: - return bool(name) and "/" not in name and name not in (".", "..") - - -def load_versions(data: dict, path: Path) -> dict[str, str]: - """Optional [versions] map and/or [[pin]] tables → package → version.""" - versions: dict[str, str] = {} - - raw_map = data.get("versions") - if raw_map is not None: - if not isinstance(raw_map, dict): - die(f"{path}: [versions] must be a table of package = \"version\"") - for pkg, ver in raw_map.items(): - if not isinstance(pkg, str) or not valid_name(pkg): - die(f"{path}: invalid [versions] package {pkg!r}") - if not isinstance(ver, str) or not valid_name(ver): - die(f"{path}: invalid [versions] version for {pkg}: {ver!r}") - versions[pkg] = ver - - pins = data.get("pin") - if pins is not None: - if not isinstance(pins, list): - die(f"{path}: [[pin]] must be an array of tables") - for i, entry in enumerate(pins): - if not isinstance(entry, dict): - die(f"{path}: [[pin]] #{i} must be a table") - pkg = entry.get("package", entry.get("pkg")) - ver = entry.get("version") - if not isinstance(pkg, str) or not valid_name(pkg): - die(f"{path}: [[pin]] #{i}: missing valid package") - if not isinstance(ver, str) or not valid_name(ver): - die(f"{path}: [[pin]] #{i}: missing valid version") - if pkg in versions and versions[pkg] != ver: - die(f"{path}: conflicting pin for {pkg}: {versions[pkg]} vs {ver}") - versions[pkg] = ver - return versions - - -def load_lockfile(path: Path) -> tuple[list[str], list[str], dict[str, str]]: - with path.open("rb") as f: - data = tomllib.load(f) - required = data.get("required_bins") - optional = data.get("optional_bins") or [] - if required is None: - required = data.get("bins") or data.get("binaries") - if not isinstance(required, list) or not required: - die(f"{path}: missing non-empty required_bins (or bins) list") - if not isinstance(optional, list): - die(f"{path}: optional_bins must be a list") - for label, names in (("required_bins", required), ("optional_bins", optional)): - for b in names: - if not isinstance(b, str) or not valid_name(b): - die(f"{path}: invalid {label} name {b!r}") - if b in BLOCKED: - die(f"{path}: {b} is not shipped on the ISO") - overlap = set(required) & set(optional) - if overlap: - die(f"{path}: bins in both required and optional: {sorted(overlap)}") - return list(required), list(optional), load_versions(data, path) - - -def pinned_artifact_url(pkg: str, version: str, filename: str) -> str: - if not valid_name(pkg) or not valid_name(version) or not valid_name(filename): - die(f"refusing pinned URL with unsafe path {pkg}/{version}/{filename}") - return f"{DL_ORIGIN}/{pkg}/{version}/{filename}" - - -def package_base_url(pkg_name: str, versions: dict[str, str], first_url: str) -> str: - pin = versions.get(pkg_name) - if pin: - if not valid_name(pkg_name) or not valid_name(pin): - die(f"refusing pinned version dir {pkg_name}/{pin}") - return f"{DL_ORIGIN}/{pkg_name}/{pin}/" - return version_dir(first_url) - - -def fetch(url: str, dest: Path, *, required: bool = True) -> bool: - dest.parent.mkdir(parents=True, exist_ok=True) - try: - urllib.request.urlretrieve(url, dest) - return True - except (urllib.error.URLError, OSError) as e: - if required: - die(f"download failed: {url}: {e}") - print(f"WARN: download failed: {url}: {e}", file=sys.stderr) - if dest.exists(): - dest.unlink() - return False - - -def sha256_file(path: Path) -> str: - h = hashlib.sha256() - with path.open("rb") as f: - for chunk in iter(lambda: f.read(1024 * 1024), b""): - h.update(chunk) - return h.hexdigest() - - -def require_sha256(value: object, what: str) -> str: - if not isinstance(value, str) or not value.strip(): - die(f"{what}: index sha256 is required and must be non-empty") - return value.strip().lower() - - -def verify_sha256(path: Path, expected: str, what: str) -> None: - actual = sha256_file(path) - if actual != expected: - die(f"{what}: sha256 mismatch (expected {expected}, got {actual})") - - -def version_dir(first_dl_url: str) -> str: - parsed = urlparse(first_dl_url) - parent = parsed.path.rsplit("/", 1)[0] - return f"{parsed.scheme}://{parsed.netloc}{parent}/" - - -def verify_index(index_path: Path, sig_path: Path) -> None: - if shutil.which("minisign") is None: - die("minisign is not installed — refuse to trust an unsigned index") - cmd = [ - "minisign", - "-V", - "-q", - "-m", - str(index_path), - "-x", - str(sig_path), - "-P", - MINISIGN_PUBKEY, - ] - result = subprocess.run(cmd, check=False) - if result.returncode != 0: - die("index.json minisign verification FAILED — refusing to proceed") - print("index.json minisign OK") - - -def bin_index(packages: dict) -> dict[str, tuple[str, dict, dict]]: - out: dict[str, tuple[str, dict, dict]] = {} - for pkg_name, pkg in packages.items(): - if pkg_name in BLOCKED: - continue - for b in pkg.get("binaries") or []: - if not isinstance(b, dict): - continue - raw = b.get("name") - if not isinstance(raw, str): - continue - dest = dest_name(raw) - if dest in BLOCKED or pkg_name in BLOCKED: - continue - if dest in out and out[dest][0] != pkg_name: - die(f"index publishes {dest} from both {out[dest][0]} and {pkg_name}") - out[dest] = (pkg_name, pkg, b) - return out - - -def patch_exec_start(text: str, bin_dir: Path) -> str: - lines = [] - for line in text.splitlines(): - if line.lstrip().startswith("ExecStart="): - rest = line.split("=", 1)[1] - argv = rest.split() - if argv: - name = os.path.basename(argv[0]) - new_path = bin_dir / name - if len(argv) == 1: - line = f"ExecStart={new_path}" - else: - line = f"ExecStart={new_path} {' '.join(argv[1:])}" - lines.append(line) - out = "\n".join(lines) - if text.endswith("\n"): - out += "\n" - return out - - -def wanted_by(text: str) -> list[str]: - targets: list[str] = [] - for line in text.splitlines(): - if line.startswith("WantedBy="): - targets.extend(line.split("=", 1)[1].split()) - return targets or ["default.target"] - - -def assert_safe_archive(path: Path) -> None: - with tarfile.open(path, "r:gz") as tf: - for info in tf.getmembers(): - name = info.name - if info.issym() or info.islnk(): - die(f"refusing archive with symlink entry {name!r}") - if name.startswith("/") or any(p in ("..", "") for p in Path(name).parts if p == ".."): - die(f"refusing archive with unsafe path {name!r}") - if Path(name).is_absolute() or ".." in Path(name).parts: - die(f"refusing archive with unsafe path {name!r}") - - -def stage_file( - url: str, - dest: Path, - sha256: str | None, - what: str, - mode: int | None = None, - *, - required: bool = True, -) -> bool: - if not fetch(url, dest, required=required): - return False - if sha256 is not None: - verify_sha256(dest, sha256, what) - if mode is not None: - dest.chmod(mode) - return True - - -def main() -> int: - # CI logs mix stdout/stderr; keep them in source order. - try: - sys.stdout.reconfigure(line_buffering=True) - sys.stderr.reconfigure(line_buffering=True) - except (AttributeError, OSError): - pass - repo = Path(__file__).resolve().parents[1] - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--home", - default=os.environ.get("LAPTOP_HOME", "/build-home"), - help="builder home to populate (default: $LAPTOP_HOME or /build-home)", - ) - parser.add_argument( - "--lockfile", - default=str(repo / "iso" / "bread-lockfile.toml"), - ) - parser.add_argument("--index-url", default=INDEX_URL) - args = parser.parse_args() - - home = Path(args.home) - lockfile = Path(args.lockfile) - if not lockfile.is_file(): - die(f"lockfile missing: {lockfile}") - - required, optional, versions = load_lockfile(lockfile) - print( - f"lockfile {lockfile}: {len(required)} required, {len(optional)} optional" - + (f", {len(versions)} pinned" if versions else "") - ) - - bin_dir = home / ".local" / "bin" - state_dir = home / ".local" / "state" / "bakery" - cache_dir = home / ".cache" / "bakery" - share_dir = home / ".local" / "share" - unit_dir = home / ".config" / "systemd" / "user" - for d in (bin_dir, state_dir, cache_dir, share_dir, unit_dir): - d.mkdir(parents=True, exist_ok=True) - - index_path = cache_dir / "index.json" - sig_path = cache_dir / "index.json.minisig" - print(f"fetch {args.index_url}") - fetch(args.index_url, index_path) - print(f"fetch {args.index_url}.minisig") - fetch(args.index_url + ".minisig", sig_path) - verify_index(index_path, sig_path) - - with index_path.open() as f: - idx = json.load(f) - packages = idx.get("packages") - if not isinstance(packages, dict): - die("index.json: missing packages object") - - published = bin_index(packages) - selected: dict[str, dict] = {} - installed_bins: dict[str, list[str]] = {} - installed_sha: dict[str, dict[str, str]] = {} - fetched_url: dict[str, str] = {} - - pin_warned: set[str] = set() - - def pin_digest(pkg_name: str, pkg: dict, value: object, what: str) -> str | None: - """Index sha256 is only valid when it describes the pinned version.""" - digest = require_sha256(value, what) - pin = versions.get(pkg_name) - if pin and str(pkg.get("version")) != pin: - if pkg_name not in pin_warned: - print( - f"WARN: {pkg_name} pin {pin} != index {pkg.get('version')}; " - f"fetching pinned URL without index sha256", - file=sys.stderr, - ) - pin_warned.add(pkg_name) - return None - return digest - - def take_bin(name: str, *, required_bin: bool) -> bool: - hit = published.get(name) - if hit is None: - if required_bin: - die(f"required bin {name!r} is not in the verified stable index") - print(f"WARN: optional bin {name} not in index — skipping", file=sys.stderr) - return False - pkg_name, pkg, binary = hit - if pkg_name in BLOCKED or name in BLOCKED: - die(f"refusing blocked package/bin {pkg_name}/{name}") - raw = binary.get("name") - index_url = binary.get("dl_url") - pin = versions.get(pkg_name) - if pin: - if not isinstance(raw, str) or not valid_name(raw): - die(f"{name}: missing binary filename for pinned URL") - url = pinned_artifact_url(pkg_name, pin, raw) - else: - url = index_url - if not isinstance(url, str) or not url: - die(f"{name}: missing dl_url") - digest = pin_digest(pkg_name, pkg, binary.get("sha256"), f"binary {name}") - dest = bin_dir / name - note = f" (pin {pkg_name}={pin})" if pin else "" - print(f" {name} <- {url}{note}") - if not stage_file( - url, dest, digest, f"binary {name}", mode=0o755, required=required_bin - ): - return False - selected[pkg_name] = pkg - installed_bins.setdefault(pkg_name, []).append(name) - if digest is not None: - installed_sha.setdefault(pkg_name, {})[name] = digest - fetched_url.setdefault(pkg_name, url) - return True - - for name in required: - take_bin(name, required_bin=True) - for name in optional: - take_bin(name, required_bin=False) - - if not selected: - die("no packages selected from lockfile ∩ index") - - now = datetime.now(timezone.utc).replace(microsecond=0).isoformat() - installed: dict[str, dict] = {} - - for pkg_name, pkg in sorted(selected.items()): - bins = pkg.get("binaries") or [] - first_url = fetched_url.get(pkg_name) - if not first_url: - for b in bins: - if isinstance(b, dict) and b.get("dl_url"): - first_url = b["dl_url"] - break - if not first_url: - die(f"{pkg_name}: no binary dl_url to derive version dir") - base = package_base_url(pkg_name, versions, first_url) - service_names: list[str] = [] - - for svc in pkg.get("services") or []: - if not isinstance(svc, dict): - die(f"{pkg_name}: service entry must be an object with unit + sha256") - unit = svc.get("unit") - if not isinstance(unit, str) or not valid_name(unit): - die(f"{pkg_name}: invalid service unit {unit!r}") - digest = pin_digest(pkg_name, pkg, svc.get("sha256"), f"{pkg_name} {unit}") - dest = unit_dir / unit - url = urljoin(base, unit) - print(f" {unit} <- {url}") - fetch(url, dest) - if digest is not None: - verify_sha256(dest, digest, f"unit {unit}") - dest.write_text(patch_exec_start(dest.read_text(), bin_dir)) - dest.chmod(0o644) - if svc.get("enable"): - for target in wanted_by(dest.read_text()): - if not valid_name(target): - die(f"{unit}: invalid WantedBy {target!r}") - wants = unit_dir / f"{target}.wants" - wants.mkdir(parents=True, exist_ok=True) - link = wants / unit - if link.exists() or link.is_symlink(): - link.unlink() - link.symlink_to(Path("..") / unit) - print(f" enabled {target}.wants/{unit}") - service_names.append(unit) - - archive = pkg.get("data_archive") - if archive: - if not isinstance(archive, str) or not valid_name(archive): - die(f"{pkg_name}: invalid data_archive {archive!r}") - digest = pin_digest( - pkg_name, pkg, pkg.get("data_archive_sha256"), f"{pkg_name} {archive}" - ) - url = urljoin(base, archive) - data_dir = share_dir / pkg_name - data_dir.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(prefix=f"bos-{pkg_name}-") as tmp: - tmp_path = Path(tmp) / archive - print(f" {archive} <- {url}") - stage_file(url, tmp_path, digest, f"{pkg_name} {archive}") - assert_safe_archive(tmp_path) - subprocess.run( - [ - "tar", - "xzf", - str(tmp_path), - "--no-same-owner", - "--no-same-permissions", - "-C", - str(data_dir), - ], - check=True, - ) - print(f" extracted to {data_dir}") - - desktop = pkg.get("desktop_file") - if desktop: - if not isinstance(desktop, str) or not valid_name(desktop): - die(f"{pkg_name}: invalid desktop_file {desktop!r}") - digest = pin_digest( - pkg_name, pkg, pkg.get("desktop_file_sha256"), f"{pkg_name} {desktop}" - ) - dest = share_dir / "applications" / f"{pkg_name}.desktop" - stage_file(urljoin(base, desktop), dest, digest, f"{pkg_name} {desktop}") - - license_file = pkg.get("license_file") - if license_file: - if not isinstance(license_file, str) or not valid_name(license_file): - die(f"{pkg_name}: invalid license_file {license_file!r}") - digest = pin_digest( - pkg_name, pkg, pkg.get("license_file_sha256"), f"{pkg_name} {license_file}" - ) - dest = share_dir / "licenses" / pkg_name / "LICENSE" - stage_file(urljoin(base, license_file), dest, digest, f"{pkg_name} {license_file}") - - installed[pkg_name] = { - "name": pkg_name, - "version": versions.get(pkg_name, pkg.get("version")), - "binaries": installed_bins.get(pkg_name, []), - "services": service_names, - "installed_at": now, - "track": "stable", - "binary_sha256": installed_sha.get(pkg_name, {}), - } - - if "breadhelp" in installed: - content = share_dir / "breadhelp" / "content" - if not content.is_dir(): - die(f"breadhelp data_archive did not produce {content}") - - state_path = state_dir / "installed.json" - state_path.write_text(json.dumps({"track": "stable", "packages": installed}, indent=2) + "\n") - print(f"installed.json written ({len(installed)} packages): {', '.join(sorted(installed))}") - print(f"staged bins: {', '.join(sorted(p.name for p in bin_dir.iterdir() if p.is_file()))}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/ci-verify-bake.sh b/scripts/ci-verify-bake.sh deleted file mode 100755 index e55b11e..0000000 --- a/scripts/ci-verify-bake.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash -# Read-only checks that a builder home (and optionally a staged image) has -# everything build-local.sh needs before mkarchiso. Exit non-zero on failure. -# -# Builder home stays user-layout (~/.local). The image is system-prefix -# /usr/local; pass SKEL and/or AIROOTFS to check those destinations. -# -# LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh -# SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh -# AIROOTFS=/tmp/bos-iso-stage/airootfs ./scripts/ci-verify-bake.sh -set -euo pipefail - -REPO="$(cd "$(dirname "$0")/.." && pwd)" -LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}" -LAPTOP_HOME="${LAPTOP_HOME:-/build-home}" -SKEL="${SKEL:-}" -AIROOTFS="${AIROOTFS:-}" - -pass=0 -fail=0 -ok() { printf ' PASS %s\n' "$1"; pass=$((pass + 1)); } -bad() { printf ' FAIL %s\n' "$1" >&2; fail=$((fail + 1)); } - -if [[ ! -f "$LOCKFILE" ]]; then - echo "ERROR: lockfile missing: $LOCKFILE" >&2 - exit 1 -fi - -eval "$(python3 - "$LOCKFILE" <<'PY' -import sys, tomllib -path = sys.argv[1] -with open(path, "rb") as f: - data = tomllib.load(f) -required = data.get("required_bins") -optional = data.get("optional_bins") or [] -if required is None: - required = data.get("bins") or data.get("binaries") or [] -def emit(name, values): - print(f"{name}=(") - for v in values: - print(f" {v!r}") - print(")") -emit("REQUIRED_BINS", required) -emit("OPTIONAL_BINS", optional) -PY -)" - -echo "== lockfile $LOCKFILE ==" -echo " ${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional" -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() { - local path="$1" label="$2" - if [[ -x "$path" && -f "$path" ]]; then - ok "$label executable: $path" - else - bad "$label missing or not executable: $path" - fi -} - -check_dir() { - local path="$1" label="$2" - if [[ -d "$path" ]]; then - ok "$label: $path" - else - bad "$label missing: $path" - fi -} - -check_file() { - local path="$1" label="$2" - if [[ -f "$path" ]]; then - ok "$label: $path" - else - bad "$label missing: $path" - fi -} - -for b in "${REQUIRED_BINS[@]}"; do - check_exec "$LAPTOP_HOME/.local/bin/$b" "required bin $b" -done -for b in "${OPTIONAL_BINS[@]}"; do - if [[ -x "$LAPTOP_HOME/.local/bin/$b" ]]; then - ok "optional bin $b present" - else - printf ' ---- optional bin %s not staged (ok until bread ships it)\n' "$b" - fi -done - -check_dir "$LAPTOP_HOME/.local/share/breadhelp/content" "breadhelp content" -check_file "$LAPTOP_HOME/.cache/bakery/index.json" "bakery index cache" -check_file "$LAPTOP_HOME/.local/state/bakery/installed.json" "bakery installed.json" - -mapfile -t UNITS < <(python3 - "$LAPTOP_HOME/.local/state/bakery/installed.json" <<'PY' -import json, sys -path = sys.argv[1] -with open(path) as f: - data = json.load(f) -pkgs = data.get("packages", data) -for pkg in pkgs.values(): - for s in pkg.get("services", []): - print(s["unit"] if isinstance(s, dict) else s) -PY -) -if [[ ${#UNITS[@]} -eq 0 ]]; then - bad "installed.json lists no service units" -else - for unit in "${UNITS[@]}"; do - [[ -n "$unit" ]] || continue - check_file "$LAPTOP_HOME/.config/systemd/user/$unit" "unit $unit" - done -fi - -if [[ -n "$SKEL" && -z "$AIROOTFS" ]]; then - if [[ -d "$SKEL/usr/local/bin" ]]; then - AIROOTFS="$SKEL" - SKEL="$AIROOTFS/etc/skel" - elif [[ -d "$SKEL/../../usr/local" ]]; then - AIROOTFS="$(cd "$SKEL/../.." && pwd)" - fi -elif [[ -n "$AIROOTFS" && -z "$SKEL" ]]; then - SKEL="$AIROOTFS/etc/skel" -fi - -if [[ -n "$AIROOTFS" || -n "$SKEL" ]]; then - if [[ -n "$AIROOTFS" ]]; then - echo "== staged image $AIROOTFS ==" - check_file "$AIROOTFS/etc/bakery/config.toml" "bakery prefix config" - if [[ -f "$AIROOTFS/etc/bakery/config.toml" ]] && grep -q 'prefix[[:space:]]*=[[:space:]]*"/usr/local"' "$AIROOTFS/etc/bakery/config.toml"; then - ok "bakery prefix = /usr/local" - else - bad "bakery prefix is not /usr/local in $AIROOTFS/etc/bakery/config.toml" - 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 -fi - -echo -printf 'Result: %d passed, %d failed\n' "$pass" "$fail" -[[ "$fail" -eq 0 ]] diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 28b5c28..6672447 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -40,13 +40,13 @@ 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 @@ -55,56 +55,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\" ]" +check "breadhelp content installed" "[ -d /usr/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\" ]"