Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df2e1310bb | ||
|
|
b270d64adc | ||
|
|
64aa3611b4 | ||
|
|
055d92cc5a | ||
|
|
9fe02eeea6 | ||
|
|
8cd5ec9e16 | ||
|
|
863fb80de2 | ||
|
|
43c0a5e2b5 | ||
|
|
f3d6c55234 | ||
|
|
70d4dd424b | ||
|
|
34043086b9 | ||
|
|
744f18cd90 | ||
|
|
fd385bafae |
37 changed files with 2132 additions and 224 deletions
|
|
@ -12,8 +12,8 @@ name: Build and release ISO
|
||||||
# MIRROR_TOKEN — GitHub personal access token with repo scope
|
# MIRROR_TOKEN — GitHub personal access token with repo scope
|
||||||
# GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing"
|
# GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing"
|
||||||
# identity (releases@breadway.dev); public half is committed
|
# identity (releases@breadway.dev); public half is committed
|
||||||
# at KEYS.asc for verifying ISO SHA256SUMS only. That key
|
# at KEYS.asc. Signs ISO SHA256SUMS here; the same secret
|
||||||
# does not sign the [breadway] pacman repo. No passphrase
|
# signs the [breadway] repo in signed-repo.yml. No passphrase
|
||||||
# (CI-only key, access controlled via the Forgejo secret
|
# (CI-only key, access controlled via the Forgejo secret
|
||||||
# store).
|
# store).
|
||||||
|
|
||||||
|
|
@ -41,7 +41,10 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
pacman -Syu --noconfirm archiso curl python git minisign
|
# 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
|
||||||
|
|
||||||
- name: Determine tag and version
|
- name: Determine tag and version
|
||||||
id: vars
|
id: vars
|
||||||
|
|
|
||||||
52
.forgejo/workflows/signed-repo.yml
Normal file
52
.forgejo/workflows/signed-repo.yml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
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
|
||||||
|
|
@ -52,4 +52,7 @@ There is no `dev` integration branch.
|
||||||
- Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs
|
- Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs
|
||||||
work does not require it.
|
work does not require it.
|
||||||
- Don't tell users to `snapper rollback` blindly; GRUB pins
|
- Don't tell users to `snapper rollback` blindly; GRUB pins
|
||||||
`rootflags=subvol=@`. Recovery is grub-btrfs reboot.
|
`rootflags=subvol=@`. Recovery is grub-btrfs reboot. Bakery desktop
|
||||||
|
apps on BOS are system-prefix `/usr/local` (`/etc/bakery/config.toml`);
|
||||||
|
snapper `@` snapshots include them. Do not move those bits back to
|
||||||
|
`~/.local` on the image (hermes / default bakery stay user-layout).
|
||||||
|
|
|
||||||
102
README.md
102
README.md
|
|
@ -14,8 +14,9 @@ wiring up dotfiles, no per-tool bakery installs.
|
||||||
|
|
||||||
- **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated
|
- **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated
|
||||||
keybinds, snappy animations, blur, and pywal-driven colours on a black base.
|
keybinds, snappy animations, blur, and pywal-driven colours on a black base.
|
||||||
- **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries
|
- **bread ecosystem**, baked into `/usr/local` from bakery-managed binaries
|
||||||
(no network needed at install time): the `bread`/`breadd` automation daemon
|
(no network needed at install time; per-user bakery state is seeded in
|
||||||
|
`/etc/skel`): the `bread`/`breadd` automation daemon
|
||||||
(`bread-emit` / `bread-module-host` when the stable bread release publishes
|
(`bread-emit` / `bread-module-host` when the stable bread release publishes
|
||||||
them), `breadbar` (status bar + notifications), `breadbox` (launcher),
|
them), `breadbar` (status bar + notifications), `breadbox` (launcher),
|
||||||
`breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles),
|
`breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles),
|
||||||
|
|
@ -45,9 +46,11 @@ wiring up dotfiles, no per-tool bakery installs.
|
||||||
Mesa only — **NVIDIA proprietary drivers are not included** and NVIDIA is
|
Mesa only — **NVIDIA proprietary drivers are not included** and NVIDIA is
|
||||||
unsupported out of the box (see [docs/hardware.md](docs/hardware.md)).
|
unsupported out of the box (see [docs/hardware.md](docs/hardware.md)).
|
||||||
- **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every
|
- **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every
|
||||||
pacman transaction; zram swap; ufw firewall (deny-incoming, mDNS allowed).
|
pacman transaction (**root `@` only** — snapper does not cover `@home`);
|
||||||
A/B root swapping is **not** implemented. Recovery is a grub-btrfs reboot,
|
home backup is **Settings → Backup** (restic, local path or SFTP); zram
|
||||||
not `snapper rollback` (GRUB pins `rootflags=subvol=@`). See
|
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).
|
[docs/hardware.md](docs/hardware.md).
|
||||||
- **Security**: optional full-disk encryption is **LUKS1** (GRUB cannot unlock
|
- **Security**: optional full-disk encryption is **LUKS1** (GRUB cannot unlock
|
||||||
LUKS2 + Argon2id). Secure Boot is **self-signed Setup Mode only** via
|
LUKS2 + Argon2id). Secure Boot is **self-signed Setup Mode only** via
|
||||||
|
|
@ -58,7 +61,7 @@ wiring up dotfiles, no per-tool bakery installs.
|
||||||
|
|
||||||
| Channel | What |
|
| Channel | What |
|
||||||
|---------|------|
|
|---------|------|
|
||||||
| **Bakery, required** | `bakery`, `bread` / `breadd`, `breadbar`, `breadbox` / `breadbox-sync`, `breadcrumbs`, `breadpad` / `breadman`, `breadpaper`, `bread-theme`, `breadmon`, `breadsearch` / `breadmill`, `breadclip` / `breadclipd`, `breadshot`, `bos-settings`, `breadhelp` (+ breadhelp content under `~/.local/share/breadhelp/`) |
|
| **Bakery, required** | `bakery`, `bread` / `breadd`, `breadbar`, `breadbox` / `breadbox-sync`, `breadcrumbs`, `breadpad` / `breadman`, `breadpaper`, `bread-theme`, `breadmon`, `breadsearch` / `breadmill`, `breadclip` / `breadclipd`, `breadshot`, `bos-settings`, `breadhelp` (+ breadhelp content under `/usr/local/share/breadhelp/`) |
|
||||||
| **Bakery, optional** | `bread-emit`, `bread-module-host` — baked when the verified stable index publishes them; skipped (not a failed bake) until bread ships them |
|
| **Bakery, optional** | `bread-emit`, `bread-module-host` — baked when the verified stable index publishes them; skipped (not a failed bake) until bread ships them |
|
||||||
| **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) |
|
| **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) |
|
||||||
| **Not shipped** | `breadcast`, `breadarr` |
|
| **Not shipped** | `breadcast`, `breadarr` |
|
||||||
|
|
@ -93,11 +96,12 @@ bos/
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── ci-stage-bakery.py # CI: minisign-verified index → $LAPTOP_HOME
|
│ ├── ci-stage-bakery.py # CI: minisign-verified index → $LAPTOP_HOME
|
||||||
│ ├── ci-verify-bake.sh # CI: read-only checks before mkarchiso
|
│ ├── 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
|
│ └── smoke-test.sh
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── hardware.md # Mesa only, NVIDIA, grub-btrfs recovery
|
│ ├── hardware.md # Mesa only, NVIDIA, grub-btrfs recovery
|
||||||
│ └── signed-repo.md # future dl.breadway.dev/arch signing
|
│ └── signed-repo.md # dl.breadway.dev/arch signing
|
||||||
├── .forgejo/workflows/ # CI: AUR republish + tagged ISO release
|
├── .forgejo/workflows/ # CI: AUR republish + signed repo + tagged ISO
|
||||||
├── build-local.sh # native ISO build for this machine
|
├── build-local.sh # native ISO build for this machine
|
||||||
├── README.md
|
├── README.md
|
||||||
└── DESIGN.md # historical plan
|
└── DESIGN.md # historical plan
|
||||||
|
|
@ -123,9 +127,16 @@ Push `origin` (and `github` when mirroring). Do not treat origin as GitHub.
|
||||||
|
|
||||||
## Building the ISO
|
## Building the ISO
|
||||||
|
|
||||||
`build-local.sh` builds the image natively (no container) and bakes this
|
`build-local.sh` builds the image natively (no container) and copies this
|
||||||
machine's bakery-installed bread binaries + breadhelp content into
|
machine's bakery-installed bread binaries + breadhelp content from the
|
||||||
`/etc/skel`:
|
builder's `~/.local` into the image at `/usr/local` (bins, share/data,
|
||||||
|
desktop files, licenses) and `/usr/lib/systemd/user` (units). Per-user
|
||||||
|
bakery state (`installed.json` + index cache) is seeded in `/etc/skel`.
|
||||||
|
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`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo ./build-local.sh # release-quality (xz squashfs)
|
sudo ./build-local.sh # release-quality (xz squashfs)
|
||||||
|
|
@ -190,8 +201,38 @@ Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to
|
||||||
avoid memory pressure.
|
avoid memory pressure.
|
||||||
|
|
||||||
Post-install, `scripts/smoke-test.sh` (run as the installed user) checks
|
Post-install, `scripts/smoke-test.sh` (run as the installed user) checks
|
||||||
subvolumes, services, bakery bins, and breadhelp content under
|
subvolumes, services, bakery bins on PATH, breadhelp content under
|
||||||
`~/.local/share/breadhelp/content`.
|
`/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
|
## bos-settings
|
||||||
|
|
||||||
|
|
@ -226,7 +267,8 @@ also get live systemd status + Start/Stop/Restart/Logs.
|
||||||
| Packages | `bakery` installed list + updates, pacman system update |
|
| Packages | `bakery` installed list + updates, pacman system update |
|
||||||
| AUR | Search via `yay`; installing opens a terminal (AUR build scripts need review) |
|
| AUR | Search via `yay`; installing opens a terminal (AUR build scripts need review) |
|
||||||
| Firmware | `fwupd` device list + updates |
|
| Firmware | `fwupd` device list + updates |
|
||||||
| Snapshots | `snapper` list / boot-into (grub-btrfs) / delete |
|
| 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 |
|
||||||
|
|
||||||
Source and build live in the [bos-settings](https://git.breadway.dev/Breadway/bos-settings)
|
Source and build live in the [bos-settings](https://git.breadway.dev/Breadway/bos-settings)
|
||||||
repo, not here.
|
repo, not here.
|
||||||
|
|
@ -234,7 +276,7 @@ repo, not here.
|
||||||
## The bread ecosystem
|
## The bread ecosystem
|
||||||
|
|
||||||
Everything below is a separate bakery-distributed project with its own repo
|
Everything below is a separate bakery-distributed project with its own repo
|
||||||
and release cadence, baked into `/etc/skel` at ISO build time so a fresh
|
and release cadence, baked into `/usr/local` at ISO build time so a fresh
|
||||||
install has them all with no network round-trip. Some ship more than one
|
install has them all with no network round-trip. Some ship more than one
|
||||||
binary from a single package — that's noted where it applies. Most have a
|
binary from a single package — that's noted where it applies. Most have a
|
||||||
corresponding **bos-settings** panel; this table is about *using* the app
|
corresponding **bos-settings** panel; this table is about *using* the app
|
||||||
|
|
@ -258,7 +300,7 @@ directly.
|
||||||
| `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` |
|
| `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` |
|
||||||
| `breadclip` | Clipboard history. `breadclipd` is the background daemon that actually records history; `breadclip` is the GTK4 popup that browses it | `SUPER+V` / `SUPER+Shift+V` |
|
| `breadclip` | Clipboard history. `breadclipd` is the background daemon that actually records history; `breadclip` is the GTK4 popup that browses it | `SUPER+V` / `SUPER+Shift+V` |
|
||||||
| `breadsearch` | Semantic system-wide search (indexes files/notes, embeds locally — CPU/ROCm/CUDA backend configurable). `breadmill` is its indexing daemon. | via breadbox, or BOS Settings → File Search |
|
| `breadsearch` | Semantic system-wide search (indexes files/notes, embeds locally — CPU/ROCm/CUDA backend configurable). `breadmill` is its indexing daemon. | via breadbox, or BOS Settings → File Search |
|
||||||
| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `~/.local/share/breadhelp/content` (bakery `content.tar.gz`, baked into skel). | `SUPER+/` |
|
| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `/usr/local/share/breadhelp/content` (bakery `content.tar.gz`, baked into the image). | `SUPER+/` |
|
||||||
|
|
||||||
**System**
|
**System**
|
||||||
|
|
||||||
|
|
@ -344,15 +386,18 @@ until `dl.breadway.dev/arch` exists).
|
||||||
|
|
||||||
## Recovery
|
## Recovery
|
||||||
|
|
||||||
**An update broke something (system still boots):** open BOS Settings →
|
**An update broke something (system still boots):** reboot → **GRUB
|
||||||
Snapshots and pick a snapshot to boot, **or** choose one from the **GRUB
|
“snapshots” submenu** (grub-btrfs), then boot that entry.
|
||||||
“snapshots” submenu** (grub-btrfs) at boot, then reboot into it.
|
|
||||||
|
|
||||||
Do **not** run `snapper rollback` as the default recovery step. BOS GRUB
|
BOS Settings → Snapshots lists each snapshot’s number, date, and
|
||||||
pins `rootflags=subvol=@`, so a snapper-swapped default subvolume is not
|
description so you know which GRUB entry to pick. It does not roll the
|
||||||
what the installed grub.cfg will boot next. Use the grub-btrfs entry so the
|
running root back in place. Snapper is root only. Home files are
|
||||||
kernel command line matches the snapshot you want. Details:
|
**Settings → Backup** (restic restore into `~/bos-restore-<id>`, not
|
||||||
[docs/hardware.md](docs/hardware.md).
|
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
|
A/B root swapping (SteamOS-style) is a **future** idea in DESIGN.md — it is
|
||||||
not shipped.
|
not shipped.
|
||||||
|
|
@ -360,14 +405,15 @@ not shipped.
|
||||||
**The system won't boot (broken GRUB / lost EFI entry):**
|
**The system won't boot (broken GRUB / lost EFI entry):**
|
||||||
|
|
||||||
1. Boot the BOS ISO and open a terminal (`SUPER+Return`).
|
1. Boot the BOS ISO and open a terminal (`SUPER+Return`).
|
||||||
2. Mount the installed root and EFI, then chroot:
|
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:
|
||||||
```sh
|
```sh
|
||||||
mount -o subvol=@ /dev/sdXN /mnt
|
mount -o subvol=@ /dev/sdXN /mnt
|
||||||
mount /dev/sdXP /mnt/boot/efi # the EFI partition
|
mount /dev/sdXP /mnt/boot/efi # the EFI partition
|
||||||
arch-chroot /mnt
|
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 --bootloader-id=BOS --recheck
|
||||||
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
|
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
|
||||||
grub-mkconfig -o /boot/grub/grub.cfg
|
grub-mkconfig -o /boot/grub/grub.cfg
|
||||||
|
|
|
||||||
240
build-local.sh
240
build-local.sh
|
|
@ -41,13 +41,15 @@ if [ "${FAST_BUILD:-0}" = "1" ]; then
|
||||||
fi
|
fi
|
||||||
grep airootfs_image_tool_options "$STAGE/profiledef.sh"
|
grep airootfs_image_tool_options "$STAGE/profiledef.sh"
|
||||||
|
|
||||||
# --- Bake this machine's bakery-installed bread ecosystem into /etc/skel ------
|
# --- Bake this machine's bakery-installed bread ecosystem into the image ------
|
||||||
# The bread desktop apps are bakery-managed (release binaries from
|
# The bread desktop apps are bakery-managed (release binaries from
|
||||||
# dl.breadway.dev / GitHub), not pacman. bakery needs DNS at install time,
|
# dl.breadway.dev / GitHub), not pacman. bakery needs DNS at install time,
|
||||||
# which the live/installed image doesn't have — so instead of running bakery
|
# which the live/installed image doesn't have — so instead of running bakery
|
||||||
# on the target, we copy the binaries + bakery manifest this builder already
|
# on the target, we copy the binaries + bakery manifest this builder already
|
||||||
# has into skel. Every user created from skel then gets those versions fully
|
# has. Builder home stays user-layout (~/.local); the *image* is system-prefix
|
||||||
# offline. Copied at build time so the binaries never bloat the git repo.
|
# /usr/local so apps live on @ and ride snapper/grub-btrfs snapshots.
|
||||||
|
# installed.json + index cache stay per-user in skel. Copied at build time
|
||||||
|
# so the binaries never bloat the git repo.
|
||||||
#
|
#
|
||||||
# CI should prefer the stable bakery index when populating the builder home.
|
# CI should prefer the stable bakery index when populating the builder home.
|
||||||
# Local builds still snapshot the builder. required_bins fail the bake if
|
# Local builds still snapshot the builder. required_bins fail the bake if
|
||||||
|
|
@ -97,9 +99,14 @@ BAKERY_BIN="$LAPTOP_HOME/.local/bin"
|
||||||
BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery"
|
BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery"
|
||||||
BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery"
|
BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery"
|
||||||
BAKERY_SHARE="$LAPTOP_HOME/.local/share"
|
BAKERY_SHARE="$LAPTOP_HOME/.local/share"
|
||||||
SKEL="$STAGE/airootfs/etc/skel"
|
AIROOTFS="$STAGE/airootfs"
|
||||||
|
IMAGE_BIN="$AIROOTFS/usr/local/bin"
|
||||||
|
IMAGE_SHARE="$AIROOTFS/usr/local/share"
|
||||||
|
IMAGE_UNITS="$AIROOTFS/usr/lib/systemd/user"
|
||||||
|
SKEL="$AIROOTFS/etc/skel"
|
||||||
echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ==="
|
echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ==="
|
||||||
echo "lockfile: $LOCKFILE (${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional)"
|
echo "lockfile: $LOCKFILE (${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional)"
|
||||||
|
echo "image prefix: /usr/local (bins $IMAGE_BIN, share $IMAGE_SHARE, units $IMAGE_UNITS)"
|
||||||
|
|
||||||
missing=()
|
missing=()
|
||||||
for b in "${REQUIRED_BINS[@]}"; do
|
for b in "${REQUIRED_BINS[@]}"; do
|
||||||
|
|
@ -124,9 +131,9 @@ for b in "${OPTIONAL_BINS[@]}"; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
install -d -m 0755 "$SKEL/.local/bin" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery"
|
install -d -m 0755 "$IMAGE_BIN" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery"
|
||||||
for b in "${BREAD_BINS[@]}"; do
|
for b in "${BREAD_BINS[@]}"; do
|
||||||
install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b"
|
install -m 0755 "$BAKERY_BIN/$b" "$IMAGE_BIN/$b"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Drop packages that are not in the lockfile (breadcast/breadarr must not
|
# Drop packages that are not in the lockfile (breadcast/breadarr must not
|
||||||
|
|
@ -164,26 +171,27 @@ if [[ ! -f "$BAKERY_CACHE/index.json" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json"
|
install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json"
|
||||||
echo "baked bins: $(ls "$SKEL/.local/bin")"
|
echo "baked bins: $(ls "$IMAGE_BIN")"
|
||||||
|
|
||||||
# --- Bake bakery data dirs the apps need offline ------------------------------
|
# --- Bake bakery data dirs the apps need offline ------------------------------
|
||||||
# bakery extracts data_archive (breadhelp's content.tar.gz) to
|
# bakery extracts data_archive (breadhelp's content.tar.gz) to
|
||||||
# ~/.local/share/<pkg>/ and writes desktop entries + licenses next to it.
|
# $prefix/share/<pkg>/ and writes desktop entries + licenses next to it.
|
||||||
# Copy those — never laptop-local state (clipboard history, WebKit cache,
|
# Builder home is still ~/.local/share; copy into the image at
|
||||||
# bread sync-repo, models).
|
# /usr/local/share. Never laptop-local state (clipboard history, WebKit
|
||||||
echo "=== baking bakery share/data into skel ==="
|
# cache, bread sync-repo, models).
|
||||||
|
echo "=== baking bakery share/data into /usr/local/share ==="
|
||||||
BREADHELP_CONTENT="$BAKERY_SHARE/breadhelp/content"
|
BREADHELP_CONTENT="$BAKERY_SHARE/breadhelp/content"
|
||||||
if [[ ! -d "$BREADHELP_CONTENT" ]]; then
|
if [[ ! -d "$BREADHELP_CONTENT" ]]; then
|
||||||
echo "ERROR: breadhelp content missing: $BREADHELP_CONTENT" >&2
|
echo "ERROR: breadhelp content missing: $BREADHELP_CONTENT" >&2
|
||||||
echo "bakery installs this from content.tar.gz into ~/.local/share/breadhelp/content" >&2
|
echo "bakery installs this from content.tar.gz into ~/.local/share/breadhelp/content on the builder" >&2
|
||||||
echo "A breadhelp binary without content is a hollow ISO." >&2
|
echo "A breadhelp binary without content is a hollow ISO." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
install -d -m 0755 "$SKEL/.local/share"
|
install -d -m 0755 "$IMAGE_SHARE"
|
||||||
cp -a "$BAKERY_SHARE/breadhelp" "$SKEL/.local/share/breadhelp"
|
cp -a "$BAKERY_SHARE/breadhelp" "$IMAGE_SHARE/breadhelp"
|
||||||
echo " baked $SKEL/.local/share/breadhelp/content"
|
echo " baked $IMAGE_SHARE/breadhelp/content"
|
||||||
|
|
||||||
python3 - "$BAKERY_CACHE/index.json" "$BAKERY_SHARE" "$SKEL/.local/share" "${BREAD_BINS[@]}" <<'PY'
|
python3 - "$BAKERY_CACHE/index.json" "$BAKERY_SHARE" "$IMAGE_SHARE" "${BREAD_BINS[@]}" <<'PY'
|
||||||
import json, os, shutil, sys
|
import json, os, shutil, sys
|
||||||
index_path, src_share, dest_share, *bins = sys.argv[1:]
|
index_path, src_share, dest_share, *bins = sys.argv[1:]
|
||||||
wanted = set(bins)
|
wanted = set(bins)
|
||||||
|
|
@ -241,58 +249,194 @@ PY
|
||||||
# bakery package's service (breadbox-sync, breadmill, breadclipd, ...) was
|
# bakery package's service (breadbox-sync, breadmill, breadclipd, ...) was
|
||||||
# silently left out, so those daemons never start on a fresh install/live
|
# silently left out, so those daemons never start on a fresh install/live
|
||||||
# boot until the user re-runs `bakery install` (which needs network).
|
# boot until the user re-runs `bakery install` (which needs network).
|
||||||
# Source of truth is the *filtered* installed.json we just wrote: only
|
# Units come from installed.json + the bakery index + local unit files
|
||||||
# lockfile packages. Copy each unit with ExecStart rewritten from this
|
# whose ExecStart is a lockfile binary (installed.json has omitted
|
||||||
# laptop's literal home path to the portable `%h` specifier, and recreate
|
# breadcrumbs.service before). Units go to /usr/lib/systemd/user with
|
||||||
# whichever *.target.wants enable symlink bakery created locally. Units
|
# ExecStart rewritten to /usr/local/bin. Recreate whichever
|
||||||
# already committed by hand (breadd.service carries a
|
# *.target.wants enable symlink bakery created locally (or that skel
|
||||||
# RuntimeDirectoryPreserve=yes fix not yet upstreamed) are left alone.
|
# already ships), and write /etc/systemd/user/*.wants/ (--global).
|
||||||
echo "=== baking bakery service units into skel ==="
|
# Hand-committed skel units (breadd.service carries a
|
||||||
|
# RuntimeDirectoryPreserve=yes fix not yet upstreamed) are the source
|
||||||
|
# for that unit and also get their ExecStart rewritten in skel.
|
||||||
|
echo "=== baking bakery service units into /usr/lib/systemd/user ==="
|
||||||
SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user"
|
SYSTEMD_USER_DIR="$LAPTOP_HOME/.config/systemd/user"
|
||||||
SKEL_SYSTEMD="$SKEL/.config/systemd/user"
|
SKEL_SYSTEMD="$SKEL/.config/systemd/user"
|
||||||
mapfile -t SERVICE_UNITS < <(python3 - "$SKEL/.local/state/bakery/installed.json" <<'PY'
|
install -d -m 0755 "$IMAGE_UNITS"
|
||||||
import json, sys
|
# installed.json on the builder can omit a service even when the index and
|
||||||
with open(sys.argv[1]) as f:
|
# the local unit file exist (breadcrumbs has done this). Merge all three
|
||||||
d = json.load(f)
|
# so every lockfile daemon is baked and can be --global enabled.
|
||||||
for pkg in d.get("packages", d).values():
|
mapfile -t SERVICE_UNITS < <(python3 - \
|
||||||
for s in pkg.get("services", []):
|
"$SKEL/.local/state/bakery/installed.json" \
|
||||||
print(s["unit"] if isinstance(s, dict) else s)
|
"$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
|
PY
|
||||||
)
|
)
|
||||||
|
if [[ ! " ${SERVICE_UNITS[*]} " =~ " breadd.service " ]]; then
|
||||||
|
echo "ERROR: breadd.service not in the bakery unit list — refusing to bake" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rewrite_exec_start() {
|
||||||
|
local src="$1" dest="$2"
|
||||||
|
python3 - "$src" "$dest" <<'PY'
|
||||||
|
import os, sys
|
||||||
|
src, dest = sys.argv[1], sys.argv[2]
|
||||||
|
text = open(src).read()
|
||||||
|
lines = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
if line.lstrip().startswith("ExecStart="):
|
||||||
|
key, rest = line.split("=", 1)
|
||||||
|
argv = rest.split()
|
||||||
|
if argv:
|
||||||
|
name = os.path.basename(argv[0])
|
||||||
|
argv[0] = "/usr/local/bin/" + name
|
||||||
|
line = key + "=" + " ".join(argv)
|
||||||
|
lines.append(line)
|
||||||
|
out = "\n".join(lines)
|
||||||
|
if text.endswith("\n"):
|
||||||
|
out += "\n"
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
with open(dest, "w") as f:
|
||||||
|
f.write(out)
|
||||||
|
PY
|
||||||
|
}
|
||||||
for unit in "${SERVICE_UNITS[@]}"; do
|
for unit in "${SERVICE_UNITS[@]}"; do
|
||||||
[[ -n "$unit" ]] || continue
|
[[ -n "$unit" ]] || continue
|
||||||
if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then
|
if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then
|
||||||
echo " $unit already committed in skel, leaving as-is"
|
src="$SKEL_SYSTEMD/$unit"
|
||||||
continue
|
echo " $unit using committed skel unit as source"
|
||||||
|
else
|
||||||
|
src="$SYSTEMD_USER_DIR/$unit"
|
||||||
|
if [[ ! -f "$src" ]]; then
|
||||||
|
echo "ERROR: $unit listed 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
|
||||||
fi
|
fi
|
||||||
src="$SYSTEMD_USER_DIR/$unit"
|
rewrite_exec_start "$src" "$IMAGE_UNITS/$unit"
|
||||||
if [[ ! -f "$src" ]]; then
|
if [[ -f "$SKEL_SYSTEMD/$unit" ]]; then
|
||||||
echo "ERROR: $unit listed in bakery installed.json but not found at $src" >&2
|
rewrite_exec_start "$src" "$SKEL_SYSTEMD/$unit"
|
||||||
echo "Refusing to bake a skel whose daemons will never start." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
install -d -m 0755 "$SKEL_SYSTEMD"
|
for base in "$SYSTEMD_USER_DIR" "$SKEL_SYSTEMD"; do
|
||||||
sed "s#ExecStart=$LAPTOP_HOME/.local/bin/#ExecStart=%h/.local/bin/#" "$src" > "$SKEL_SYSTEMD/$unit"
|
[[ -d "$base" ]] || continue
|
||||||
for wants_dir in "$SYSTEMD_USER_DIR"/*.target.wants; do
|
for wants_dir in "$base"/*.target.wants; do
|
||||||
[[ -L "$wants_dir/$unit" ]] || continue
|
[[ -e "$wants_dir" || -L "$wants_dir" ]] || continue
|
||||||
target_name="$(basename "$wants_dir")"
|
[[ -L "$wants_dir/$unit" ]] || continue
|
||||||
install -d -m 0755 "$SKEL_SYSTEMD/$target_name"
|
target_name="$(basename "$wants_dir")"
|
||||||
ln -sf "../$unit" "$SKEL_SYSTEMD/$target_name/$unit"
|
install -d -m 0755 "$IMAGE_UNITS/$target_name"
|
||||||
|
ln -sf "../$unit" "$IMAGE_UNITS/$target_name/$unit"
|
||||||
|
done
|
||||||
done
|
done
|
||||||
echo " baked $unit"
|
# systemctl --global enable equivalent: /etc/systemd/user/<WantedBy>.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"
|
||||||
done
|
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
|
# mkarchiso resets every airootfs file to 0644, so executables must be declared
|
||||||
# in profiledef.sh's file_permissions array or they ship non-executable and the
|
# in profiledef.sh's file_permissions array or they ship non-executable and the
|
||||||
# exec-once launches fail with "permission denied". Inject a 0755 entry for each
|
# exec-once launches fail with "permission denied". Inject a 0755 entry for each
|
||||||
# baked binary right after the array opener (keeps the binary list in one place).
|
# baked bakery binary right after the array opener (bos-* bins are already
|
||||||
|
# listed; keeps the bakery list in one place — the lockfile).
|
||||||
perm_file="$(mktemp)"
|
perm_file="$(mktemp)"
|
||||||
for b in "${BREAD_BINS[@]}"; do
|
for b in "${BREAD_BINS[@]}"; do
|
||||||
printf ' ["/etc/skel/.local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file"
|
printf ' ["/usr/local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file"
|
||||||
done
|
done
|
||||||
sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh"
|
sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh"
|
||||||
rm -f "$perm_file"
|
rm -f "$perm_file"
|
||||||
echo "=== file_permissions after injection ==="; grep -A14 '^file_permissions=(' "$STAGE/profiledef.sh"
|
echo "=== file_permissions after injection ==="; grep -A40 '^file_permissions=(' "$STAGE/profiledef.sh"
|
||||||
|
|
||||||
# Pin one timestamp for the whole build. Without this, mkarchiso derives the
|
# Pin one timestamp for the whole build. Without this, mkarchiso derives the
|
||||||
# boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID
|
# boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,43 @@
|
||||||
|
|
||||||
BOS ships the generic **Mesa** stack. AMD and Intel work out of the box.
|
BOS ships the generic **Mesa** stack. AMD and Intel work out of the box.
|
||||||
|
|
||||||
**NVIDIA is unsupported.** The proprietary driver is not included, NVIDIA
|
The proprietary NVIDIA driver is **not on the ISO**. NVIDIA firmware is
|
||||||
firmware is not on the image, and there is no Hyprland NVIDIA env wiring.
|
not on the image either (`linux-firmware-nvidia` stays commented out in
|
||||||
Installing `nvidia` / `nvidia-utils` after the fact is not a product path.
|
`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
|
## Recovery
|
||||||
|
|
||||||
An update that breaks the system is recovered from the **GRUB "snapshots"
|
An update that breaks the system is recovered by **reboot → GRUB
|
||||||
submenu** (grub-btrfs), not `snapper rollback`.
|
“snapshots” submenu** (grub-btrfs). `snapper rollback` will not change
|
||||||
|
what GRUB boots (`rootflags=subvol=@`).
|
||||||
|
|
||||||
BOS GRUB pins `rootflags=subvol=@`. `snapper rollback` swaps the default
|
`snapper rollback` swaps the default subvolume; the installed `grub.cfg`
|
||||||
subvolume; the installed `grub.cfg` will still boot `@`. Pick the grub-btrfs
|
still boots `@`. Pick the grub-btrfs entry so the kernel command line
|
||||||
entry so the kernel command line matches the snapshot you want.
|
matches the snapshot you want.
|
||||||
|
|
||||||
A/B root swapping is not implemented. See the README Recovery section for
|
BOS Settings → Snapshots lists snapshot number, date, and description so
|
||||||
the "system will not boot" GRUB/EFI repair path.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,24 @@
|
||||||
Today the ISO's `[Breadway.os.git.breadway.dev]` section is
|
Today the ISO's `[Breadway.os.git.breadway.dev]` section is
|
||||||
`SigLevel = Never`. That is TLS-only integrity: packages come from Forgejo's
|
`SigLevel = Never`. That is TLS-only integrity: packages come from Forgejo's
|
||||||
Arch registry, which does **not** serve pacman-compatible database
|
Arch registry, which does **not** serve pacman-compatible database
|
||||||
signatures. `KEYS.asc` signs **ISO `SHA256SUMS` only** — it is not imported
|
signatures. `KEYS.asc` signs **ISO `SHA256SUMS`** and, once published, the
|
||||||
as a pacman repo key. Do not flip `SigLevel` to `Required` on that section
|
`dl.breadway.dev/arch` database. It is **not** imported as a pacman repo key
|
||||||
until a signed repo exists and has been verified; Required without
|
on the ISO yet. Do not flip `SigLevel` to `Required` on that section until
|
||||||
signatures breaks the ISO and every installed system.
|
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
|
The signed repo belongs at `https://dl.breadway.dev/arch`, not on Forgejo's
|
||||||
registry.
|
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`
|
## 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:
|
Use the same release-signing key already in CI:
|
||||||
|
|
||||||
- Public half: [`KEYS.asc`](../KEYS.asc)
|
- Public half: [`KEYS.asc`](../KEYS.asc)
|
||||||
|
|
@ -20,6 +28,7 @@ Use the same release-signing key already in CI:
|
||||||
`releases@breadway.dev`)
|
`releases@breadway.dev`)
|
||||||
- Private half: the `GPG_PRIVATE_KEY` Forgejo secret (armoured secret key,
|
- Private half: the `GPG_PRIVATE_KEY` Forgejo secret (armoured secret key,
|
||||||
no passphrase). Same secret `release-iso.yml` uses to sign `SHA256SUMS`.
|
no passphrase). Same secret `release-iso.yml` uses to sign `SHA256SUMS`.
|
||||||
|
The workflow **fails** if this secret is missing.
|
||||||
|
|
||||||
Layout (example for `x86_64`):
|
Layout (example for `x86_64`):
|
||||||
|
|
||||||
|
|
@ -33,53 +42,112 @@ https://dl.breadway.dev/arch/x86_64/
|
||||||
breadway.files.sig
|
breadway.files.sig
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the database **and sign it** with `repo-add -s`:
|
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
|
```sh
|
||||||
export GNUPGHOME=/tmp/gnupg-breadway-repo
|
export GNUPGHOME=/tmp/gnupg-breadway-repo
|
||||||
mkdir -m 700 -p "$GNUPGHOME"
|
mkdir -m 700 -p "$GNUPGHOME"
|
||||||
printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
|
||||||
cd /srv/dl.breadway.dev/arch/x86_64
|
gpg --batch --yes --local-user releases@breadway.dev \
|
||||||
|
--detach-sign breadlock-<ver>-1-x86_64.pkg.tar.zst
|
||||||
|
# → breadlock-<ver>-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 -k releases@breadway.dev breadway.db.tar.gz *.pkg.tar.zst
|
||||||
```
|
```
|
||||||
|
|
||||||
`repo-add -s` writes `breadway.db.tar.gz.sig` (and the `.files` pair).
|
`repo-add -s` writes `breadway.db.tar.gz.sig` (and the `.files` pair).
|
||||||
Pacman fetches `<section>.db` + `<section>.db.sig` from `Server`.
|
Pacman fetches `<section>.db` + `<section>.db.sig` from `Server`.
|
||||||
|
|
||||||
Package signatures are separate from the database signature. Detach-sign
|
## Dispatch the workflow
|
||||||
each `.pkg.tar.zst` as a **binary** sidecar (pacman wants `.sig`, not
|
|
||||||
armoured `.asc`):
|
Forgejo UI: **Actions → "Publish signed [breadway] repo" → Run workflow**.
|
||||||
|
Select `main`.
|
||||||
|
|
||||||
|
API (`workflow_dispatch`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gpg --batch --yes --local-user releases@breadway.dev \
|
curl -fsS -X POST \
|
||||||
--detach-sign breadlock-<ver>-1-x86_64.pkg.tar.zst
|
-H "Authorization: token ${RELEASE_TOKEN}" \
|
||||||
# → breadlock-<ver>-1-x86_64.pkg.tar.zst.sig
|
-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` sidecar
|
||||||
|
|
||||||
[`breadlock` `package.yml`](https://git.breadway.dev/Breadway/breadlock/src/branch/main/.forgejo/workflows/package.yml)
|
[`breadlock` `package.yml`](https://git.breadway.dev/Breadway/breadlock/src/branch/main/.forgejo/workflows/package.yml)
|
||||||
already `makepkg`s and PUTs the archive at Forgejo's registry. When the
|
still `makepkg`s and PUTs the archive at Forgejo's registry. That path
|
||||||
signed repo exists, that job can also emit the sidecar and publish both
|
stays; Never installs keep working. The signed tree is rebuilt by the bos
|
||||||
files to `dl.breadway.dev/arch`:
|
workflow above (registry fetch + sign + `repo-add -s`), not by writing
|
||||||
|
`/srv` from breadlock's container.
|
||||||
```sh
|
|
||||||
PKG=$(find packaging/arch -name '*.pkg.tar.zst' | head -1)
|
|
||||||
printf '%s\n' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
|
||||||
gpg --batch --yes --local-user releases@breadway.dev --detach-sign "$PKG"
|
|
||||||
# upload "$PKG" and "${PKG}.sig" to dl.breadway.dev/arch/x86_64/
|
|
||||||
# then repo-add -s as above
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep publishing to Forgejo until installs have been switched. The ISO
|
|
||||||
section stays `SigLevel = Never` until the signed tree is live.
|
|
||||||
|
|
||||||
## After the signed repo exists
|
## 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`).
|
1. Import `KEYS.asc` into the ISO keyring (`pacman-key --add` + `--lsign-key`).
|
||||||
2. Point `[breadway]` `Server` at `https://dl.breadway.dev/arch/$arch`.
|
2. Point `[breadway]` `Server` at `https://dl.breadway.dev/arch/$arch`.
|
||||||
3. Only then flip that section to `SigLevel = Required`.
|
3. Only then flip that section to `SigLevel = Required`.
|
||||||
|
|
||||||
Do not do those three steps against Forgejo's registry. See
|
Do not do those three steps against Forgejo's registry. See
|
||||||
`iso/pacman.conf` and `iso/airootfs/etc/pacman.conf`.
|
`iso/pacman.conf` and `iso/airootfs/etc/pacman.conf`. This tree does
|
||||||
|
**not** change either file.
|
||||||
|
|
|
||||||
2
iso/airootfs/etc/bakery/config.toml
Normal file
2
iso/airootfs/etc/bakery/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Bakery desktop apps live under /usr/local so they ride snapper @ snapshots.
|
||||||
|
prefix = "/usr/local"
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
---
|
---
|
||||||
|
# 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
|
backend: pacman
|
||||||
|
|
||||||
options:
|
skip_if_no_internet: true
|
||||||
- update_db: true
|
update_db: true
|
||||||
|
ignore_update_db_error: true
|
||||||
|
update_system: false
|
||||||
|
|
||||||
operations:
|
pacman:
|
||||||
- try_install:
|
num_retries: 1
|
||||||
- pipewire-pulse
|
disable_download_timeout: false
|
||||||
- pipewire-alsa
|
needed_only: true
|
||||||
|
|
||||||
|
operations: []
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,19 @@ showSupportUrl: false
|
||||||
showKnownIssuesUrl: false
|
showKnownIssuesUrl: false
|
||||||
showReleaseNotesUrl: 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:
|
requirements:
|
||||||
requiredStorage: 20
|
requiredStorage: 20
|
||||||
requiredRam: 2.0
|
requiredRam: 2.0
|
||||||
checkInternet: true
|
internetCheckUrl: "https://breadway.dev"
|
||||||
checkPower: true
|
check:
|
||||||
internetCheckUrl: "https://archlinux.org"
|
- storage
|
||||||
|
- ram
|
||||||
|
- power
|
||||||
|
- internet
|
||||||
|
- root
|
||||||
|
required:
|
||||||
|
- storage
|
||||||
|
- ram
|
||||||
|
- root
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
# Best-effort: do NOT use `set -e`; a single failure here must not abort the rest.
|
# Best-effort: do NOT use `set -e`; a single failure here must not abort the rest.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)"
|
|
||||||
|
|
||||||
# Whether Calamares encrypted the root partition (LUKS) — checked once here,
|
# Whether Calamares encrypted the root partition (LUKS) — checked once here,
|
||||||
# used below to conditionally wire mkinitcpio's encrypt hook and GRUB's
|
# used below to conditionally wire mkinitcpio's encrypt hook and GRUB's
|
||||||
# cryptodisk support. `lsblk TYPE` reports "crypt" for a cryptsetup-opened
|
# cryptodisk support. `lsblk TYPE` reports "crypt" for a cryptsetup-opened
|
||||||
|
|
@ -33,6 +31,14 @@ rm -f /usr/local/bin/bos-live-setup /usr/local/bin/bos-launch-calamares
|
||||||
rm -f /etc/sudoers.d/99-bos-live
|
rm -f /etc/sudoers.d/99-bos-live
|
||||||
userdel -r liveuser 2>/dev/null || true
|
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
|
# unpackfs copies the entire live squashfs onto the target. Remove live-only
|
||||||
# packages (Calamares + archiso boot chain + memtest/EFI-shell payloads) so
|
# packages (Calamares + archiso boot chain + memtest/EFI-shell payloads) so
|
||||||
# they do not stay on disk forever. pacman -Rs (not -Rns) keeps /etc configs
|
# they do not stay on disk forever. pacman -Rs (not -Rns) keeps /etc configs
|
||||||
|
|
@ -425,10 +431,20 @@ if command -v ufw &>/dev/null; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The whole bread ecosystem (bakery, bread, breadbar, breadbox, breadcrumbs,
|
# The whole bread ecosystem (bakery, bread, breadbar, breadbox, breadcrumbs,
|
||||||
# breadpad, bos-settings, breadhelp, ...) is bakery-managed, not pacman: the
|
# breadpad, bos-settings, breadhelp, ...) is bakery-managed, not pacman:
|
||||||
# binaries and bakery manifest live in /etc/skel/.local (baked in at ISO
|
# binaries, share/data, and user units are baked into /usr/local and
|
||||||
# build time from iso/bread-lockfile.toml) and are copied into the user's
|
# /usr/lib/systemd/user (system prefix). Per-user bakery state (installed.json
|
||||||
|
# + index cache) is seeded from /etc/skel/.local and copied into the user's
|
||||||
# home below, so the install works fully offline with no DNS for bakery.
|
# home below, so the install works fully offline with no DNS for bakery.
|
||||||
|
#
|
||||||
|
# systemd --user units in /usr/lib/systemd/user are not enabled for new
|
||||||
|
# accounts unless enabled --global (or the user enables them). Do that here
|
||||||
|
# so a later `useradd -m` starts breadd / breadbox-sync / breadclipd /
|
||||||
|
# breadcrumbs / breadmill on first login. Safe if the helper is missing.
|
||||||
|
if [[ -x /usr/local/bin/bos-enable-bakery-user-units ]]; then
|
||||||
|
/usr/local/bin/bos-enable-bakery-user-units \
|
||||||
|
|| echo "WARN: enabling bakery user units globally failed"
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Deploy dotfiles + the bakery bread ecosystem into the user's home (Calamares
|
# Deploy dotfiles + the bakery bread ecosystem into the user's home (Calamares
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,6 @@ sequence:
|
||||||
- users
|
- users
|
||||||
- networkcfg
|
- networkcfg
|
||||||
- hwclock
|
- 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
|
# archiso strips the kernel from the squashfs; stage it, drop the archiso
|
||||||
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
|
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
|
||||||
- shellprocess@kernel
|
- shellprocess@kernel
|
||||||
|
|
@ -57,6 +51,12 @@ sequence:
|
||||||
# BOS finalization: GRUB install + cleanup + snapper + services + dotfiles.
|
# BOS finalization: GRUB install + cleanup + snapper + services + dotfiles.
|
||||||
# All fast, and runs after initcpio so /boot has the kernel + initramfs.
|
# All fast, and runs after initcpio so /boot has the kernel + initramfs.
|
||||||
- shellprocess
|
- 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
|
- umount
|
||||||
- show:
|
- show:
|
||||||
- finished
|
- finished
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,8 @@ GROUP=users
|
||||||
HOME=/home
|
HOME=/home
|
||||||
INACTIVE=-1
|
INACTIVE=-1
|
||||||
EXPIRE=
|
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
|
SKEL=/etc/skel
|
||||||
CREATE_MAIL_SPOOL=no
|
CREATE_MAIL_SPOOL=no
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
# alongside BOS's own bos.desktop, and breadgreet's session picker matches by
|
# alongside BOS's own bos.desktop, and breadgreet's session picker matches by
|
||||||
# .desktop file stem — with no override it picks "hyprland.desktop" over
|
# .desktop file stem — with no override it picks "hyprland.desktop" over
|
||||||
# "bos.desktop", which skips bos-session's PATH fixup (adds ~/.local/bin for
|
# "bos.desktop", which skips bos-session's PATH fixup (adds ~/.local/bin for
|
||||||
# the bakery bread apps; greetd starts no login shell, so /etc/profile.d is
|
# per-user tools; bakery apps are in /usr/local/bin). greetd starts no login
|
||||||
# never sourced any other way). Confirmed via breadgreet's own test suite
|
# shell, so /etc/profile.d is never sourced any other way. Confirmed via
|
||||||
|
# breadgreet's own test suite
|
||||||
# (sessions.rs: discover_prefers_configured_default_over_first_entry).
|
# (sessions.rs: discover_prefers_configured_default_over_first_entry).
|
||||||
|
|
||||||
[sessions]
|
[sessions]
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ Include = /etc/pacman.d/mirrorlist
|
||||||
# Breadway custom repo — breadlock plus AUR republishes the ISO needs
|
# Breadway custom repo — breadlock plus AUR republishes the ISO needs
|
||||||
# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin,
|
# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin,
|
||||||
# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp
|
# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp
|
||||||
# are NOT here; they are bakery-baked into /etc/skel at ISO build time.
|
# are NOT here; they are bakery-baked into /usr/local at ISO build time.
|
||||||
#
|
#
|
||||||
# Packages are published to the Forgejo Arch registry (group "os") by the
|
# Packages are published to the Forgejo Arch registry (group "os") by the
|
||||||
# .forgejo/workflows/*.yml workflows in this repo (and breadlock's).
|
# .forgejo/workflows/*.yml workflows in this repo (and breadlock's).
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Put the per-user bakery bin dir on PATH. The bread ecosystem (breadd, breadbar,
|
# Keep ~/.local/bin on PATH for per-user tools. Arch already includes
|
||||||
# breadbox, …) is installed there by bakery, and the Hyprland session launches
|
# /usr/local/bin (where bakery desktop apps live on BOS). The Hyprland
|
||||||
# them via `exec-once`, which resolves against the PATH it inherits from the
|
# session resolves exec-once against the PATH it inherits from the login
|
||||||
# login shell. Arch's stock /etc/profile does not add ~/.local/bin, so do it here
|
# shell; Arch's stock /etc/profile does not add ~/.local/bin, so do it
|
||||||
# for every login shell (live user and installed user alike).
|
# here for every login shell (live user and installed user alike).
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*":$HOME/.local/bin:"*) ;;
|
*":$HOME/.local/bin:"*) ;;
|
||||||
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
{ "command": "breadbar", "label": "Bar (breadbar)", "enabled": true },
|
{ "command": "breadbar", "label": "Bar (breadbar)", "enabled": true },
|
||||||
{ "command": "hypridle", "label": "Idle / lock daemon (hypridle)", "enabled": true },
|
{ "command": "hypridle", "label": "Idle / lock daemon (hypridle)", "enabled": true },
|
||||||
{ "command": "bos-netcheck", "label": "Network connectivity check", "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": "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 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 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 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 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 breadsearch >/dev/null && exec breadsearch listen'", "label": "Search command bus (breadsearch listen)", "enabled": true },
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,17 @@ hl.env("SDL_VIDEODRIVER", "wayland")
|
||||||
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
|
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
|
||||||
hl.env("_JAVA_AWT_WM_NONREPARENTING", "1")
|
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
|
-- kitty sets its own background_opacity (see kitty.conf), so the global blur
|
||||||
-- above blurs behind the terminal while keeping text fully opaque.
|
-- above blurs behind the terminal while keeping text fully opaque.
|
||||||
|
|
||||||
|
|
@ -124,9 +135,12 @@ hl.on("hyprland.start", function()
|
||||||
"gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Ice",
|
"gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Ice",
|
||||||
"gsettings set org.gnome.desktop.interface cursor-size 24",
|
"gsettings set org.gnome.desktop.interface cursor-size 24",
|
||||||
-- Clipboard history is breadclipd, a bakery-managed systemd --user
|
-- Clipboard history is breadclipd, a bakery-managed systemd --user
|
||||||
-- service (auto-started via skel — see build-local.sh's service bake)
|
-- service (auto-started from /usr/lib/systemd/user — see
|
||||||
-- rather than an exec-once here.
|
-- build-local.sh's service bake) rather than an exec-once here.
|
||||||
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
-- 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'",
|
||||||
"awww-daemon",
|
"awww-daemon",
|
||||||
-- Set the default wallpaper once the daemon is up (retry until ready).
|
-- Set the default wallpaper once the daemon is up (retry until ready).
|
||||||
-- Raw `awww img`, NOT `breadpaper set` — breadpaper set also runs real
|
-- Raw `awww img`, NOT `breadpaper set` — breadpaper set also runs real
|
||||||
|
|
@ -140,8 +154,9 @@ hl.on("hyprland.start", function()
|
||||||
-- breadpaper reads) is baked into skel too, right beside colors.json.
|
-- breadpaper reads) is baked into skel too, right beside colors.json.
|
||||||
-- pywal only runs for real once the user picks a wallpaper themselves.
|
-- pywal only runs for real once the user picks a wallpaper themselves.
|
||||||
[[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']],
|
[[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']],
|
||||||
-- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service,
|
-- breadd runs as a systemd user service (/usr/lib/systemd/user/breadd.service,
|
||||||
-- enabled in skel). It autostarts at login but before Hyprland exists, so
|
-- enabled --global so every account starts it). It autostarts at login
|
||||||
|
-- but before Hyprland exists, so
|
||||||
-- push the compositor's Wayland env into the user manager and restart breadd
|
-- push the compositor's Wayland env into the user manager and restart breadd
|
||||||
-- to pick it up — that's how it gets HYPRLAND_INSTANCE_SIGNATURE to talk to Hyprland.
|
-- to pick it up — that's how it gets HYPRLAND_INSTANCE_SIGNATURE to talk to Hyprland.
|
||||||
"dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE",
|
"dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE",
|
||||||
|
|
@ -162,7 +177,8 @@ hl.on("hyprland.start", function()
|
||||||
|
|
||||||
-- breadbox-sync is a Type=oneshot systemd --user service
|
-- breadbox-sync is a Type=oneshot systemd --user service
|
||||||
-- (WantedBy=default.target, no Hyprland IPC dependency) — it already
|
-- (WantedBy=default.target, no Hyprland IPC dependency) — it already
|
||||||
-- runs on login via the unit baked into skel, independent of this list.
|
-- runs on login via the unit baked into /usr/lib/systemd/user,
|
||||||
|
-- independent of this list.
|
||||||
local ok, extra = pcall(function()
|
local ok, extra = pcall(function()
|
||||||
return dofile(script_dir .. "system/autostart.lua")()
|
return dofile(script_dir .. "system/autostart.lua")()
|
||||||
end)
|
end)
|
||||||
|
|
@ -174,9 +190,11 @@ hl.on("hyprland.start", function()
|
||||||
"breadbar",
|
"breadbar",
|
||||||
"hypridle",
|
"hypridle",
|
||||||
"bos-netcheck",
|
"bos-netcheck",
|
||||||
|
"bash -c 'command -v bos-first-boot >/dev/null && exec bos-first-boot'",
|
||||||
"breadhelp --autostart",
|
"breadhelp --autostart",
|
||||||
"bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'",
|
"bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'",
|
||||||
"bash -c 'command -v breadshot >/dev/null && exec breadshot 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 breadbox >/dev/null && exec breadbox listen'",
|
||||||
"bash -c 'command -v breadhelp >/dev/null && exec breadhelp 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 breadsearch >/dev/null && exec breadsearch listen'",
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ local DEFAULT_EXTRA = {
|
||||||
{ command = "breadbar", enabled = true },
|
{ command = "breadbar", enabled = true },
|
||||||
{ command = "hypridle", enabled = true },
|
{ command = "hypridle", enabled = true },
|
||||||
{ command = "bos-netcheck", 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 = "breadhelp --autostart", enabled = true },
|
||||||
{ command = "bash -c 'command -v breadpaper >/dev/null && exec breadpaper listen'", 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 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 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 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 breadsearch >/dev/null && exec breadsearch listen'", enabled = true },
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ Description=Bread Runtime Daemon
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
# %h = the user's home — works for any account created from this skel.
|
# System-prefix bakery install — same path for every account.
|
||||||
ExecStart=%h/.local/bin/breadd
|
ExecStart=/usr/local/bin/breadd
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
UMask=0077
|
UMask=0077
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ alias alt-install='yay -S'
|
||||||
alias alt-uninstall='yay -R'
|
alias alt-uninstall='yay -R'
|
||||||
alias alt-srchpkg='yay -Ss'
|
alias alt-srchpkg='yay -Ss'
|
||||||
|
|
||||||
# ~/.local/bin holds the bread* binaries baked in at build time.
|
# Per-user tools. Bakery desktop apps live in /usr/local/bin (already on PATH).
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
# Powerlevel10k prompt configuration.
|
# Powerlevel10k prompt configuration.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/lib/systemd/user/breadd.service
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 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
|
||||||
90
iso/airootfs/usr/local/bin/bos-enable-bakery-user-units
Executable file
90
iso/airootfs/usr/local/bin/bos-enable-bakery-user-units
Executable file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Enable bakery systemd --user units for every account (current and future).
|
||||||
|
#
|
||||||
|
# `systemctl --global enable` writes /etc/systemd/user/<target>.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
|
||||||
187
iso/airootfs/usr/local/bin/bos-first-boot
Executable file
187
iso/airootfs/usr/local/bin/bos-first-boot
Executable file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#!/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" <<EOF
|
||||||
|
{
|
||||||
|
"detected": true,
|
||||||
|
"pci": "$(json_escape "$nvidia_pci")",
|
||||||
|
"driver_on_iso": false,
|
||||||
|
"auto_install": false,
|
||||||
|
"message": "NVIDIA GPU detected. The proprietary driver is not on the ISO.",
|
||||||
|
"offered_at": "$(iso_now)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
notify "NVIDIA GPU detected. The proprietary driver is not on the ISO — open BOS Settings later. Nothing was installed." normal
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# VM without GL (no /dev/dri). Notify only when both are true.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
virt="none"
|
||||||
|
if command -v systemd-detect-virt >/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" <<EOF
|
||||||
|
{
|
||||||
|
"virt": "$(json_escape "$virt")",
|
||||||
|
"gl": false,
|
||||||
|
"dri": false,
|
||||||
|
"noted_at": "$(iso_now)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
notify "This looks like a virtual machine without hardware GL. Hyprland may use software rendering." normal
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# HiDPI — hint file for bos-settings. Do not rewrite monitors.json.
|
||||||
|
# scale > 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
|
||||||
|
|
@ -7,9 +7,17 @@
|
||||||
# bos-launch-calamares). Runs once at boot, before the tty1 autologin getty.
|
# bos-launch-calamares). Runs once at boot, before the tty1 autologin getty.
|
||||||
set -e
|
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
|
# useradd -m copies /etc/skel, so the live user gets the real BOS desktop
|
||||||
# (breadd + breadbar + breadbox + keybinds) — proper live-media functionality,
|
# (hypr + bread config + bakery state) — proper live-media functionality,
|
||||||
# not an installer kiosk.
|
# not an installer kiosk. Binaries are /usr/local, not skel.
|
||||||
if ! id liveuser &>/dev/null; then
|
if ! id liveuser &>/dev/null; then
|
||||||
useradd -m -s /usr/bin/zsh liveuser
|
useradd -m -s /usr/bin/zsh liveuser
|
||||||
for g in wheel video input audio storage power; do
|
for g in wheel video input audio storage power; do
|
||||||
|
|
|
||||||
150
iso/airootfs/usr/local/bin/bos-nvidia-setup
Executable file
150
iso/airootfs/usr/local/bin/bos-nvidia-setup
Executable file
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/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
|
||||||
598
iso/airootfs/usr/local/bin/bos-rescue
Executable file
598
iso/airootfs/usr/local/bin/bos-rescue
Executable file
|
|
@ -0,0 +1,598 @@
|
||||||
|
#!/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: DEVICE<TAB>KIND<TAB>PRETTY 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 <disk-hosting-root>
|
||||||
|
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 "$@"
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
# BOS graphical session launcher, run by greetd on the INSTALLED system after
|
# BOS graphical session launcher, run by greetd on the INSTALLED system after
|
||||||
# the user authenticates (see /etc/greetd/config.toml).
|
# the user authenticates (see /etc/greetd/config.toml).
|
||||||
#
|
#
|
||||||
# greetd does not start a login shell, so /etc/profile.d is never sourced — which
|
# greetd does not start a login shell, so /etc/profile.d is never sourced.
|
||||||
# means ~/.local/bin (where bakery installs the bread ecosystem: breadd, breadbar,
|
# Bakery desktop apps live in /usr/local/bin (already on Arch PATH). Source
|
||||||
# breadbox-sync, …) would be missing from PATH and the Hyprland `exec-once`
|
# the login profile here so ~/.local/bin (per-user tools) is also on PATH,
|
||||||
# launches would fail. Source the login profile here so PATH is correct, set the
|
# set the Wayland session hints, then hand off to Hyprland.
|
||||||
# Wayland session hints, then hand off to Hyprland.
|
|
||||||
#
|
#
|
||||||
# Launched via start-hyprland (ships with the hyprland package) rather than the
|
# Launched via start-hyprland (ships with the hyprland package) rather than the
|
||||||
# raw Hyprland binary — Hyprland upstream no longer recommends exec'ing it
|
# raw Hyprland binary — Hyprland upstream no longer recommends exec'ing it
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,12 @@
|
||||||
# or other bakery desktop apps. Every transaction is
|
# or other bakery desktop apps. Every transaction is
|
||||||
# snapshotted by snap-pac; recover via the GRUB "snapshots"
|
# snapshotted by snap-pac; recover via the GRUB "snapshots"
|
||||||
# submenu (grub-btrfs), not `snapper rollback`.
|
# submenu (grub-btrfs), not `snapper rollback`.
|
||||||
# 2. bakery — the bread ecosystem apps in ~/.local/bin (whatever `bakery list`
|
# 2. bakery — the bread ecosystem apps in /usr/local (whatever `bakery list`
|
||||||
# reports as installed — bakery, bread, breadbar, breadbox,
|
# reports as installed — bakery, bread, breadbar, breadbox,
|
||||||
# breadcrumbs, breadpad, breadman, bread-theme, breadpaper,
|
# breadcrumbs, breadpad, breadman, bread-theme, breadpaper,
|
||||||
# breadmon, breadsearch, breadclip, breadshot, bos-settings,
|
# breadmon, breadsearch, breadclip, breadshot, bos-settings,
|
||||||
# breadhelp, ...).
|
# breadhelp, ...). Those bits live on @ and ride snapper
|
||||||
|
# root snapshots; recover via grub-btrfs, not `snapper rollback`.
|
||||||
#
|
#
|
||||||
# Best-effort: a failure in one channel doesn't abort the other.
|
# Best-effort: a failure in one channel doesn't abort the other.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
@ -20,12 +21,11 @@ set -uo pipefail
|
||||||
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
|
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
|
||||||
|
|
||||||
# Timed snapper pre snapshot before either channel. snap-pac already
|
# Timed snapper pre snapshot before either channel. snap-pac already
|
||||||
# snapshots root around pacman; bakery writes ~/.local/bin ($HOME / @home),
|
# snapshots root around pacman; bakery now writes /usr/local (on @), so
|
||||||
# which is outside that root snapshot. This extra snapshot is still
|
# that root snapshot includes the desktop apps. This extra snapshot is
|
||||||
# best-effort and covers bakery $HOME updates as well as possible — a
|
# still best-effort — a home config if the installer created one (user
|
||||||
# home config if the installer created one, otherwise the root timeline
|
# bakery state), plus a root timeline around the whole update. Never
|
||||||
# around the whole update. Never fail the update if snapper is missing
|
# fail the update if snapper is missing or the create errors.
|
||||||
# or the create errors.
|
|
||||||
if command -v snapper >/dev/null; then
|
if command -v snapper >/dev/null; then
|
||||||
if snapper -c home list >/dev/null 2>&1; then
|
if snapper -c home list >/dev/null 2>&1; then
|
||||||
snapper -c home create -t pre -c number \
|
snapper -c home create -t pre -c number \
|
||||||
|
|
@ -47,10 +47,22 @@ fi
|
||||||
echo
|
echo
|
||||||
bold "==> Bread ecosystem (bakery update --all)"
|
bold "==> Bread ecosystem (bakery update --all)"
|
||||||
if command -v bakery >/dev/null; then
|
if command -v bakery >/dev/null; then
|
||||||
bakery update --all || echo "WARN: bakery update failed"
|
# /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
|
||||||
else
|
else
|
||||||
echo "bakery not found; skipping"
|
echo "bakery not found; skipping"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
bold "==> BOS is up to date."
|
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=@)."
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Bakery binaries baked into the live/installed skel.
|
# Bakery binaries baked into the live/installed image at /usr/local.
|
||||||
#
|
#
|
||||||
# build-local.sh and CI (scripts/ci-stage-bakery.py) read this file. A missing
|
# build-local.sh and CI (scripts/ci-stage-bakery.py) read this file. A missing
|
||||||
# *required* binary fails the bake: a hollow ISO is worse than a failed build.
|
# *required* binary fails the bake: a hollow ISO is worse than a failed build.
|
||||||
# optional_bins are baked when the verified stable index publishes them, and
|
# optional_bins are baked when the verified stable index publishes them, and
|
||||||
# skipped with a warning when it does not (today: bread 0.8.0 has no
|
# skipped with a warning when it does not. bread 0.8.0 ships bread-emit and
|
||||||
# bread-emit / bread-module-host).
|
# bread-module-host, so those are required_bins.
|
||||||
#
|
#
|
||||||
# A flat `bins` list is still accepted and treated as required_bins.
|
# A flat `bins` list is still accepted and treated as required_bins.
|
||||||
#
|
#
|
||||||
|
|
@ -23,6 +23,8 @@ required_bins = [
|
||||||
"bakery",
|
"bakery",
|
||||||
"bread",
|
"bread",
|
||||||
"breadd",
|
"breadd",
|
||||||
|
"bread-emit",
|
||||||
|
"bread-module-host",
|
||||||
"breadman",
|
"breadman",
|
||||||
"breadbar",
|
"breadbar",
|
||||||
"breadbox",
|
"breadbox",
|
||||||
|
|
@ -41,30 +43,21 @@ required_bins = [
|
||||||
"breadhelp",
|
"breadhelp",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Bake if the verified index publishes them; do not fail the ISO if absent.
|
|
||||||
optional_bins = [
|
|
||||||
"bread-emit",
|
|
||||||
"bread-module-host",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Package name → version. Must exist at dl.breadway.dev/<pkg>/<ver>/ and
|
# Package name → version. Must exist at dl.breadway.dev/<pkg>/<ver>/ and
|
||||||
# should match the signed index so CI can verify sha256. Newer git tags
|
# should match the signed index so CI can verify sha256.
|
||||||
# that bakery release CI did not publish (breadpad 0.5.1, breadmon 0.1.3,
|
|
||||||
# breadclip 0.2.2, breadshot 0.1.2, breadhelp 0.2.4, breadcrumbs 2.1.7)
|
|
||||||
# stay off this list until those artifacts exist.
|
|
||||||
# [[pin]] { package, version } is accepted as well and merged (conflict = bake error).
|
# [[pin]] { package, version } is accepted as well and merged (conflict = bake error).
|
||||||
[versions]
|
[versions]
|
||||||
bakery = "0.7.2"
|
bakery = "0.7.4"
|
||||||
bread = "0.8.0"
|
bread = "0.8.0"
|
||||||
bread-theme = "0.7.2"
|
bread-theme = "0.7.4"
|
||||||
breadbar = "0.3.1"
|
breadbar = "0.3.2"
|
||||||
breadbox = "0.3.1"
|
breadbox = "0.3.2"
|
||||||
breadcrumbs = "2.1.6"
|
breadcrumbs = "2.1.8"
|
||||||
breadpad = "0.5.0"
|
breadpad = "0.5.2"
|
||||||
breadpaper = "0.1.12"
|
breadpaper = "0.1.13"
|
||||||
breadmon = "0.1.2"
|
breadmon = "0.1.4"
|
||||||
breadsearch = "0.3.1"
|
breadsearch = "0.3.2"
|
||||||
breadclip = "0.1.1"
|
breadclip = "0.2.3"
|
||||||
breadshot = "0.1.1"
|
breadshot = "0.1.3"
|
||||||
bos-settings = "0.8.0"
|
bos-settings = "0.8.1"
|
||||||
breadhelp = "0.2.3"
|
breadhelp = "0.2.5"
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ snapper
|
||||||
snap-pac
|
snap-pac
|
||||||
grub-btrfs
|
grub-btrfs
|
||||||
inotify-tools
|
inotify-tools
|
||||||
|
# Home backup (Settings → Backup). Snapper is root (`@`) only; restic covers $HOME.
|
||||||
|
restic
|
||||||
|
|
||||||
# Wayland / Hyprland
|
# Wayland / Hyprland
|
||||||
hyprland
|
hyprland
|
||||||
|
|
@ -206,10 +208,10 @@ yay-bin
|
||||||
# /etc/pam.d/breadlock). Everything else — bakery, bread/breadd/bread-emit/
|
# /etc/pam.d/breadlock). Everything else — bakery, bread/breadd/bread-emit/
|
||||||
# bread-module-host, breadbar, breadbox, breadcrumbs, breadpad, breadpaper,
|
# bread-module-host, breadbar, breadbox, breadcrumbs, breadpad, breadpaper,
|
||||||
# bread-theme, breadmon, breadsearch, breadclip, breadshot, bos-settings,
|
# bread-theme, breadmon, breadsearch, breadclip, breadshot, bos-settings,
|
||||||
# breadhelp — is bakery-managed and baked into /etc/skel/.local at ISO build
|
# breadhelp — is bakery-managed and baked into /usr/local at ISO build
|
||||||
# time from iso/bread-lockfile.toml (see build-local.sh). breadcast and
|
# time from iso/bread-lockfile.toml (see build-local.sh). breadcast and
|
||||||
# breadarr are not shipped. bos-settings/breadhelp desktop entries are
|
# breadarr are not shipped. bos-settings/breadhelp desktop entries are
|
||||||
# committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime
|
# also committed under iso/airootfs/etc/skel/.local/share/applications/. Runtime
|
||||||
# deps stay listed even though no bread package depends on them via pacman
|
# deps stay listed even though no bread package depends on them via pacman
|
||||||
# (gtk4, gtk4-layer-shell, webkit2gtk-4.1, iw, libpulse, librsvg, …).
|
# (gtk4, gtk4-layer-shell, webkit2gtk-4.1, iw, libpulse, librsvg, …).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Include = /etc/pacman.d/mirrorlist
|
||||||
# Breadway custom repo — breadlock plus AUR republishes the ISO needs
|
# Breadway custom repo — breadlock plus AUR republishes the ISO needs
|
||||||
# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin,
|
# (calamares, zen-browser-bin, bibata-cursor-theme-bin, yay-bin,
|
||||||
# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp
|
# zsh-theme-powerlevel10k). bakery / breadbar / bos-settings / breadhelp
|
||||||
# are NOT here; they are bakery-baked into /etc/skel at ISO build time.
|
# are NOT here; they are bakery-baked into /usr/local at ISO build time.
|
||||||
#
|
#
|
||||||
# Packages are published to the Forgejo Arch registry (group "os") by the
|
# Packages are published to the Forgejo Arch registry (group "os") by the
|
||||||
# .forgejo/workflows/*.yml workflows in this repo (and breadlock's).
|
# .forgejo/workflows/*.yml workflows in this repo (and breadlock's).
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,8 @@ file_permissions=(
|
||||||
["/usr/local/bin/bos-session"]="0:0:755"
|
["/usr/local/bin/bos-session"]="0:0:755"
|
||||||
["/usr/local/bin/bos-netcheck"]="0:0:755"
|
["/usr/local/bin/bos-netcheck"]="0:0:755"
|
||||||
["/usr/local/bin/bos-update"]="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"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
307
scripts/ci-publish-signed-repo.sh
Executable file
307
scripts/ci-publish-signed-repo.sh
Executable file
|
|
@ -0,0 +1,307 @@
|
||||||
|
#!/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"
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Read-only checks that a builder home (and optionally a staged skel) has
|
# Read-only checks that a builder home (and optionally a staged image) has
|
||||||
# everything build-local.sh needs before mkarchiso. Exit non-zero on failure.
|
# everything build-local.sh needs before mkarchiso. Exit non-zero on failure.
|
||||||
#
|
#
|
||||||
|
# Builder home stays user-layout (~/.local). The image is system-prefix
|
||||||
|
# /usr/local; pass SKEL and/or AIROOTFS to check those destinations.
|
||||||
|
#
|
||||||
# LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh
|
# LAPTOP_HOME=/build-home ./scripts/ci-verify-bake.sh
|
||||||
# SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh
|
# SKEL=/tmp/bos-iso-stage/airootfs/etc/skel ./scripts/ci-verify-bake.sh
|
||||||
|
# AIROOTFS=/tmp/bos-iso-stage/airootfs ./scripts/ci-verify-bake.sh
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
REPO="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}"
|
LOCKFILE="${LOCKFILE:-$REPO/iso/bread-lockfile.toml}"
|
||||||
LAPTOP_HOME="${LAPTOP_HOME:-/build-home}"
|
LAPTOP_HOME="${LAPTOP_HOME:-/build-home}"
|
||||||
SKEL="${SKEL:-}"
|
SKEL="${SKEL:-}"
|
||||||
|
AIROOTFS="${AIROOTFS:-}"
|
||||||
|
|
||||||
pass=0
|
pass=0
|
||||||
fail=0
|
fail=0
|
||||||
|
|
@ -42,6 +47,17 @@ PY
|
||||||
|
|
||||||
echo "== lockfile $LOCKFILE =="
|
echo "== lockfile $LOCKFILE =="
|
||||||
echo " ${#REQUIRED_BINS[@]} required, ${#OPTIONAL_BINS[@]} optional"
|
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 =="
|
echo "== builder home $LAPTOP_HOME =="
|
||||||
|
|
||||||
check_exec() {
|
check_exec() {
|
||||||
|
|
@ -106,21 +122,108 @@ else
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$SKEL" ]]; then
|
if [[ -n "$SKEL" && -z "$AIROOTFS" ]]; then
|
||||||
echo "== staged skel $SKEL =="
|
if [[ -d "$SKEL/usr/local/bin" ]]; then
|
||||||
for b in "${REQUIRED_BINS[@]}"; do
|
AIROOTFS="$SKEL"
|
||||||
check_exec "$SKEL/.local/bin/$b" "skel required bin $b"
|
SKEL="$AIROOTFS/etc/skel"
|
||||||
done
|
elif [[ -d "$SKEL/../../usr/local" ]]; then
|
||||||
check_dir "$SKEL/.local/share/breadhelp/content" "skel breadhelp content"
|
AIROOTFS="$(cd "$SKEL/../.." && pwd)"
|
||||||
check_file "$SKEL/.cache/bakery/index.json" "skel bakery index cache"
|
fi
|
||||||
for unit in "${UNITS[@]}"; do
|
elif [[ -n "$AIROOTFS" && -z "$SKEL" ]]; then
|
||||||
[[ -n "$unit" ]] || continue
|
SKEL="$AIROOTFS/etc/skel"
|
||||||
if [[ -f "$SKEL/.config/systemd/user/$unit" ]]; then
|
fi
|
||||||
ok "skel unit $unit"
|
|
||||||
|
if [[ -n "$AIROOTFS" || -n "$SKEL" ]]; then
|
||||||
|
if [[ -n "$AIROOTFS" ]]; then
|
||||||
|
echo "== staged image $AIROOTFS =="
|
||||||
|
check_file "$AIROOTFS/etc/bakery/config.toml" "bakery prefix config"
|
||||||
|
if [[ -f "$AIROOTFS/etc/bakery/config.toml" ]] && grep -q 'prefix[[:space:]]*=[[:space:]]*"/usr/local"' "$AIROOTFS/etc/bakery/config.toml"; then
|
||||||
|
ok "bakery prefix = /usr/local"
|
||||||
else
|
else
|
||||||
bad "skel unit missing: $SKEL/.config/systemd/user/$unit"
|
bad "bakery prefix is not /usr/local in $AIROOTFS/etc/bakery/config.toml"
|
||||||
fi
|
fi
|
||||||
done
|
for b in "${REQUIRED_BINS[@]}"; do
|
||||||
|
check_exec "$AIROOTFS/usr/local/bin/$b" "image required bin $b"
|
||||||
|
done
|
||||||
|
check_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
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|
|
||||||
|
|
@ -40,22 +40,15 @@ check "grub-btrfs present" "pacman -Qq grub-btrfs"
|
||||||
|
|
||||||
echo "== enabled system services =="
|
echo "== enabled system services =="
|
||||||
for unit in NetworkManager.service greetd.service bluetooth.service tlp.service \
|
for unit in NetworkManager.service greetd.service bluetooth.service tlp.service \
|
||||||
cups.socket avahi-daemon.service ufw.service systemd-timesyncd.service; do
|
cups.socket avahi-daemon.socket ufw.service systemd-timesyncd.service; do
|
||||||
check "$unit enabled" "systemctl is-enabled $unit"
|
check "$unit enabled" "systemctl is-enabled $unit"
|
||||||
done
|
done
|
||||||
check "graphical.target is default" "[ \"\$(systemctl get-default)\" = graphical.target ]"
|
check "graphical.target is default" "[ \"\$(systemctl get-default)\" = graphical.target ]"
|
||||||
|
|
||||||
echo "== bread ecosystem on PATH =="
|
echo "== bread ecosystem on PATH =="
|
||||||
for bin in bakery bread breadd breadbar breadbox breadbox-sync breadcrumbs breadpad breadman; do
|
for bin in bakery bread breadd bread-emit bread-module-host breadbar breadbox breadbox-sync breadcrumbs breadpad breadman; do
|
||||||
check "$bin found" "command -v $bin"
|
check "$bin found" "command -v $bin"
|
||||||
done
|
done
|
||||||
for bin in bread-emit bread-module-host; do
|
|
||||||
if command -v "$bin" >/dev/null 2>&1; then
|
|
||||||
ok "$bin found"
|
|
||||||
else
|
|
||||||
note "$bin not on PATH (optional until stable bread ships it)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "== bos-settings =="
|
echo "== bos-settings =="
|
||||||
check "bos-settings installed" "command -v bos-settings"
|
check "bos-settings installed" "command -v bos-settings"
|
||||||
|
|
@ -63,15 +56,55 @@ check "bos-settings installed" "command -v bos-settings"
|
||||||
echo "== breadhelp =="
|
echo "== breadhelp =="
|
||||||
check "breadhelp installed" "command -v breadhelp"
|
check "breadhelp installed" "command -v breadhelp"
|
||||||
check "breadhelp content installed" \
|
check "breadhelp content installed" \
|
||||||
"[ -d \"\$HOME/.local/share/breadhelp/content\" ] || [ -d /etc/skel/.local/share/breadhelp/content ]"
|
"[ -d /usr/local/share/breadhelp/content ] || [ -d \"\$HOME/.local/share/breadhelp/content\" ]"
|
||||||
check "bos-netcheck present" "command -v bos-netcheck"
|
check "bos-netcheck present" "command -v bos-netcheck"
|
||||||
|
check "bos-rescue present" "command -v bos-rescue"
|
||||||
|
check "bos-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/<target>.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 =="
|
echo "== default dotfiles =="
|
||||||
check "hyprland.lua present" "[ -f \"\$HOME/.config/hypr/hyprland.lua\" ]"
|
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 "binds.json present" "[ -f \"\$HOME/.config/hypr/binds.json\" ]"
|
||||||
check "monitors.json present" "[ -f \"\$HOME/.config/hypr/monitors.json\" ]"
|
check "monitors.json present" "[ -f \"\$HOME/.config/hypr/monitors.json\" ]"
|
||||||
check "settings.json present" "[ -f \"\$HOME/.config/hypr/settings.json\" ]"
|
check "settings.json present" "[ -f \"\$HOME/.config/hypr/settings.json\" ]"
|
||||||
check "autostart.json present" "[ -f \"\$HOME/.config/hypr/autostart.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 "hypr scripts/lib present" "[ -f \"\$HOME/.config/hypr/scripts/lib/json.lua\" ]"
|
||||||
check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]"
|
check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]"
|
||||||
check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]"
|
check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue