Skip to content

merge: unify margine-fedora-atomic into margine-image (single repo)#272

Merged
daniel-g-carrasco merged 191 commits into
mainfrom
merge/fedora-atomic
Jul 5, 2026
Merged

merge: unify margine-fedora-atomic into margine-image (single repo)#272
daniel-g-carrasco merged 191 commits into
mainfrom
merge/fedora-atomic

Conversation

@daniel-g-carrasco

Copy link
Copy Markdown
Owner

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)

  • 16 spec scripts + declarations/margine-atomic.yaml -> build_files/40-spec-scripts/
  • 14 branding assets -> build_files/50-branding/assets/
  • handbook, ADRs, audits, lessons-learned, roadmap -> docs/spec/
  • check-upstreams cron -> .github/workflows/ against the local script
  • dropped: superseded apply-* scripts, assets/offline-docs, cache junk

What was rewired (build no longer fetches from the companion)

  • 00-common.sh: MARGINE_REPO/MARGINE_REF exports 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 -> local cp, strict emptiness guards kept
  • Containerfile: ARG MARGINE_REF removed (spec state = image git SHA)
  • build.yml + build-nvidia.yml: specref step deleted, --build-arg MARGINE_REF removed, spec-ref labels -> github.sha
  • os-release / systemd / README / SECURITY / BRANDING links -> margine-image

Verification

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; 4-agent rewire + adversarial verify pass. This PR carries the vm-test label 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.

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).
daniel-g-carrasco and others added 27 commits June 14, 2026 16:33
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.
@daniel-g-carrasco daniel-g-carrasco added the vm-test Trigger build.yml + publish :pr-N tag for VM rebase smoke testing label Jul 5, 2026
@daniel-g-carrasco
daniel-g-carrasco merged commit e7b6fa9 into main Jul 5, 2026
4 checks passed
@daniel-g-carrasco
daniel-g-carrasco deleted the merge/fedora-atomic branch July 5, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vm-test Trigger build.yml + publish :pr-N tag for VM rebase smoke testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant