merge: unify margine-fedora-atomic into margine-image (single repo)#272
Merged
Conversation
Establishes the complete phase 1 design: docs (goals, architecture, lab procedure, validation, known risks, personal migration, Secure Boot/TPM2, GNOME layer, declarative model, hardware/media, gaming runtime, update orchestration), draft declarations, Topgrade profile, and six read-only validation/orchestration scripts. All scripts pass bash -n and shellcheck. YAML and TOML parse cleanly. Validation report run; findings M-1 through L-4 resolved before first commit.
- Rewrite README.md with Margine identity (logo, tagline, Why Margine, What You Get table, architecture diagram, doc map) - Add CONTRIBUTING.md with repo purpose, change style, validation expectations - Add docs/README.md as a documentation map with reading order - Add docs/roadmap.md with 4-phase plan and milestones - Add docs/adr/ with four initial architecture decision records: 0001 why Silverblue not Kinoite 0002 GNOME stock in phase 1, Hyprland deferred 0003 Fedora-native boot security (not Limine/sbctl/mkinitcpio) 0004 rpm-ostree owns the base OS boundary - Update docs/00-goals.md with Margine ecosystem context and ADR/roadmap refs - Add assets/branding/ with Margine logo (wide, square, base) and ASCII art - Add files/home/ with margine-fetch script and fastfetch config for VM use
- docs/02a-custom-partitioning.md: step-by-step Anaconda custom partitioning guide with LUKS2 encryption, @root/@home/@DaTa Btrfs subvolume layout, post-install @DaTa mount via fstab, and XDG user dirs setup - declarations/margine-atomic.yaml: add btrfs_subvolumes block documenting the @root/@home/@DaTa layout with notes and phase markers - docs/README.md: link to the new partitioning guide The @DaTa subvolume separates personal data (photos, documents, media) from home config and caches, enabling independent snapshot policies without conflicting with rpm-ostree's OS deployment rollback model.
Remove redundant sections, long references list, and Mermaid diagram. Lead with logo, tagline, and a two-sentence description. Phase 1 status in a blockquote. Two focused tables: repo layout and key docs.
512 MiB fills up with multiple kernels and Secure Boot files. Recommend 2 GiB as default, 4 GiB on larger disks.
- declarations: add io.github.zen_browser.zen to default_apps - docs/06: update browser section to Zen Browser, rewrite policy notes, add Firefox as explicit fallback in Flatpak candidates table - docs/08: update browser reference in personal layer
- validate-atomic-layout: fix /usr composefs false positive; distinguish crypttab missing from crypttab unreadable - 02-install-lab: note that systemctl reboot is inhibited in GNOME session; use GNOME menu instead - 07-secure-boot-tpm2: add lab-validated TPM2 procedure with exact commands, PCR policy rationale, crypttab update, and rpm-ostree initramfs --enable step - 14-expected-behaviors: new doc covering systemd-remount-fs, mcelog, /usr composefs, GNOME reboot inhibitor, crypttab permissions, and initramfs default
Documents the key finding: TPM2 auto-unlock works on both local and stock initramfs deployments because systemd-cryptsetup discovers the LUKS2 token from the header directly. The crypttab entry and initramfs regeneration are best practice but not strictly required for basic auto-unlock.
Rewrite 02a-custom-partitioning.md with the nested-subvolume design (home/<user>/.cache, home/<user>/dev, home/<user>/scratch) that aligns the Btrfs snapshot boundary with the value boundary of the three-root home model. Use real Anaconda naming (no @ prefix on installer-created subvolumes; keep @DaTa prefixed only as our addition). Update declarations YAML to record the full subvolume set with parent and nested fields. Switch Zen browser flatpak ID to app.zen_browser.zen (actual current ID on Flathub; io.github.zen_browser.zen was renamed). Cross-reference the new design from 08-gnome-personal-layer.md so the home layout doc points at the partitioning design instead of duplicating it. Fix collect-diagnostics to query /sysroot instead of /: on Silverblue with composefs, btrfs commands cannot operate on / because the root is an overlay, not a Btrfs subvolume. Validated end-to-end in the Fedora Silverblue 44 VM: @DaTa + three nested subvolumes created post-install, fstab auto-mount of @DaTa survives reboot, snapshot of @home correctly excludes the four subvolumes while including the dotfile region.
Document the end-to-end phase 1 VM lab: CPU check, staging command, first-boot validation, TPM2 PCR behavior across kernel change, rollback and roll-forward tests, key findings, and notes on the out-of-tree module warnings (false positives in this configuration). Record the lab-only choice to leave CachyOS as the booted deployment in the VM, while keeping force_as_default false in the declarative model. Making CachyOS the default on hardware requires a separate Secure Boot trust path analysis.
Move GIMP from photography to a new graphics category alongside Inkscape (vector graphics). Photography stays for raw-workflow tools (darktable, displaycal) where pixel-level retouching is downstream rather than the primary surface.
GNOME Settings → Colour assigns pre-computed ICC profiles per display through colord natively, which covers the day-to-day color workflow. Calibration tools like DisplayCAL/ArgyllCMS are needed only when a calibrator device is actually being used, so they belong in ad-hoc install territory rather than the default optional set.
Restructure default_apps as a category map (internet, productivity, graphics, photography, multimedia, development) so the declarative model mirrors how the apps are surfaced in the GNOME app grid. Promote Darktable, Audacity, OBS Studio, EasyEffects, and VSCodium from optional to default — they cover the recurring photo/audio/video and editor workflows on this profile. Add gnome.app_folders.list to the declarative model with eight folders (Internet, Productivity, Graphics, Photography, Multimedia, Development, Utilities, System) using both pinned .desktop ids and XDG categories. The Utilities/System folders are category-only on purpose: they absorb GNOME core apps without enumerating them. Add scripts/configure-gnome-app-folders (Python, dry-run by default) to apply the folder layout via gsettings, with a --reset path that returns the grid to the flat default. Document the layout in 08-gnome-personal-layer.md alongside the validation commands.
Make the day-to-day experience of stock Margine match what Bluefin ships out of the box (codec, drivers, virt, hardware tools, fonts, gnome-tweaks) while staying on the layered rpm-ostree architecture instead of switching to an image-based variant. The image-based path remains on the roadmap under base.image_workflow.future_candidates (bootc). Declarations: - host_packages.baseline is no longer empty: it holds RPMFusion enablement, codec_replacement (override-remove + freeworld install), media diagnostics, audio baseline, virtualization stack, hardware diagnostics, gnome_tools, and curated fonts. Same intent as the Bluefin base/04-packages.sh set, restated as Margine declarations. - optional_after_validation keeps the truly hardware-specific or scenario-specific bits (Intel/AMD GPU extras, Framework EasyEffects layer, gaming runtime, Tailscale). - toolbox.default.packages is now categorised (core_cli, build_essentials, container_tooling, shells_optional) and expanded with the dev-tool set Bluefin DX layers onto the host (gcc, just, glow, gum, fastfetch, podman-compose, distrobox). Dev tools stay in the toolbox container, not the host. - toolbox.user_layer documents Homebrew on Linux (~/.linuxbrew) and starship as user-binary install paths — fast-moving tools that shouldn't pin the host deployment. - hardware_media.policy.rpmfusion_codec_replacement_default switches to true; rejected_phase1.hardware_media drops the matching entry. scripts/apply-host-layer: Bash script (dry-run by default) that reads the declaration and runs the four rpm-ostree transactions for the baseline: RPMFusion release RPMs, codec override+install, media+audio+virt install, hardware diag + gnome tools + fonts install. Stages a new deployment; reboot via GNOME menu is required. Docs: - docs/15-host-layer.md (new): full rationale, package list, mapping vs Bluefin DX, explicit list of what Bluefin patterns we don't adopt (gnome-software removed, uupd as orchestrator, signed kernel from ublue-os/akmods, Docker CE from docker.com, VS Code from MS repo). - docs/10-hardware-media-stack.md updated for the policy change. - docs/README.md indexes the new doc.
Extend docs/15-host-layer.md with a "Using the baseline" cookbook so the package list isn't just an inventory — each group ships with concrete commands and recipes: - multimedia: how to confirm RPMFusion ffmpeg/mesa-freeworld took effect, hardware-accelerated H.264 encode example, OBS VA-API setup, browser VA-API verification - virtualization: first-time libvirt group setup, virt-manager walkthrough for UEFI + virtual TPM 2.0 (the same lab profile we use for Margine itself), useful virsh commands, custom storage pool under ~/data - hardware diagnostics: sensors-detect, powertop/powerstat workflows, fwupd update path (Framework 13 / LVFS), DDC/CI brightness control via ddcutil, SMART self-tests, hardware enumeration - GNOME Tweaks + AppIndicator: typical first-time settings, legacy tray apps - fonts: applying the curated set via gsettings, fontconfig queries, user font directory layout New docs/16-developer-toolbox.md covers what lives outside the host: - toolbox lifecycle, what each tool does (just, glow, gum, fastfetch, podman-compose, distrobox) - just task runner with concrete example - gum interactive shell helpers - distrobox: when to reach for it vs toolbox, app-export workflow - Homebrew on Linux: install, recommended fast-moving CLI tools (starship, zellij, lazygit, atuin, zoxide, helix, etc.) - starship prompt: install paths, shell wiring, minimal Margine preset - container surfaces table: when to pick host / toolbox / distrobox / podman / Flatpak / brew - GNOME app-grid folders apply/reset commands - a suggested daily workflow that uses every layer Index in docs/README.md.
Two fixes after the first VM apply attempt. Drop gstreamer1-libav from the codec_replacement install list. On Fedora 44 the real package is gstreamer1-plugin-libav (already in the base); gstreamer1-libav is only a virtual provides, which rpm-ostree override refuses with "already provided by ... use --allow-inactive". The codec capability is unchanged because gstreamer1-plugin-libav is already there. Docs updated to match. Add an idempotency check for Step 1 (RPMFusion release RPMs). The check parses rpm-ostree status --json and skips the install if both rpmfusion-free-release and rpmfusion-nonfree-release are already layered in the staged or booted deployment. This makes the script safe to re-run after a partial-failure: e.g. when Step 1 succeeded but Step 2 failed (as happened on the VM), running --apply again now proceeds straight to Step 2 instead of repeating the RPMFusion install.
Same root cause as the previous gstreamer1-libav fix: on Fedora 44 the
VA-API GStreamer plugin moved into gstreamer1-plugins-bad-{free,freeworld},
so gstreamer1-vaapi is now a virtual provides only. rpm-ostree override
refuses it with "already provided by ... use --allow-inactive".
Updated declarations install_note to document both cases together (libav
and vaapi). The VA-API capability still works — it ships inside
gstreamer1-plugins-bad-freeworld, which we already install. Docs in
15-host-layer.md and 10-hardware-media-stack.md updated.
Same root cause as the two previous gstreamer fixes: on Fedora 44 libavif and libheif are already in the Silverblue base (they ship for AVIF/HEIF support in Loupe and GIMP). rpm-ostree override refuses to re-install them with "already provided by ...". Drop both from the codec_replacement install list. Update install_note to consolidate all four skipped symbols (gstreamer1-libav, gstreamer1-vaapi, libavif, libheif) in one place. Also add a "Compatibility with creative apps" section to docs/15-host-layer.md that answers honestly what the baseline does and does not cover for Inkscape, darktable, and DaVinci Resolve: - Inkscape: fully covered by Flatpak; baseline irrelevant. - darktable: partially covered. Video codec/VA-API are covered; OpenCL is not (it's hardware-specific and lives in optional_after_validation). Flatpak darktable doesn't see host OpenCL ICD by default; recommend rpm-ostree install or a flatpak permission override. - DaVinci Resolve: explicitly excluded from the baseline. Documented why (non-Flatpak, CUDA/AMF needed, free edition codec licensing, audio path quirks) and the realistic install paths (distrobox Ubuntu recommended, rpm-ostree layer as fallback). A quick-reference table maps each common creative app to its default delivery channel and the host extras (if any) it needs.
darktable moves from the Flatpak default set to a host-layered package under host_packages.baseline.creative_apps. Reason: the Flathub Flatpak does not see the host OpenCL ICD (ROCm / intel-compute-runtime) without an explicit permission override, so the GPU-accelerated raw processing path is off. The host install picks it up directly when the GPU vendor extras from optional_after_validation are layered in. apply-host-layer now picks creative_apps up alongside hardware_diagnostics / gnome_tools / fonts in Step 4. Showtime joins gnome_tools so the GNOME-native video player ships out of the box (default since GNOME 48; Loupe is the matching image viewer and is already in the Silverblue base). New scripts/configure-default-applications registers MIME type and URL scheme handlers via xdg-mime + xdg-settings, reading the mapping from gnome.default_applications in the YAML: - Web browser → Zen Browser - Mail reader → Thunderbird - Calendar → GNOME Calendar - Music player → Gapless (g4music) - Video player → GNOME Showtime - Image viewer → GNOME Loupe - PDF viewer → GNOME Papers - Text editor → GNOME Text Editor - Archive mgr → File Roller This fixes the "Web=Firefox, Email=Zen" misregistration observed in GNOME Settings → Default Applications: without an explicit handler for the mailto scheme, GNOME picks whatever was registered last, which can land on Zen because it auto-claims mailto on first launch. docs/15-host-layer.md updated with the new sections (Creative apps, default applications script + role mapping).
Validated every baseline package against quay.io/fedora/fedora:44 plus RPMFusion in a podman container — 55/55 OK after this fix. This stops the "rpm-ostree fails, push fix, try again" loop that wasted several iterations. codec_replacement corrected against F44 reality: - mesa-va-drivers and mesa-vdpau-drivers do NOT exist as separate packages on F44 (VA drivers consolidated into mesa-dri-drivers; VDPAU deprecated upstream). Both removed from override_remove. - mesa-vdpau-drivers-freeworld does not exist in RPMFusion F44 (no upstream to wrap). Removed from install. - mesa-vulkan-drivers-freeworld added — Vulkan video codec acceleration (browsers, video editors). - Only ffmpeg-free remains in override_remove (it's a real package and the only one needing replacement). New scripts/validate-baseline-packages: Reads host_packages.baseline from the YAML, spins up a fedora:44 podman container, enables RPMFusion, and runs `dnf info` for every package. Prints OK / MISS and exits 1 if anything is missing. Catches: package rename (e.g. when GStreamer moves things), virtual provides that rpm-ostree override rejects, packages that disappeared upstream, RPMFusion gaps. Run it before pushing any change to host_packages.baseline. Usage: scripts/validate-baseline-packages # pull and check scripts/validate-baseline-packages --keep-image # skip pull scripts/validate-baseline-packages --image-tag 45 docs/15-host-layer.md updated to put the validator first in the "How to apply" sequence.
…olve Two-part fix. 1) codec_replacement: real F44 + Silverblue-state semantics ffmpeg from RPMFusion pulls ffmpeg-libs, which conflicts with three "libav*-free" packages from the Fedora base (libavcodec-free, libavutil-free, libswresample-free). All four must be override-removed together; previously only ffmpeg-free was listed and rpm-ostree failed with "conflicting requests". Skip mesa-va-drivers-freeworld and mesa-vulkan-drivers-freeworld for now: at the time of this commit Mesa Fedora updates (26.0.6) is one point ahead of RPMFusion Mesa (26.0.3 / 26.0.7), and dnf only reconciles via --allowerasing (downgrading base mesa-dri-drivers). rpm-ostree has no downgrade semantics for this. Reintroduce when RPMFusion catches up. Loss: H.264/HEVC VA-API hardware path; ffmpeg software, Mesa base AV1/VP9, and Vulkan video remain. Documented all of the above in install_note so future maintainers understand why each item is or isn't in the list. 2) validate-baseline-packages: depsolve check, not just existence Added a second stage that: - pre-installs the conflicting base packages (libavcodec-free, etc.) to simulate Silverblue state - enables RPMFusion - runs `dnf remove` for the override_remove list - runs `dnf install --setopt=tsflags=test` for the install list WITHOUT --allowerasing This reproduces rpm-ostree's depsolve behaviour and catches conflicts the existence-only check missed. Exits non-zero if the resolution fails. This is the missing layer of the previous validator pass and should have prevented the ffmpeg-libs vs libavcodec-free conflict observed in the last apply attempt.
Previous attempt missed four ffmpeg split packages installed on Silverblue but absent from the minimal fedora:44 container the validator was probing. Real Silverblue ships ffmpeg-free as eight packages, not three: ffmpeg-free libavcodec-free (the one we already had) libavdevice-free (added) libavfilter-free (added) libavformat-free (added) libavutil-free (the one we already had) libswresample-free (the one we already had) libswscale-free (added) Adding the missing four to override_remove lets ffmpeg-libs from RPMFusion replace the whole set in one rpm-ostree transaction, which is what was failing with "libavformat-free requires libavcodec-free ... none of the providers can be installed". Updated scripts/validate-baseline-packages so its Silverblue-state simulation pre-installs ffmpeg-free directly. Since ffmpeg-free pulls in all seven split packages as hard dependencies, this gives the container the same conflict surface a real Silverblue host has, and the depsolve test now matches reality. Re-running the validator confirms RESOLUTION_OK with the full eight-package override_remove.
rpm-ostree refuses an install request whose packages are already provided by the deployment (already-satisfied dependencies, or packages installed as transitive deps of the base). On Silverblue this hits mesa-libGLU (pulled in by GIMP/Loupe), pipewire-pulseaudio / wireplumber / alsa-utils (audio base), and potentially fwupd / gnome-shell-extension-appindicator depending on the install lineage. --allow-inactive tells rpm-ostree to record the request explicitly even when it doesn't change the resulting tree. For a declarative baseline, this is exactly the right semantic: "I want this listed as a Margine intent regardless of whether the base happens to ship it already". The flag is added only to Step 3 and Step 4 (plain rpm-ostree install). Step 2 is rpm-ostree override remove ... --install=, where the install list is paired with explicit removals; --allow-inactive isn't relevant there and wasn't the failure mode last time.
Symmetrically to the Step 1 RPMFusion check, Step 2 now inspects rpm-ostree status --json and skips the override-remove + install if 'ffmpeg' is already in the staged or booted deployment's layered package set. Without this, re-running apply-host-layer after a partial-success aborts on "Package/capability 'ffmpeg' is already requested". Combined with Step 1's existing check and Step 3/4's --allow-inactive, apply-host-layer is now safely re-runnable at any point.
Add kitty and gnome-shell-extension-forge to the host baseline (validated against Fedora 44 + RPMFusion via scripts/validate-baseline-packages — 60/60 OK, RESOLUTION_OK). Add gnome.workspaces (static, count=10) and gnome.keybindings to the declarative model. The bindings mirror margine-os-personal/files/home/.config/hypr/conf.d/60-binds.conf: - workspace + window actions → org.gnome.desktop.wm.keybindings - shell + screenshot → org.gnome.shell.keybindings - lock screen → org.gnome.settings-daemon.plugins.media-keys - app launchers (kitty, zen, nautilus, btop, etc.) → custom slots under media-keys - tiling (focus/swap/resize /group) → org.gnome.shell.extensions.forge.keybindings Two deliberate overrides vs the Hyprland source: - SUPER+E mapped to Nautilus (replaces SUPER SHIFT+F), matching the recurring desktop convention for "Explorer/Files". - Terminal target is kitty (replaces ptyxis), which is also added as gnome.default_applications.terminal_emulator. Skipped: SUPER+K (Hyprland-binds help), SUPER SHIFT+P (waybar restart), SUPER+P (pseudotile, Hyprland-only concept), SUPER+S (scratchpad — Forge has no built-in equivalent). All documented in 17-keyboard-bindings.md. New scripts/configure-gnome-keybindings (Python, dry-run by default) applies the layout via gsettings and dconf. It handles: - the static workspaces model - four native schemas (wm.keybindings, shell.keybindings, media-keys, mutter) - creating custom-keybinding relocatable slots (one per app launcher) - Forge schema (with a friendly note when the extension is not yet enabled — gsettings just fails silently on an unknown schema otherwise) - --reset --apply path that wipes only the custom slots New docs/17-keyboard-bindings.md documents the full mapping (with override notes), how to apply, how to verify, and how to enable Forge. docs/README.md indexes it.
apply-host-layer: filter already-requested packages Step 3 and Step 4 now use a Python helper that reads rpm-ostree status --json and removes packages already in requested-packages of the staged or booted deployment from the install list. Without this, re-running after a partial-success aborts on "Package/capability 'mesa-demos' is already requested" (--allow-inactive only covers "already provided", not "already requested"). With it, re-running is safe at any point — only newly-added packages are passed to rpm-ostree install. If the filtered list is empty, the step is skipped entirely with "All packages already requested ... — skip." configure-gnome-keybindings: graceful per-key failure Switched set_key() to a new run_soft() that captures gsettings stderr and prints "(skipped: <reason>)" instead of crashing the whole script. Two failure modes this fixes: 1. Missing GNOME schema (e.g. Forge before the extension is enabled — the note already warned about it, but the script aborted anyway). 2. Missing key in a schema across GNOME versions. Concrete instance of #2: open-message-tray was renamed to toggle-message-tray in GNOME 47/48. The YAML now uses the current name; the soft-failure handler makes future renames non-fatal.
GNOME 48 ships shell.keybindings.switch-to-application-1..9 bound to <Super>1..9 (launch the Nth pinned app on the dash). That clashes with our wm.keybindings.switch-to-workspace-1..10 on the same combos, and shell.keybindings wins, so the bindings ended up launching apps instead of switching workspaces. Explicitly clear switch-to-application-1..9 before applying the shell group. The WM bindings then take effect. Observed symptom: SUPER+1 opens browser, SUPER+5 text editor, SUPER+6 calculator (the first three pinned dash apps).
Forge 89 uses a different gsettings schema from the earlier guesses: - per-edge resize (window-resize-right/bottom-increase/decrease) instead of per-dimension (window-resize-width/height-*) - con-split-layout-toggle instead of window-rotate-split - no focus-next/previous-window (list-cycling within stacked containers was removed; Forge 89 only does directional focus) Verified by listing org.gnome.shell.extensions.forge.keybindings on the running VM via SSH. Updated declarations + the keybindings table in 17-keyboard-bindings.md, with a note that the schema is version- sensitive and a check command to re-verify after Forge upgrades. Added bonus binding SUPER SHIFT+G for con-tabbed-layout-toggle (tabbed container as a third layout option alongside stacked and split).
The margine boot splash already scaled the logo proportionally, but to 22% of screen WIDTH. On tall HiDPI panels (e.g. 2880x1920, 3:2) that is only ~8% of screen HEIGHT, so the wordmark looks tiny even though scaling "works". Size to ~15% of screen HEIGHT instead, with a 40%-of-width cap for ultrawides and a no-upscale guard against the 1200x300 source. On 2880x1920 the logo goes from ~633x158 to ~1152x288 (about 1.8x) with no upscaling. (Sibling boot-time HiDPI issue: GRUB menu text is also tiny on the same panels; tracked separately since it's a bootloader-config change.)
…raming) (#64) * docs: sync reference docs with shipped state (Forge purge + phase-1 framing) - 08-gnome-personal-layer.md: dropped the two stale Forge references — the shell-extension styling/config ships via the dconf distro.d keyfiles + the zz1 enabled-extensions list (not Forge's schema), and the validation commands now probe org.gnome.shell.extensions.o-tiling instead of the non-existent Forge schema. - 00-goals.md + CONTRIBUTING.md: these captured the phase-1 plan ("VM lab, no image yet, GNOME stock, Fedora Silverblue base"), which now reads as if it were the current state. Added a "superseded / phase 1 complete" status note (00-goals) and reframed CONTRIBUTING's repo-purpose paragraph: phase 1 is done and the spec now feeds the margine-image pipeline that ships the Bluefin-DX image + ISO. The planning intent is preserved, not rewritten. * docs(handbook): describe the two gaming recipes, not one Handbook §6.6 described gaming as a single recipe (ujust margine-gaming, Flatpak launchers + gamescope/vkBasalt RPMs). The shipped model is two recipes: the Flatpak default AND ujust margine-gaming-native (Steam/Lutris/ RetroArch as native RPMs with the full 32-bit closure baked into the base for offline layering). Mirrored in the site handbook split.
…NOME app-grid (#65) Companion to margine-image's search-light removal. search-light owned the <Super>space chord, so disabling it would leave Super+Space unbound — rebind toggle-application-view to ['<Super>space','<Super>r'] so Super+Space opens GNOME's native searchable app-grid (Super alone already opens the searchable overview via overlay-key). Remove search-light from gnome.extensions.enable (Bluefin's copy stays installed + patched, just not auto-enabled) and update the 08-gnome-personal-layer extension table.
* feat(shell): drop search-light from the spec, rebind Super+Space to GNOME app-grid Companion to margine-image's search-light removal. search-light owned the <Super>space chord, so disabling it would leave Super+Space unbound — rebind toggle-application-view to ['<Super>space','<Super>r'] so Super+Space opens GNOME's native searchable app-grid (Super alone already opens the searchable overview via overlay-key). Remove search-light from gnome.extensions.enable (Bluefin's copy stays installed + patched, just not auto-enabled) and update the 08-gnome-personal-layer extension table. * docs(handbook): deep-update CI/CD + recipe chapters (2026-06 work) Monolith half (vendored split updated in lockstep in the website repo). Chapter 9: soft user-smoke gate, /status freshness dashboard producer + base-digest label, GHCR retention, pin + Flatpak-ref automation, direct-push cross-repo bump fix. Chapter 5: AI layer (Flatpak + GPU plugin/Vulkan), safe TPM-unlock/autologin helpers, margine-status/update.
Plymouth (margine.script): center the logo on EVERY display, sized per display, instead of a single sprite centered on window 0 — which landed the wordmark in the corner of a second/larger monitor (reported: 4K external + laptop showed it top-left). Iterate Window.GetWidth(i) and keep each sprite referenced. validate-margine-system: - EXPECTED_EXTENSIONS in lockstep with zz1: drop search-light (removed 2026-06-14), add smile (added 2026-06-13) — was false-failing on both. - booted-image check WARNs (not FAILs) when bootc status returns nothing (no root / image context, e.g. ujust margine-doctor) — "can't determine" is not "wrong image". - failed-services check filters to unit-like names (grep -F .), so a non-unit token from a privilege-less systemctl call can't surface as a spurious "<word> failed".
Decision + GO-with-fixes verdict + rollout + honest risks for the signed NVIDIA image variant (build-arg-gated akmod-nvidia-open against kernel-cachyos-devel-matched, signed by the existing MOK loop). Scaffold landed in margine-image #161.
…urce revision (#69) - validate-margine-system: expect num-workspaces=5 (Daniel's standing preference; pairs with the zz1 change in margine-image). dynamic-workspaces expectation (false) was already correct — the zz1 change now matches it. - 17-keyboard-bindings: document 5 fixed workspaces (Super+1..5; 6..0 inert). - ADR 0009: correct the "no precedent" claim — RakuOS (rakuos.org) is a production CachyOS+bootc+MOK-Secure-Boot distro shipping NVIDIA (same MOK model as Margine). Revise the kmod source recommendation from RPMFusion akmod to NVIDIA-upstream (DKMS/.run) compiled against the CachyOS kernel, per RakuOS + coreos/fedora-bootc-nvidia, to avoid RPMFusion schedule-skew.
margine-image PR #164 implements the source revision (RPMFusion akmod → NVIDIA-upstream kmod-nvidia-open-dkms, DKMS-built against the CachyOS kernel tree, signed by the existing MOK loop). Document the v1 fixes (no binutils-gold; exact %fedora repo hard-fail; nvidia-drm.fbdev=1; noscripts contingency) and note that the v0 akmod-specific "Mandatory fixes" bullets are superseded. Experimental status + CI/hardware rollout gates unchanged.
…ker (#71) The recurring "tiling toggle doesn't switch" turned out to be a stale user-level o-tiling copy shadowing the system one (GNOME loads ~/.local and refuses /usr/share), making every system-side fix invisible. Teach validate-margine-system to: - FAIL when a ~/.local o-tiling copy coexists with the system copy (the shadow), with the exact rm + logout remediation; - check the session-only toggle marker (margine-image #165) on the loaded system copy so a pre-fix image is flagged.
…ation (#72) blur-my-shell@aunetx was removed from Margine's default enabled set (margine-image #167: 120Hz HiDPI iGPU jank from dynamic per-frame blur + three unfixed-upstream GNOME-50 bugs). Remove it from EXPECTED_EXTENSIONS so the enabled-extensions comparison (section 7) doesn't flag it as MISSING. It stays installed, so EXT_SCHEMA_MAP keeps asserting its schema is registered (same as the not-enabled-but-installed search-light).
The "blur keeps coming back after an upgrade" recurrence: configure-gnome- extensions treats the declarations/margine-atomic.yaml `enable:` list as the source of truth and re-applies it to org.gnome.shell enabled-extensions on every bootstrap. blur-my-shell@aunetx was still in that list, so each bootstrap (e.g. after a `ujust margine-update` first boot) re-enabled it — even though it was dropped from the zz1 gschema default (margine-image #167) and the validator (#72). Remove it from `enable:` and document why; keep its (smooth, static) dconf defaults so a manual re-enable doesn't jank. Now in lockstep with the image's enabled-extensions.
…otstrap reset) (#74) configure-gnome-appearance --apply writes the declarations/margine-atomic.yaml dash-to-dock block to the user's dconf, so the stale "semi-transparent, DOTS" values would RESET the dock on the next margine-bootstrap. Update the yaml to match the new image default (margine-image #173): fully-transparent fixed bottom dock, 36px icons, DEFAULT indicator. Host-specific monitor keys left out.
These three extensions are now removed entirely from margine-image
(build-margine-extensions.sh, 2026-06-16) rather than merely left out of
the enable list, so the spec must stop declaring them.
declarations/margine-atomic.yaml:
* remove the blur-my-shell dconf defaults block and the search-light
dconf defaults block (configure-gnome-appearance silently skipped
them once the schemas went absent, but they were stale).
* drop gnome-shell-extension-blur-my-shell from gnome_tools (the
package is no longer part of the image surface).
* rewrite the enable: list comment — blur/search-light/logomenu are
REMOVED, not "still installed, re-enable from the list". A user who
wants one back installs it from Extensions Manager.
* rename branding_extensions_disabled -> branding_extensions_removed
(logomenu is gone, not just off).
* note on the search-light removed_user_install entry that it is now
removed system-wide too; the entry still prunes stale user copies.
scripts/validate-margine-system:
* EXT_SCHEMA_MAP no longer lists blur-my-shell / search-light schemas
(intentionally absent now), keeping dash-to-dock, caffeine, o-tiling.
Mirrors margine-image #174.
Closes the highest-value OpenSSF Best Practices gap for a project that ships a cosign-signed image and a Secure Boot MOK: a documented vulnerability-disclosure process. Private reporting via GitHub Security Advisories (this repo + margine-image) with an email fallback, a best-effort response window, and scope. Records that the MOK passphrase margine-os is public by design and not a vulnerability. Mirrors the margine-image SECURITY.md.
… claim (#77) Mirror the margine-image perf hero on the spec repo: benefit-framed claim (up to ~1.8x scheduling throughput, 40-55% lower median/avg latency vs stock Fedora, same laptop) + the median benchmark chart (assets/perf-kernel.svg, ASCII-safe snapshot of the canonical result in margine-image), linking to the raw results for transparency.
…nel" (#78) - README perf hero: name the machine (Framework Laptop 13 / Ryzen 5 7640U) and refresh the chart asset (Framework-labeled, ASCII-safe). - Handbook (monolith docs/atomic-distro-handbook.md): new §3.6 "Benchmarking the kernel" — the harness (container-based, shares host kernel, stress-ng load, the 4 scheduler benches, JSON+DMI identity, empty-result guard), the deployment-switch A/B method, the median comparer (variance + thermal + identity-only guards, ASCII-safe SVG), thermal control, and the honest median result (~1.8x scheduling throughput, 40-55% lower median/avg latency, ~10% higher tail). Mirrors the site handbook split (content/handbook/03-kernel).
Super+E ran bare 'nautilus', which is single-instance — repeated presses just re-focus the running window instead of opening another (user-reported). Use 'nautilus --new-window' so each press opens a new Files window.
…w) in yaml (#81) The Super+arrows "move focused window in the tiled layout" binding lived only in margine-image's 03-margine-o-tiling dconf default. The o_tiling block here is the authoritative source that ujust margine-bootstrap re-applies to user dconf, and its own comment requires the two to stay in sync. Add tile-move-{left,right,up,down}-global = Super+arrows so the declaration is complete, and document the move binding (and why it must be tile-move-*-global, not the no-op-as-global tile-swap-*) in 17-keyboard-bindings.md.
The wordmark was positioned once, on the early-KMS framebuffer. In VMs the mode switches mid-boot (early res -> final res), so the logo ended up off-centre (user-reported); at shutdown, where the resolution is already final, it was centred — so the centering math was correct, only the timing was wrong. Wrap the layout in layout_logos() and call it from refresh_callback whenever the primary display geometry changes (cheap: just an int compare per frame). Sprites are created once and reused via SetImage; the scaled image stays in the global `logos` hash so it isn't GC'd. Uses the same idioms as the existing dialog code.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
… parse gate (#83) Validation (multi-agent review) found #82 re-centred the logo but NOT the unlock dialog: dialog_top was computed once at the early-KMS resolution and never recomputed, so the LUKS passphrase prompt — the user's actual complaint — stayed mispositioned after the mid-boot mode switch in VMs. Recompute dialog_top live in dialog_show(), track dialog visibility + last prompt/input as flat globals (not under the `dialog` hash, so the lazy setup check still fires), and re-flow the dialog from refresh_callback when the geometry changes. Also add a build-time guard: margine-validate-branding now asserts the three plymouth theme files exist and that margine.script is brace/paren-balanced and still registers the refresh + password callbacks. There's no offline Plymouth-Script linter, so without this a broken theme edit reaches :stable and only surfaces at boot. Verified: gate PASSes on the current theme; shellcheck clean.
…ow) (#84) The multi-display refactor laid the wordmark out per display with a `for (i = 0; Window.GetWidth(i) > 0; i++)` loop and stored each sprite in a nested `logos[i]` hash. That regressed the logo to invisible everywhere — no wordmark at the LUKS passphrase prompt nor during boot (reported 2026-06-20) — for two compounding reasons: * a sprite reference held only inside a nested hash is not retained the way a flat global is, so it was collected before it drew; and * the indexed multi-display Window calls are fragile at the early-KMS / initramfs stage. The unlock dialog never had this problem because it uses flat globals and the non-indexed Window API. Collapse the logo to a single centred sprite using that exact, proven pattern. Re-centre on the early-boot mode switch is preserved. On a multi-monitor host the logo centres on the primary, which is acceptable; rendering at all is what matters.
…eworld (#86) RPMFusion's gstreamer1-plugins-bad-freeworld links libx265.so.215 while the base's negativo17 multimedia stack ships x265 4.2 (.216) — since 2026-07-04 the two can no longer coexist and the margine-image base build fails at the creator-tier install. negativo17's gstreamer1-plugins-bad (epoch 1) is a superset of the freeworld build (bundles libgstx265.so, libgstva.so, nvcodec/ msdk/qsv, fdkaac) and is built against the same x265 the base already ships, so the codec promises in the handbook are unchanged. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Step 1 of the repo unification: the companion's full tree lands under vendor-spec/ with its 187 commits preserved in history. Follow-up commits move the content to its final in-repo homes and rewire the build from network fetches to local files. git-subtree-dir: vendor-spec git-subtree-mainline: 2600f45 git-subtree-split: 8e698b2
- scripts (22, minus 2 SUPERSEDED) + declarations -> build_files/40-spec-scripts/ (must live under build_files/ to be visible in the Containerfile ctx COPY) - assets/branding (17 files) -> build_files/50-branding/assets/ - docs, ADRs, lessons-learned, handbook monolith, config + files reference -> docs/spec/ - dropped: superseded apply-* scripts, assets/offline-docs (superseded), CONTRIBUTING/SECURITY/LICENSE duplicates, cache junk
… parked in docs/spec) - docs/spec/README.md = the companion's Documentation Map (kept as the index) - companion top-level README -> docs/spec/COMPANION-README.md (historical) - perf-kernel.svg -> docs/spec/assets/ (handbook figure, not branding) - companion check-upstreams.yml + lint.yml -> docs/spec/companion-workflows/ (to be ported to real .github/workflows in the rewire step) - renovate.json5 parked; FUNDING/.gitignore dropped (dupes) vendor-spec/ is now gone.
The base build no longer fetches anything from margine-fedora-atomic. All of it is now local under build_files/ (visible in the Containerfile ctx COPY): - 00-common.sh: dropped MARGINE_REPO/MARGINE_REF exports (retry_curl helpers stay) - 40-spec-scripts/install.sh: preflight probe gone; the 16 spec scripts and the declarations YAML now install from /ctx/40-spec-scripts/ (identical install set to before, verified against main; CI validators margine-validate-margine- system/-branding still resolve) - 50-branding/install.sh: 9 branding pulls now cp from /ctx/50-branding/assets/, strict emptiness guards preserved - Containerfile: ARG MARGINE_REF removed (spec state = image repo git SHA) - build.yml + build-nvidia.yml: specref resolution step deleted, --build-arg MARGINE_REF removed, spec-ref OCI labels repointed to github.sha - os-release DOCUMENTATION_URL + systemd Documentation= + README/SECURITY/ BRANDING links repointed to margine-image; vendored script header URLs too - check-upstreams.yml migrated to .github/workflows/ against the local script Verified: no MARGINE_REPO/REF or dangling specref anywhere, no live companion fetch in the build path, bash -n + YAML all clean, every install source exists.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Folds the companion margine-fedora-atomic into this repo, so there is ONE repo. Motivation, grounded in this week's pain: forks could not build (the CI resolved
<owner>/margine-fedora-atomic, so mikaeldui's fork 404'd), and single fixes needed coordinated PRs across two repos (the x265 codec fix). External exposure of the companion is ~zero (0 forks, 1 star, bot-only issues), so this is safe.What moved (history preserved via git subtree, 187 commits)
declarations/margine-atomic.yaml->build_files/40-spec-scripts/build_files/50-branding/assets/docs/spec/check-upstreamscron ->.github/workflows/against the local scriptapply-*scripts,assets/offline-docs, cache junkWhat was rewired (build no longer fetches from the companion)
00-common.sh:MARGINE_REPO/MARGINE_REFexports gone (retry_curl helpers stay)40-spec-scripts/install.sh: preflight probe gone; scripts + declarations install from/ctx/...(identical install set to main, verified; CI validators still resolve)50-branding/install.sh: 9 pulls -> localcp, strict emptiness guards keptContainerfile:ARG MARGINE_REFremoved (spec state = image git SHA)build.yml+build-nvidia.yml: specref step deleted,--build-arg MARGINE_REFremoved, spec-ref labels ->github.shaVerification
No
MARGINE_REPO/REFor danglingspecrefanywhere; no live companion fetch in the build path;bash -n+ YAML all clean; every install source exists; 4-agent rewire + adversarial verify pass. This PR carries thevm-testlabel so CI builds the base image for real — that green is the acceptance gate.Companion repo gets archived-in-place (tree intact + pointer README) after this merges, so every baked-in DOCUMENTATION_URL in already-installed systems keeps resolving.