diff --git a/docs/skills/brew-lifecycle/SKILL.md b/docs/skills/brew-lifecycle/SKILL.md index c3b1c1fea..9d4e507b3 100644 --- a/docs/skills/brew-lifecycle/SKILL.md +++ b/docs/skills/brew-lifecycle/SKILL.md @@ -1,7 +1,7 @@ --- name: brew-lifecycle version: "1.5" -last_updated: "2026-09-12" +last_updated: "2026-09-23" id: brew-lifecycle one_line_purpose: Manage OS-managed Homebrew packages and RPM/brew placement. entry_point: docs/skills/brew-lifecycle/SKILL.md @@ -178,6 +178,9 @@ not `system_files/bluefin/preinstall.d/`. See [package-set.md](references/packag - Using `arm:` / `intel:` checksum keys for a Linux cask — those keys are macOS-only and resolve to no checksum on Linux - Adding unknown keys to `/usr/share/chairlift/config.yml` — ChairLift disables the whole application on unknown page, group, or field names +- Placing a bundle Brewfile in a subdirectory of + `/usr/share/ublue-os/homebrew/` — ChairLift's `brew_bundles_group` does not + recurse, so the bundle silently never appears on the Applications page - Writing `/etc/chairlift/config.yml` from image content or setup code; that path is administrator-owned override state - Bumping a version number or manual stamp to "trigger" a brew-preinstall re-run — the service is content-addressed; edit the Brewfile and the hash change triggers it automatically @@ -208,6 +211,6 @@ After any change to `preinstall.d/` or `brew-preinstall`: | File | Contents | |---|---| -| [package-set.md](references/package-set.md) | Current 11-package default set, ChairLift managed cask, what belongs in preinstall.d, fzf/ujust bootstrap, opt-in Brewfiles, shared/ vs bluefin/ placement rule | +| [package-set.md](references/package-set.md) | Current 11-package default set, ChairLift managed cask, what belongs in preinstall.d, fzf/ujust bootstrap, opt-in Brewfiles, ChairLift brew bundles, shared/ vs bluefin/ placement rule | | [service-mechanics.md](references/service-mechanics.md) | How brew-preinstall.service works, state file format, login flow, ChairLift config ownership, long-time user removal scenario, bonedigger-report integration, merging order, path convention | | [placement-rules.md](references/placement-rules.md) | No rpm-ostree rule, what can move to brew, Homebrew 6.0 tap trust details, Starship shell initialization | diff --git a/docs/skills/brew-lifecycle/references/package-set.md b/docs/skills/brew-lifecycle/references/package-set.md index 9443ca58d..d1429374a 100644 --- a/docs/skills/brew-lifecycle/references/package-set.md +++ b/docs/skills/brew-lifecycle/references/package-set.md @@ -101,6 +101,8 @@ These live in `system_files/shared/usr/share/ublue-os/homebrew/` (not in - `ide.Brewfile` - `k8s-tools.Brewfile` - `swift.Brewfile` +- `video-wallpaper.Brewfile` — Hidamari, video wallpaper +- `wallpaper-slideshow.Brewfile` — Damask, wallpaper slideshow Bluefin-specific (in `system_files/bluefin/`): - `full-desktop.Brewfile` — GNOME Circle + community flatpaks @@ -108,6 +110,33 @@ Bluefin-specific (in `system_files/bluefin/`): These are validated by the `validate-brewfiles.yaml` CI workflow on every PR that touches `system_files/shared/usr/share/ublue-os/homebrew/**`. +### A second consumer: ChairLift `brew_bundles_group` + +`ujust bbrew` is not the only reader of this directory. ChairLift's +`applications_page.brew_bundles_group` in +`/usr/share/chairlift/config.yml` points `bundles_paths` at +`/usr/share/ublue-os/homebrew` and renders one installable bundle per +**immediate** `*.Brewfile` child, naming the bundle after the filename stem. +So a file added here also appears on ChairLift's Applications page without any +further wiring — and the filename is user-visible UI text. + +Two consequences when adding a bundle: + +- The file must be a **direct** child of the directory and carry the exact + `.Brewfile` suffix. ChairLift does not recurse, and a misplaced file is + silently absent from the UI rather than an error. +- Names must be unique and readable as a bundle label + (`wallpaper-slideshow`, not `wallpaper_slideshow_v2`). + +The bundle only covers **install**. Uninstall is ChairLift's +`flatpak_user_group` / `flatpak_system_group`; do not add an uninstall action +for a bundle, and do not invent a group to express one — an unknown group key +fails ChairLift's strict schema validation and disables the whole application. + +CI's `scripts/validate-brewfiles.sh` validator skips `flatpak` lines (it only +validates `brew` and `cask` lines), so the unit tests in `test_chairlift_config.py` +are what pin these app ids. + --- ## Brewfile scope: shared/ vs bluefin/ for all-variant packages diff --git a/docs/skills/index.json b/docs/skills/index.json index f6a940d47..d050518e6 100644 --- a/docs/skills/index.json +++ b/docs/skills/index.json @@ -68,7 +68,7 @@ ], "description": "Manage OS-managed Homebrew packages. Use when adding/removing default brew packages, moving tools between RPM and brew, or auditing image-vs-brew placement.", "version": "1.5", - "last_updated": "2026-09-12", + "last_updated": "2026-09-23", "doc_type": "procedure" }, { diff --git a/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile b/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile new file mode 100644 index 000000000..59c50c900 --- /dev/null +++ b/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile @@ -0,0 +1,6 @@ +# Video Wallpaper +# Hidamari plays a video file as the desktop background. +# Opt-in: ChairLift lists this as a bundle on its Applications page, and +# `ujust bbrew` offers it too. + +flatpak "io.github.jeffshee.Hidamari" diff --git a/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile b/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile new file mode 100644 index 000000000..531b37a03 --- /dev/null +++ b/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile @@ -0,0 +1,6 @@ +# Wallpaper Slideshow +# Damask cycles your desktop background through a folder or a featured set. +# Opt-in: ChairLift lists this as a bundle on its Applications page, and +# `ujust bbrew` offers it too. + +flatpak "app.drey.Damask" diff --git a/tests/test_chairlift_config.py b/tests/test_chairlift_config.py index 9c416e0df..e2d934ecc 100644 --- a/tests/test_chairlift_config.py +++ b/tests/test_chairlift_config.py @@ -31,6 +31,9 @@ ROOT / "system_files/shared/usr/share/ublue-os/homebrew/preinstall.d/chairlift.Brewfile" ) +#: The directory ChairLift's brew_bundles_group scans. Its image path is the +#: one in config.yml; this is the repo-side path of the same tree. +BUNDLES_DIR = ROOT / "system_files/shared/usr/share/ublue-os/homebrew" BOOTC_POLICY = ( ROOT / "system_files/shared/usr/share/polkit-1/actions" @@ -527,6 +530,72 @@ def test_bundles_paths_point_at_bluefin_brewfiles(): assert group["bundles_paths"] == ["/usr/share/ublue-os/homebrew"] +#: Bundles ChairLift renders on its Applications page, keyed by Brewfile name. +#: ChairLift derives the bundle's display name from the filename, so these +#: stems are user-visible. They must be immediate children of the directory in +#: brew_bundles_group.bundles_paths -- a nested file is never discovered -- and +#: the filename must carry the exact suffix ChairLift globs for. +EXPECTED_BUNDLES = { + "wallpaper-slideshow": "app.drey.Damask", + "video-wallpaper": "io.github.jeffshee.Hidamari", +} + + +def test_bundle_brewfiles_are_discoverable_by_chairlift(): + """Each bundle must be an immediate *.Brewfile child of bundles_paths. + + ChairLift only scans the top level of each configured path and only for + the *.Brewfile suffix, so a misplaced file is silently absent from the UI + rather than an error -- the user just never sees the bundle. Pin both + halves of that discovery contract against the configured path. + """ + configured = _load_config()["applications_page"]["brew_bundles_group"][ + "bundles_paths" + ] + assert configured == ["/usr/share/ublue-os/homebrew"], ( + "update BUNDLES_DIR if the image path in config.yml changed" + ) + image_path = configured[0].removeprefix("/usr/share/ublue-os/homebrew") + assert image_path == "", "BUNDLES_DIR assumes the path is the scan root" + + for stem in EXPECTED_BUNDLES: + bundle = BUNDLES_DIR / f"{stem}.Brewfile" + assert bundle.is_file(), f"missing bundle Brewfile: {bundle.name}" + assert bundle.parent == BUNDLES_DIR, ( + f"{bundle.name} must be an immediate child of {BUNDLES_DIR.name}/; " + "ChairLift does not recurse" + ) + assert bundle.stat().st_mode & 0o444 == 0o444, ( + f"{bundle.name} must be world-readable so every user's ChairLift " + "can read it" + ) + + +def test_bundle_brewfiles_declare_the_expected_flatpaks(): + """Pin the app ids: a typo here installs the wrong app, or none at all.""" + for stem, app_id in EXPECTED_BUNDLES.items(): + content = (BUNDLES_DIR / f"{stem}.Brewfile").read_text(encoding="utf-8") + declarations = [ + line.strip() + for line in content.splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + assert declarations == [f'flatpak "{app_id}"'], ( + f"{stem}.Brewfile must declare exactly flatpak \"{app_id}\", " + f"found {declarations}" + ) + + +def test_bundle_names_do_not_collide_with_other_discovered_brewfiles(): + """A duplicate stem would render two identically-named bundles across discoverable paths.""" + all_brewfile_stems = [] + for brewfile in ROOT.glob("system_files/**/usr/share/ublue-os/homebrew/*.Brewfile"): + all_brewfile_stems.append(brewfile.stem) + assert len(all_brewfile_stems) == len(set(all_brewfile_stems)), ( + f"duplicate bundle names across homebrew directories: {all_brewfile_stems}" + ) + + def test_help_links_point_at_bluefin(): resources = _load_config()["help_page"]["help_resources_group"] for key in ("website", "issues", "chat"):