From 3402c7f8905a6e4d3a54654b9b2f13fe15b26261 Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:19:56 -0600 Subject: [PATCH 01/13] Move freeworld codec stack to same-build dnf swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The force-install OCI artifact snapshotted libavcodec-freeworld, gstreamer1-plugins-bad-freeworld, mesa-*-freeworld, and vvenc-libs on a schedule independent of the main image. Those RPMs link against versioned x265/vvenc symbols (e.g. x265_api_get_215, libvvenc.so.1.13) that no longer exist on the rolling Bazzite base, producing runtime errors when ffmpeg or GStreamer try to load them. Install the freeworld codec stack atomically in 02-build.sh via a narrowly-scoped dnf swap allowlist instead — versions co-resolve against the live repos, so ABI skew is impossible by construction. Adds a codec-integrity stage to 08-validate.sh (ldd + ffmpeg -encoders + gst-inspect) that turns this class of failure into a build-time hard fail instead of a runtime one. Co-Authored-By: Claude Opus 4.7 --- .github/force-install-builder/packages.txt | 10 +- build_files/02-build.sh | 130 ++++++++++++++++++++- build_files/08-validate.sh | 115 ++++++++++++++++++ 3 files changed, 246 insertions(+), 9 deletions(-) diff --git a/.github/force-install-builder/packages.txt b/.github/force-install-builder/packages.txt index 6b20849..175dac2 100644 --- a/.github/force-install-builder/packages.txt +++ b/.github/force-install-builder/packages.txt @@ -23,10 +23,10 @@ # audacity-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates mutter-devel | dnf | terra -libavcodec-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates -gstreamer1-plugins-bad-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates -mesa-vulkan-drivers-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates -mesa-va-drivers-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates -vvenc-libs | dnf | rpmfusion-free,rpmfusion-free-updates +# Codec stack (libavcodec-freeworld, gstreamer1-plugins-bad-freeworld, +# mesa-*-freeworld, vvenc-libs) is no longer cached here — those packages +# link against versioned x265/vvenc symbols, and snapshotting them in this +# artifact produces ABI skew against the rolling Bazzite base. They are +# installed atomically in the same dnf transaction by build_files/02-build.sh. gnome-rounded-blur | github-build | kancko/gnome-rounded-blur glycin-fix-bc7 | github-release | phantomcortex/glycin diff --git a/build_files/02-build.sh b/build_files/02-build.sh index bf96a93..68c424f 100755 --- a/build_files/02-build.sh +++ b/build_files/02-build.sh @@ -18,6 +18,13 @@ source /ctx/95-utility-functions.sh # ============================================================================ # --allowerasing lets dnf silently remove packages to satisfy an install, which # can clobber required packages. Wrap dnf/dnf5 so any future use is rejected. +# +# Narrow exception: the freeworld codec stack (libavcodec-freeworld, +# mesa-*-freeworld, gstreamer1-plugins-bad-freeworld) overlaps file paths +# with the Fedora "free" variants, so a clean install requires removing the +# free variant first. The dnf_codec_swap() function in the codec section +# bypasses these wrappers via `command dnf5` after validating that the +# swap is between a specific known-safe pair — see CODEC_SWAP_ALLOWLIST. dnf() { local arg @@ -487,9 +494,11 @@ declare -A RPM_PACKAGES=( glib2-devel \ firejail" + # vvenc and the freeworld codec stack are installed by the dedicated + # codec section below — they need atomic resolution against x265 and + # libavcodec-freeworld in the same transaction. ["rpmfusion-free,rpmfusion-free-updates,rpmfusion-nonfree,rpmfusion-nonfree-updates"]="\ - gstreamer1-plugins-ugly \ - vvenc" + gstreamer1-plugins-ugly" ["terra,terra-extras"]=" \ gstreamer1-vaapi \ @@ -511,6 +520,110 @@ for repo in "${!RPM_PACKAGES[@]}"; do install_packages_resilient "$repo" "${pkg_array[@]}" done +# ────────────────────────────────────────────────────────────────────────── +# Codec Stack — atomic swap to RPMFusion freeworld variants +# ────────────────────────────────────────────────────────────────────────── +# Why this lives here, not in the force-install OCI artifact: +# +# libavcodec-freeworld, gstreamer1-plugins-bad-freeworld, and the +# mesa-*-freeworld pair link against versioned symbols from x265 and vvenc +# (e.g. x265_api_get_215, libvvenc.so.1.13). When these RPMs were snapshotted +# in the force-install artifact — which builds on a separate schedule from +# the main image — the captured .so files reference symbol versions that +# no longer exist on the rolling Bazzite base, producing runtime errors: +# +# ffmpeg: error while loading shared libraries: libvvenc.so.1.13 +# ffmpeg: symbol lookup error: ... undefined symbol: x265_api_get_215 +# +# Installing the whole freeworld codec stack in a single dnf transaction +# against the live repos co-resolves them with their current x265/vvenc +# dependencies — versions match by construction, no skew possible. + +log_section "Installing freeworld codec stack" + +# Narrowly-scoped escape hatch for the --allowerasing guard. +# Calls `command dnf5` directly (bypassing the wrapper) but only after +# validating that the swap pair is in CODEC_SWAP_ALLOWLIST. Any other use +# of --allowerasing remains blocked by the dnf/dnf5 wrappers above. +readonly -a CODEC_SWAP_ALLOWLIST=( + "ffmpeg-free|ffmpeg" + "libavcodec-free|libavcodec-freeworld" + "libavdevice-free|libavdevice-freeworld" + "libavfilter-free|libavfilter-freeworld" + "libavformat-free|libavformat-freeworld" + "libavutil-free|libavutil-freeworld" + "libpostproc-free|libpostproc-freeworld" + "libswresample-free|libswresample-freeworld" + "libswscale-free|libswscale-freeworld" + "mesa-va-drivers|mesa-va-drivers-freeworld" + "mesa-vulkan-drivers|mesa-vulkan-drivers-freeworld" + "gstreamer1-plugins-bad-free|gstreamer1-plugins-bad-freeworld" + "gstreamer1-plugins-bad-free-extras|gstreamer1-plugins-bad-freeworld-extras" +) + +dnf_codec_swap() { + local from="$1" to="$2" + local pair="${from}|${to}" + + local allowed=false + for entry in "${CODEC_SWAP_ALLOWLIST[@]}"; do + [[ "$entry" == "$pair" ]] && allowed=true && break + done + + if ! $allowed; then + log_error " ✗ dnf_codec_swap: '$from' → '$to' not in allowlist" + FAILED_PACKAGES+=("$to") + return 1 + fi + + # If source isn't installed, fall back to a plain install of the target. + # This handles bases that already ship the freeworld variant, or where + # the -free counterpart was never installed. + if ! rpm -q "$from" &>/dev/null; then + if command dnf5 -y install "$to" &>/dev/null; then + log_success " ✓ $to (direct install — no $from present)" + SUCCEEDED_PACKAGES+=("$to") + return 0 + fi + log_warning " ✗ $to (direct install failed)" + FAILED_PACKAGES+=("$to") + return 1 + fi + + # Source installed → swap. --allowerasing lets dnf remove the source + # if the target's file list overlaps. The allowlist above bounds the + # blast radius to known-safe codec pairs. + if command dnf5 -y swap "$from" "$to" --allowerasing &>/dev/null; then + log_success " ✓ $from → $to" + SUCCEEDED_PACKAGES+=("$to") + return 0 + fi + + log_warning " ✗ $from → $to (swap failed)" + FAILED_PACKAGES+=("$to") + return 1 +} + +# Order: top-level meta-packages first (ffmpeg pulls the libav* family), +# then GPU driver swaps, then the standalone libavcodec/gstreamer adders. +dnf_codec_swap "ffmpeg-free" "ffmpeg" +dnf_codec_swap "mesa-va-drivers" "mesa-va-drivers-freeworld" +dnf_codec_swap "mesa-vulkan-drivers" "mesa-vulkan-drivers-freeworld" +dnf_codec_swap "libavcodec-free" "libavcodec-freeworld" +dnf_codec_swap "gstreamer1-plugins-bad-free" "gstreamer1-plugins-bad-freeworld" + +# vvenc and svt-av1 round out the encoder set. They have no -free counterpart +# (RPMFusion-only), so a plain install is correct — it co-resolves with the +# freeworld libavcodec just installed, guaranteeing matching sonames. +log_info "Installing standalone codec libraries (vvenc, svt-av1)" +if command dnf5 -y install vvenc vvenc-libs svt-av1-libs &>/dev/null; then + log_success " ✓ vvenc, vvenc-libs, svt-av1-libs" + SUCCEEDED_PACKAGES+=("vvenc" "vvenc-libs" "svt-av1-libs") +else + log_warning " ✗ vvenc/svt-av1 install failed (codec encoders may be missing)" + FAILED_PACKAGES+=("vvenc") +fi + # ────────────────────────────────────────────────────────────────────────── # COPR Repository Packages # ────────────────────────────────────────────────────────────────────────── @@ -603,7 +716,8 @@ readonly -a CRITICAL_PACKAGES=( "gnome-shell" "gnome-session" "vulkan-headers" - "mesa-vulkan-drivers" + "mesa-vulkan-drivers-freeworld" # post-swap: replaces mesa-vulkan-drivers + "mesa-va-drivers-freeworld" # post-swap: replaces mesa-va-drivers "mesa-filesystem" "mesa-dri-drivers" "mesa-libGL" @@ -613,6 +727,9 @@ readonly -a CRITICAL_PACKAGES=( "mesa-libEGL" "mesa-libEGL-devel" "ffmpeg" + "libavcodec-freeworld" # codec stack: patent-encumbered encoders + "vvenc-libs" # H.266/VVC encoder runtime + "gstreamer1-plugins-bad-freeworld" "mutter" "wayland-devel" "ScopeBuddy" @@ -689,7 +806,8 @@ readonly -a VERSIONLOCK_PACKAGES=( "gnome-shell" "gnome-session" "vulkan-headers" - "mesa-vulkan-drivers" + "mesa-vulkan-drivers-freeworld" # post-swap name + "mesa-va-drivers-freeworld" # post-swap name "mesa-filesystem" "mesa-dri-drivers" "mesa-libGL" @@ -699,6 +817,10 @@ readonly -a VERSIONLOCK_PACKAGES=( "mesa-libEGL" "mesa-libEGL-devel" "ffmpeg" + "libavcodec-freeworld" + "vvenc" + "vvenc-libs" + "gstreamer1-plugins-bad-freeworld" "mutter" "wayland-devel" "ScopeBuddy" diff --git a/build_files/08-validate.sh b/build_files/08-validate.sh index 40f34a0..0334278 100755 --- a/build_files/08-validate.sh +++ b/build_files/08-validate.sh @@ -196,6 +196,121 @@ for bin in "${CRITICAL_BINS[@]}"; do fi done +# ── Codec stack integrity ─────────────────────────────────────────────────── +# Catches the ABI-skew class of bug where libavcodec-freeworld links against +# x265/vvenc symbol versions that no longer exist on the live system. Used +# to be a runtime failure (ffmpeg refusing to launch, GStreamer pipelines +# erroring out); now a build failure with a clear diagnostic. +# +# Three layers, cheap to expensive: +# 1. ldd on ffmpeg/ffprobe + every libav*.so — catches missing sonames +# 2. ffmpeg -encoders — catches versioned-symbol failures ldd can't see +# (e.g. "undefined symbol: x265_api_get_215") +# 3. gst-inspect of va/x265enc plugins — catches GStreamer-side regressions +log_section "Codec stack integrity" + +# ─ Layer 1: dynamic linker can resolve every soname ─ +codec_link_failed=0 +readonly -a CODEC_BINS=(/usr/bin/ffmpeg /usr/bin/ffprobe) +for codec_bin in "${CODEC_BINS[@]}"; do + if [[ ! -x "$codec_bin" ]]; then + log_warning " - $codec_bin not installed (skipping)" + continue + fi + missing=$(ldd "$codec_bin" 2>&1 | grep "not found" || true) + if [[ -n "$missing" ]]; then + fail "$codec_bin has unresolved libraries:" + echo "$missing" | head -5 | while read -r line; do + echo " $line" + done + codec_link_failed=1 + else + log_success " ✓ $(basename "$codec_bin") sonames resolve" + fi +done + +# libav*.so* may live in /usr/lib64 or /usr/lib64/ffmpeg depending on packaging +mapfile -t libav_so < <( + find /usr/lib64 -maxdepth 3 -type f \ + -regex '.*/lib\(avcodec\|avformat\|avfilter\|avdevice\|avutil\|postproc\|swresample\|swscale\)\.so\.[0-9]+' \ + 2>/dev/null +) +libav_failed=0 +for so in "${libav_so[@]}"; do + missing=$(ldd "$so" 2>&1 | grep "not found" || true) + if [[ -n "$missing" ]]; then + fail "$so has unresolved sonames:" + echo "$missing" | head -3 | while read -r line; do + echo " $line" + done + codec_link_failed=1 + libav_failed=1 + fi +done +if [[ ${#libav_so[@]} -gt 0 && $libav_failed -eq 0 ]]; then + log_success " ✓ ${#libav_so[@]} libav* dylib(s) link cleanly" +fi + +# ─ Layer 2: ffmpeg actually runs and resolves versioned symbols ─ +# Skip if Layer 1 already failed — the error message will be misleading +# and the real cause is upstream. +if [[ $codec_link_failed -eq 0 && -x /usr/bin/ffmpeg ]]; then + if ffmpeg_out=$(/usr/bin/ffmpeg -hide_banner -encoders 2>&1) \ + && ! grep -qiE 'symbol lookup error|undefined symbol' <<<"$ffmpeg_out"; then + + # Tier 1 (hard requirement): baseline software encoders. + readonly -a TIER1_ENCODERS=(libx264 libx265) + # Tier 2 (soft requirement): newer codecs that may lag in some ffmpeg builds. + readonly -a TIER2_ENCODERS=(libsvtav1 libvvenc) + # Tier 3 (hard requirement): AMD/Intel VAAPI HW path — load-bearing for + # the user's "feature complete with proper GPU encode/decode" goal. + readonly -a TIER3_ENCODERS=(h264_vaapi hevc_vaapi av1_vaapi) + + for enc in "${TIER1_ENCODERS[@]}"; do + if grep -qE "^ [VA]\.\.\.\.\. ${enc} " <<<"$ffmpeg_out"; then + log_success " ✓ encoder: $enc" + else + fail "encoder missing (Tier 1, hard): $enc" + fi + done + for enc in "${TIER2_ENCODERS[@]}"; do + if grep -qE "^ [VA]\.\.\.\.\. ${enc} " <<<"$ffmpeg_out"; then + log_success " ✓ encoder: $enc" + else + log_warning " ? encoder missing (Tier 2, soft): $enc" + fi + done + for enc in "${TIER3_ENCODERS[@]}"; do + if grep -qE "^ [VA]\.\.\.\.\. ${enc} " <<<"$ffmpeg_out"; then + log_success " ✓ encoder: $enc" + else + fail "encoder missing (Tier 3, hard — AMD GPU encode): $enc" + fi + done + else + fail "ffmpeg failed to enumerate encoders (symbol-resolution failure):" + echo "$ffmpeg_out" | head -5 | while read -r line; do + echo " $line" + done + fi +fi + +# ─ Layer 3: GStreamer sees the freeworld plugins ─ +if command -v gst-inspect-1.0 &>/dev/null; then + if gst-inspect-1.0 va &>/dev/null; then + log_success " ✓ GStreamer 'va' plugin present (VAAPI HW codec path)" + else + fail "GStreamer 'va' plugin missing — VAAPI HW decode/encode unavailable" + fi + # x265enc lives in gstreamer1-plugins-bad-freeworld; its absence usually + # means the freeworld swap failed silently in 02-build.sh. + if gst-inspect-1.0 x265enc &>/dev/null; then + log_success " ✓ GStreamer x265enc present" + else + fail "GStreamer x265enc missing — gstreamer1-plugins-bad-freeworld likely failed" + fi +fi + # ── Final report ──────────────────────────────────────────────────────────── log_section "Validation summary" From eb924715cdb1724b227676a742f4e8da2f47e89a Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:13:23 -0600 Subject: [PATCH 02/13] Fix codec swap/install logic and encoder regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues exposed by the first testing build: 1. gstreamer1-plugins-bad-{free,freeworld} are NOT exclusive — bad-free ships libgstva.so (the VAAPI plugin); freeworld only adds x265enc, de265, and rtmp on top. The swap was silently removing the va plugin, breaking GStreamer HW codec acceleration. Switch to an additive install. 2. The Tier 1/2/3 encoder regex required five literal dots in the capability field, but real ffmpeg output is "V....D" (four dots + D for direct-rendering support). Every encoder check was a false negative. Use "[.A-Z]{5}" to match any capability flag combination. 3. libavcodec-freeworld is a parallel-library package useful only when the system runs Fedora's ffmpeg-free. Bazzite ships RPMFusion's full ffmpeg which bundles patent codecs in its own libavcodec, so the freeworld variant is uninstallable (file conflict) and unnecessary. Drop it from CRITICAL_PACKAGES/VERSIONLOCK_PACKAGES; the swap stays as a best-effort no-op for non-Bazzite bases. Also: stop suppressing dnf stderr in the codec section — failures now log dnf's error message inline for diagnostics. Co-Authored-By: Claude Opus 4.7 --- build_files/02-build.sh | 80 ++++++++++++++++++++++++++++---------- build_files/08-validate.sh | 6 +-- 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/build_files/02-build.sh b/build_files/02-build.sh index 68c424f..815c4ab 100755 --- a/build_files/02-build.sh +++ b/build_files/02-build.sh @@ -545,6 +545,11 @@ log_section "Installing freeworld codec stack" # Calls `command dnf5` directly (bypassing the wrapper) but only after # validating that the swap pair is in CODEC_SWAP_ALLOWLIST. Any other use # of --allowerasing remains blocked by the dnf/dnf5 wrappers above. +# +# Note: gstreamer1-plugins-bad-{free,freeworld} are NOT in this list — they +# are co-installable (bad-free ships the `va`/`festival` plugins; freeworld +# adds `x265enc`/`de265`/`rtmp`). Swapping them would silently lose `va` and +# break VAAPI HW decode. They are handled by a plain install below. readonly -a CODEC_SWAP_ALLOWLIST=( "ffmpeg-free|ffmpeg" "libavcodec-free|libavcodec-freeworld" @@ -557,8 +562,6 @@ readonly -a CODEC_SWAP_ALLOWLIST=( "libswscale-free|libswscale-freeworld" "mesa-va-drivers|mesa-va-drivers-freeworld" "mesa-vulkan-drivers|mesa-vulkan-drivers-freeworld" - "gstreamer1-plugins-bad-free|gstreamer1-plugins-bad-freeworld" - "gstreamer1-plugins-bad-free-extras|gstreamer1-plugins-bad-freeworld-extras" ) dnf_codec_swap() { @@ -576,53 +579,85 @@ dnf_codec_swap() { return 1 fi + local err_file + err_file=$(mktemp) + # If source isn't installed, fall back to a plain install of the target. # This handles bases that already ship the freeworld variant, or where # the -free counterpart was never installed. if ! rpm -q "$from" &>/dev/null; then - if command dnf5 -y install "$to" &>/dev/null; then + if command dnf5 -y install "$to" >/dev/null 2>"$err_file"; then log_success " ✓ $to (direct install — no $from present)" SUCCEEDED_PACKAGES+=("$to") + rm -f "$err_file" return 0 fi - log_warning " ✗ $to (direct install failed)" + log_warning " ✗ $to (direct install failed; dnf says:)" + sed 's/^/ /' "$err_file" | head -5 FAILED_PACKAGES+=("$to") + rm -f "$err_file" return 1 fi # Source installed → swap. --allowerasing lets dnf remove the source # if the target's file list overlaps. The allowlist above bounds the # blast radius to known-safe codec pairs. - if command dnf5 -y swap "$from" "$to" --allowerasing &>/dev/null; then + if command dnf5 -y swap "$from" "$to" --allowerasing >/dev/null 2>"$err_file"; then log_success " ✓ $from → $to" SUCCEEDED_PACKAGES+=("$to") + rm -f "$err_file" return 0 fi - log_warning " ✗ $from → $to (swap failed)" + log_warning " ✗ $from → $to (swap failed; dnf says:)" + sed 's/^/ /' "$err_file" | head -5 FAILED_PACKAGES+=("$to") + rm -f "$err_file" return 1 } # Order: top-level meta-packages first (ffmpeg pulls the libav* family), -# then GPU driver swaps, then the standalone libavcodec/gstreamer adders. -dnf_codec_swap "ffmpeg-free" "ffmpeg" -dnf_codec_swap "mesa-va-drivers" "mesa-va-drivers-freeworld" -dnf_codec_swap "mesa-vulkan-drivers" "mesa-vulkan-drivers-freeworld" -dnf_codec_swap "libavcodec-free" "libavcodec-freeworld" -dnf_codec_swap "gstreamer1-plugins-bad-free" "gstreamer1-plugins-bad-freeworld" - -# vvenc and svt-av1 round out the encoder set. They have no -free counterpart -# (RPMFusion-only), so a plain install is correct — it co-resolves with the -# freeworld libavcodec just installed, guaranteeing matching sonames. +# then GPU driver swaps. libavcodec-freeworld is a parallel-library package +# only meaningful when the system runs Fedora's `ffmpeg-free` — Bazzite +# ships RPMFusion's `ffmpeg` which bundles patent codecs in its own +# libavcodec, making libavcodec-freeworld redundant (and uninstallable +# because of file conflicts with the bundled libs). The swap below is a +# best-effort no-op in that common case; it only does work on bases that +# actually shipped Fedora's `libavcodec-free`. +dnf_codec_swap "ffmpeg-free" "ffmpeg" || true +dnf_codec_swap "mesa-va-drivers" "mesa-va-drivers-freeworld" || true +dnf_codec_swap "mesa-vulkan-drivers" "mesa-vulkan-drivers-freeworld" || true +dnf_codec_swap "libavcodec-free" "libavcodec-freeworld" || true + +# gstreamer1-plugins-bad-freeworld: additive install (NOT a swap — see comment +# above CODEC_SWAP_ALLOWLIST). Provides x265enc, de265, rtmp. The base bad-free +# package (which ships `va`/VAAPI) must stay installed. +log_info "Installing gstreamer1-plugins-bad-freeworld (additive on top of bad-free)" +gst_err=$(mktemp) +if command dnf5 -y install gstreamer1-plugins-bad-freeworld >/dev/null 2>"$gst_err"; then + log_success " ✓ gstreamer1-plugins-bad-freeworld" + SUCCEEDED_PACKAGES+=("gstreamer1-plugins-bad-freeworld") +else + log_warning " ✗ gstreamer1-plugins-bad-freeworld (dnf says:)" + sed 's/^/ /' "$gst_err" | head -5 + FAILED_PACKAGES+=("gstreamer1-plugins-bad-freeworld") +fi +rm -f "$gst_err" + +# vvenc and svt-av1 round out the encoder set. RPMFusion-only (no -free +# counterpart), so a plain install is correct. Co-resolves with the +# freeworld libavcodec/ffmpeg just installed. log_info "Installing standalone codec libraries (vvenc, svt-av1)" -if command dnf5 -y install vvenc vvenc-libs svt-av1-libs &>/dev/null; then +vvenc_err=$(mktemp) +if command dnf5 -y install vvenc vvenc-libs svt-av1-libs >/dev/null 2>"$vvenc_err"; then log_success " ✓ vvenc, vvenc-libs, svt-av1-libs" SUCCEEDED_PACKAGES+=("vvenc" "vvenc-libs" "svt-av1-libs") else - log_warning " ✗ vvenc/svt-av1 install failed (codec encoders may be missing)" + log_warning " ✗ vvenc/svt-av1 install failed (dnf says:)" + sed 's/^/ /' "$vvenc_err" | head -5 FAILED_PACKAGES+=("vvenc") fi +rm -f "$vvenc_err" # ────────────────────────────────────────────────────────────────────────── # COPR Repository Packages @@ -727,7 +762,12 @@ readonly -a CRITICAL_PACKAGES=( "mesa-libEGL" "mesa-libEGL-devel" "ffmpeg" - "libavcodec-freeworld" # codec stack: patent-encumbered encoders + # libavcodec-freeworld is intentionally NOT here: when ffmpeg is the + # RPMFusion freeworld variant (Bazzite default), it bundles its own + # libavcodec with patent codecs, making libavcodec-freeworld redundant + # and uninstallable due to file conflicts. The codec validator in + # 08-validate.sh asserts the actual encoders exist regardless of which + # package provides them. "vvenc-libs" # H.266/VVC encoder runtime "gstreamer1-plugins-bad-freeworld" "mutter" @@ -817,7 +857,7 @@ readonly -a VERSIONLOCK_PACKAGES=( "mesa-libEGL" "mesa-libEGL-devel" "ffmpeg" - "libavcodec-freeworld" + # libavcodec-freeworld omitted — see CRITICAL_PACKAGES for rationale "vvenc" "vvenc-libs" "gstreamer1-plugins-bad-freeworld" diff --git a/build_files/08-validate.sh b/build_files/08-validate.sh index 0334278..ab93de6 100755 --- a/build_files/08-validate.sh +++ b/build_files/08-validate.sh @@ -267,21 +267,21 @@ if [[ $codec_link_failed -eq 0 && -x /usr/bin/ffmpeg ]]; then readonly -a TIER3_ENCODERS=(h264_vaapi hevc_vaapi av1_vaapi) for enc in "${TIER1_ENCODERS[@]}"; do - if grep -qE "^ [VA]\.\.\.\.\. ${enc} " <<<"$ffmpeg_out"; then + if grep -qE "^ [VAS][.A-Z]{5} ${enc}( |$)" <<<"$ffmpeg_out"; then log_success " ✓ encoder: $enc" else fail "encoder missing (Tier 1, hard): $enc" fi done for enc in "${TIER2_ENCODERS[@]}"; do - if grep -qE "^ [VA]\.\.\.\.\. ${enc} " <<<"$ffmpeg_out"; then + if grep -qE "^ [VAS][.A-Z]{5} ${enc}( |$)" <<<"$ffmpeg_out"; then log_success " ✓ encoder: $enc" else log_warning " ? encoder missing (Tier 2, soft): $enc" fi done for enc in "${TIER3_ENCODERS[@]}"; do - if grep -qE "^ [VA]\.\.\.\.\. ${enc} " <<<"$ffmpeg_out"; then + if grep -qE "^ [VAS][.A-Z]{5} ${enc}( |$)" <<<"$ffmpeg_out"; then log_success " ✓ encoder: $enc" else fail "encoder missing (Tier 3, hard — AMD GPU encode): $enc" From 11e7f0a03e3cad3b2c7aad09109e904502bb806d Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:42:14 -0600 Subject: [PATCH 03/13] Constrain freeworld codec libs to x86_64; tolerate pre-installed vvenc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second testing build surfaced two new failure modes: 1. F44 RPMFusion's i686 builds of libavcodec-freeworld (8.0.1-6) and gstreamer1-plugins-bad-freeworld (1.28.1-3) are stale — they require libx265.so.215 which the current x265 no longer provides (now .216). The x86_64 builds are fresh and link correctly. Bazzite pulls i686 by default for Steam multilib, so a bare install pulls both arches and aborts on the stale i686. Pin both to .x86_64 — no 32-bit userland needs patent codec libraries. 2. vvenc-libs is a transitive dep of ffmpeg and was already installed by the time we reached the explicit `dnf install vvenc vvenc-libs svt-av1-libs` line. dnf5 aborts the entire transaction with "Failed to resolve" when any requested package is a no-op, even if others would install successfully. Pre-filter against `rpm -q` so we only ask dnf for packages it can actually install. The dnf_codec_swap function now accepts an optional arch suffix that only applies to its direct-install fallback path; the swap path itself remains arch-agnostic since it inherits whatever arches the source had. Co-Authored-By: Claude Opus 4.7 --- build_files/02-build.sh | 68 +++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 20 deletions(-) diff --git a/build_files/02-build.sh b/build_files/02-build.sh index 815c4ab..c217c01 100755 --- a/build_files/02-build.sh +++ b/build_files/02-build.sh @@ -566,6 +566,14 @@ readonly -a CODEC_SWAP_ALLOWLIST=( dnf_codec_swap() { local from="$1" to="$2" + # Optional third arg: arch suffix for the direct-install fallback path + # (e.g. "x86_64"). Some F44 RPMFusion freeworld packages ship a stale i686 + # build that requires a since-rotated x265 soname (libx265.so.215). The + # x86_64 build is current; constraining the install to x86_64 sidesteps + # the broken multilib pull on Bazzite (which enables i686 for Steam). + # Only applied when source isn't installed — the swap path leaves arch + # selection to dnf because it matches whatever arches the source had. + local arch_pref="${3:-}" local pair="${from}|${to}" local allowed=false @@ -586,13 +594,14 @@ dnf_codec_swap() { # This handles bases that already ship the freeworld variant, or where # the -free counterpart was never installed. if ! rpm -q "$from" &>/dev/null; then - if command dnf5 -y install "$to" >/dev/null 2>"$err_file"; then - log_success " ✓ $to (direct install — no $from present)" + local install_target="$to${arch_pref:+.$arch_pref}" + if command dnf5 -y install "$install_target" >/dev/null 2>"$err_file"; then + log_success " ✓ $install_target (direct install — no $from present)" SUCCEEDED_PACKAGES+=("$to") rm -f "$err_file" return 0 fi - log_warning " ✗ $to (direct install failed; dnf says:)" + log_warning " ✗ $install_target (direct install failed; dnf says:)" sed 's/^/ /' "$err_file" | head -5 FAILED_PACKAGES+=("$to") rm -f "$err_file" @@ -624,40 +633,59 @@ dnf_codec_swap() { # because of file conflicts with the bundled libs). The swap below is a # best-effort no-op in that common case; it only does work on bases that # actually shipped Fedora's `libavcodec-free`. -dnf_codec_swap "ffmpeg-free" "ffmpeg" || true -dnf_codec_swap "mesa-va-drivers" "mesa-va-drivers-freeworld" || true -dnf_codec_swap "mesa-vulkan-drivers" "mesa-vulkan-drivers-freeworld" || true -dnf_codec_swap "libavcodec-free" "libavcodec-freeworld" || true +dnf_codec_swap "ffmpeg-free" "ffmpeg" || true +dnf_codec_swap "mesa-va-drivers" "mesa-va-drivers-freeworld" || true +dnf_codec_swap "mesa-vulkan-drivers" "mesa-vulkan-drivers-freeworld" || true +# libavcodec-freeworld constrained to x86_64: F44 RPMFusion's i686 build +# (8.0.1-6.fc44) requires libx265.so.215, but the live x265 has moved to .216. +# No 32-bit userland needs the patent-codec libavcodec; Steam et al. use +# system ffmpeg-libs.i686 from base Fedora. +dnf_codec_swap "libavcodec-free" "libavcodec-freeworld" "x86_64" || true # gstreamer1-plugins-bad-freeworld: additive install (NOT a swap — see comment # above CODEC_SWAP_ALLOWLIST). Provides x265enc, de265, rtmp. The base bad-free # package (which ships `va`/VAAPI) must stay installed. -log_info "Installing gstreamer1-plugins-bad-freeworld (additive on top of bad-free)" +# +# Pinned to .x86_64 for the same reason as libavcodec-freeworld above — +# the F44 i686 RPM (1.28.1-3.fc44) is stale against the current x265 ABI. +log_info "Installing gstreamer1-plugins-bad-freeworld.x86_64 (additive on top of bad-free)" gst_err=$(mktemp) -if command dnf5 -y install gstreamer1-plugins-bad-freeworld >/dev/null 2>"$gst_err"; then - log_success " ✓ gstreamer1-plugins-bad-freeworld" +if command dnf5 -y install gstreamer1-plugins-bad-freeworld.x86_64 >/dev/null 2>"$gst_err"; then + log_success " ✓ gstreamer1-plugins-bad-freeworld.x86_64" SUCCEEDED_PACKAGES+=("gstreamer1-plugins-bad-freeworld") else - log_warning " ✗ gstreamer1-plugins-bad-freeworld (dnf says:)" + log_warning " ✗ gstreamer1-plugins-bad-freeworld.x86_64 (dnf says:)" sed 's/^/ /' "$gst_err" | head -5 FAILED_PACKAGES+=("gstreamer1-plugins-bad-freeworld") fi rm -f "$gst_err" # vvenc and svt-av1 round out the encoder set. RPMFusion-only (no -free -# counterpart), so a plain install is correct. Co-resolves with the -# freeworld libavcodec/ffmpeg just installed. +# counterpart). Pre-filter against rpm -q so dnf5 doesn't abort the whole +# transaction with "Failed to resolve" when packages are already installed +# as transitive deps of ffmpeg (which is the common case on Bazzite). log_info "Installing standalone codec libraries (vvenc, svt-av1)" -vvenc_err=$(mktemp) -if command dnf5 -y install vvenc vvenc-libs svt-av1-libs >/dev/null 2>"$vvenc_err"; then - log_success " ✓ vvenc, vvenc-libs, svt-av1-libs" +vvenc_pkgs=() +for pkg in vvenc vvenc-libs svt-av1-libs; do + if ! rpm -q "$pkg" &>/dev/null; then + vvenc_pkgs+=("$pkg") + fi +done +if [[ ${#vvenc_pkgs[@]} -eq 0 ]]; then + log_success " ✓ vvenc, vvenc-libs, svt-av1-libs (already installed via deps)" SUCCEEDED_PACKAGES+=("vvenc" "vvenc-libs" "svt-av1-libs") else - log_warning " ✗ vvenc/svt-av1 install failed (dnf says:)" - sed 's/^/ /' "$vvenc_err" | head -5 - FAILED_PACKAGES+=("vvenc") + vvenc_err=$(mktemp) + if command dnf5 -y install "${vvenc_pkgs[@]}" >/dev/null 2>"$vvenc_err"; then + log_success " ✓ ${vvenc_pkgs[*]}" + SUCCEEDED_PACKAGES+=("${vvenc_pkgs[@]}") + else + log_warning " ✗ ${vvenc_pkgs[*]} (dnf says:)" + sed 's/^/ /' "$vvenc_err" | head -5 + FAILED_PACKAGES+=("${vvenc_pkgs[@]}") + fi + rm -f "$vvenc_err" fi -rm -f "$vvenc_err" # ────────────────────────────────────────────────────────────────────────── # COPR Repository Packages From f0222b3216f5645b74f4276e403b15e91aaaa764 Mon Sep 17 00:00:00 2001 From: Phantom Date: Wed, 10 Jun 2026 08:41:32 -0600 Subject: [PATCH 04/13] Update packages.txt --- .github/force-install-builder/packages.txt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/force-install-builder/packages.txt b/.github/force-install-builder/packages.txt index 175dac2..053c74c 100644 --- a/.github/force-install-builder/packages.txt +++ b/.github/force-install-builder/packages.txt @@ -23,10 +23,6 @@ # audacity-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates mutter-devel | dnf | terra -# Codec stack (libavcodec-freeworld, gstreamer1-plugins-bad-freeworld, -# mesa-*-freeworld, vvenc-libs) is no longer cached here — those packages -# link against versioned x265/vvenc symbols, and snapshotting them in this -# artifact produces ABI skew against the rolling Bazzite base. They are -# installed atomically in the same dnf transaction by build_files/02-build.sh. +gstreamer1-plugins-bad-freeworld | dnf | rpmfusion-free,rpmfusion-free-updates gnome-rounded-blur | github-build | kancko/gnome-rounded-blur glycin-fix-bc7 | github-release | phantomcortex/glycin From 9914f6be89060e7226b542d2355de6afc5cd5767 Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:35:28 -0600 Subject: [PATCH 05/13] Drop versionlock leftover, fix set +eu, prune validation cruft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 00-kernel.sh: remove `dnf5 versionlock add kernel-cachyos-lto` — no-op on ostree (already documented in 02-build.sh). 07-config.sh: drop the global `set +eu` flip-without-restore; switch the two `((counter++))` increments to arithmetic assign so the script keeps `set -euo pipefail` end-to-end. Also drops the unused "hide incompatible Bazzite recipes" section. 08-validate.sh: remove the broken "Experimential" ldconfig stanza (truncated path, `ldconfig -i` misuse), plus the GDK pixbuf / GLib schemas / font registration / x265enc checks that overlapped with 02-build.sh's own validation. Co-Authored-By: Claude Opus 4.7 --- build_files/00-kernel.sh | 1 - build_files/07-config.sh | 30 +------------ build_files/08-validate.sh | 88 +------------------------------------- 3 files changed, 3 insertions(+), 116 deletions(-) diff --git a/build_files/00-kernel.sh b/build_files/00-kernel.sh index 7b86e60..5a010b6 100755 --- a/build_files/00-kernel.sh +++ b/build_files/00-kernel.sh @@ -21,4 +21,3 @@ popd dnf5 -y copr enable bieszczaders/kernel-cachyos-lto dnf5 -y install kernel-cachyos-lto kernel-cachyos-lto-devel-matched dnf5 -y copr disable bieszczaders/kernel-cachyos-lto -dnf5 versionlock add kernel-cachyos-lto diff --git a/build_files/07-config.sh b/build_files/07-config.sh index 275665e..d72b5ba 100755 --- a/build_files/07-config.sh +++ b/build_files/07-config.sh @@ -87,29 +87,6 @@ else log_error "Universal Blue justfile not found at $UBLUE_JUSTFILE" fi -# Hide incompatible Bazzite just recipes by prefixing with underscore -# Reason: Certain Bazzite recipes conflict with DistinctionOS configuration -log_section "Hiding incompatible Bazzite recipes" - -readonly -a INCOMPATIBLE_RECIPES=( - "install-coolercontrol" # Conflicts with our thermal management - #"install-openrgb" # Conflicts with our RGB control setup -) - -for recipe in "${INCOMPATIBLE_RECIPES[@]}"; do - log_info "Hiding recipe: $recipe" - - # Find which just file contains this recipe - recipe_file=$(grep -l "^$recipe:" /usr/share/ublue-os/just/*.just 2>/dev/null || true) - - if [[ -n "$recipe_file" ]]; then - # Prefix recipe name with underscore to hide it - sed -i "s/^$recipe:/_$recipe:/" "$recipe_file" - log_success "Recipe '$recipe' hidden" - else - log_warning "Recipe '$recipe' not found in any just file" - fi -done # ============================================================================ # Application Customization @@ -172,9 +149,6 @@ fi log_section "Updating system caches" -#some of the steps here are excessively error prone, thus breaking the build constantly -set +eu - # Icon cache refresh (required after Kora theme installation) log_info "Updating icon cache" if gtk-update-icon-cache -f /usr/share/icons/kora; then @@ -250,7 +224,7 @@ for desktop_file in "${WINE_DESKTOP_FILES[@]}"; do desktop_path="/usr/share/applications/$desktop_file" if [[ -e "$desktop_path" ]]; then rm -f "$desktop_path" - ((wine_removed++)) + wine_removed=$((wine_removed + 1)) fi done @@ -284,7 +258,7 @@ for file_path in "${!BAZZITE_FILES[@]}"; do log_info "Removing: $file_path" log_info " Reason: $reason" rm -f "$file_path" - ((bazzite_removed++)) + bazzite_removed=$((bazzite_removed + 1)) fi done diff --git a/build_files/08-validate.sh b/build_files/08-validate.sh index ab93de6..73c2371 100755 --- a/build_files/08-validate.sh +++ b/build_files/08-validate.sh @@ -95,76 +95,6 @@ for theme_dir in "${ICON_THEMES[@]}"; do fi done -# ── GDK pixbuf loaders ────────────────────────────────────────────────────── -# A missing SVG loader is the most likely cause of icons rendering as blank -# squares — Adwaita icons are SVG, and without the loader they can't paint. -log_section "GDK pixbuf loaders" - -pixbuf_update_output=$(gdk-pixbuf-query-loaders-64 --update-cache 2>&1) -pixbuf_update_rc=$? -if [[ $pixbuf_update_rc -eq 0 ]]; then - loader_count=$(gdk-pixbuf-query-loaders-64 2>/dev/null | grep -c '^"' || true) - if [[ "$loader_count" -gt 0 ]]; then - log_success " ✓ $loader_count loaders registered" - else - fail "No pixbuf loaders registered" - fi - - if gdk-pixbuf-query-loaders-64 2>/dev/null | grep -q "image/svg"; then - log_success " ✓ SVG loader registered" - else - fail "SVG pixbuf loader not registered — icons will render as blank squares" - fi - - if gdk-pixbuf-query-loaders-64 2>/dev/null | grep -q "image/png"; then - log_success " ✓ PNG loader registered" - else - fail "PNG pixbuf loader not registered" - fi -else - fail "gdk-pixbuf-query-loaders-64 --update-cache failed (exit code $pixbuf_update_rc)" - printf '%s\n' "$pixbuf_update_output" | head -5 | while read -r line; do - echo " $line" - done -fi - -# ── GLib schemas ──────────────────────────────────────────────────────────── -log_section "GLib schemas" - -schema_out=$(glib-compile-schemas /usr/share/glib-2.0/schemas/ 2>&1 || true) -if echo "$schema_out" | grep -qiE 'error|conflict'; then - fail "GLib schema compilation reported issues:" - echo "$schema_out" | head -5 | while read -r line; do - echo " $line" - done -else - log_success " ✓ Schemas compiled clean" -fi - -# ── Font registration ─────────────────────────────────────────────────────── -log_section "Font registration" - -fc-cache -f &>/dev/null - -font_count=$(fc-list | wc -l) -if [[ "$font_count" -lt 10 ]]; then - fail "Only $font_count fonts registered — fontconfig is likely broken" -else - log_success " ✓ $font_count fonts registered" -fi - -# Fonts 02-build.sh explicitly installs — if missing, the install step failed. -readonly -a EXPECTED_FONTS=( - "Inter" - "Cantarell" -) -for font in "${EXPECTED_FONTS[@]}"; do - if fc-list | grep -qi "$font"; then - log_success " ✓ $font" - else - log_warning " ? $font not registered (soft warning)" - fi -done # ── Critical binary link integrity ────────────────────────────────────────── # Catches the case where a package is installed but a runtime dep was removed, @@ -197,16 +127,7 @@ for bin in "${CRITICAL_BINS[@]}"; do done # ── Codec stack integrity ─────────────────────────────────────────────────── -# Catches the ABI-skew class of bug where libavcodec-freeworld links against -# x265/vvenc symbol versions that no longer exist on the live system. Used -# to be a runtime failure (ffmpeg refusing to launch, GStreamer pipelines -# erroring out); now a build failure with a clear diagnostic. -# -# Three layers, cheap to expensive: -# 1. ldd on ffmpeg/ffprobe + every libav*.so — catches missing sonames -# 2. ffmpeg -encoders — catches versioned-symbol failures ldd can't see -# (e.g. "undefined symbol: x265_api_get_215") -# 3. gst-inspect of va/x265enc plugins — catches GStreamer-side regressions + log_section "Codec stack integrity" # ─ Layer 1: dynamic linker can resolve every soname ─ @@ -302,13 +223,6 @@ if command -v gst-inspect-1.0 &>/dev/null; then else fail "GStreamer 'va' plugin missing — VAAPI HW decode/encode unavailable" fi - # x265enc lives in gstreamer1-plugins-bad-freeworld; its absence usually - # means the freeworld swap failed silently in 02-build.sh. - if gst-inspect-1.0 x265enc &>/dev/null; then - log_success " ✓ GStreamer x265enc present" - else - fail "GStreamer x265enc missing — gstreamer1-plugins-bad-freeworld likely failed" - fi fi # ── Final report ──────────────────────────────────────────────────────────── From fc6bd0209c8eec9605ececa991c5324f036672b2 Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:39:54 -0600 Subject: [PATCH 06/13] Prune dead code; rewrite README from draft merge - Delete build_files/wine-installer.sh (~280 lines, no caller in the Containerfile RUN chain or anywhere else in the repo). - Drop counter_init / counter_increment from 95-utility-functions.sh (no call sites). counter_display is retained for 02-build.sh:280. - Replace README.md with a merge of the option-a + option-c drafts, dropping the CachyOS kernel and freeworld Mesa feature copy and the stale Build Architecture script tree. Removes README-option-{a,b,c}.md. - Delete docs/claude.md (legacy AI-context doc; CLAUDE.md + .claude/ skills/ now cover this role) and drop its README link. Co-Authored-By: Claude Opus 4.7 --- README.md | 138 +++--- build_files/95-utility-functions.sh | 41 +- build_files/wine-installer.sh | 283 ------------ docs/claude.md | 662 ---------------------------- 4 files changed, 97 insertions(+), 1027 deletions(-) delete mode 100755 build_files/wine-installer.sh delete mode 100644 docs/claude.md diff --git a/README.md b/README.md index ee244d0..8629fb9 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,110 @@ +
+ # DistinctionOS -> A fully-featured [Bazzite](https://github.com/ublue-os/bazzite)-based gaming and development environment +**A curated, immutable desktop for gamers and creators — built on Bazzite.** -[![Built on Bazzite](https://img.shields.io/badge/Built%20on-Bazzite-blue)](https://github.com/ublue-os/bazzite) -[![Fedora Atomic](https://img.shields.io/badge/Fedora-Atomic-51A2DA)](https://fedoraproject.org/) -[![License](https://img.shields.io/github/license/phantomcortex/distinctionos)](./LICENSE) +[![Built on Bazzite](https://img.shields.io/badge/Built%20on-Bazzite-blue?style=flat-square)](https://github.com/ublue-os/bazzite) +[![Fedora Atomic](https://img.shields.io/badge/Fedora-Atomic-51A2DA?style=flat-square)](https://fedoraproject.org/) +[![License](https://img.shields.io/github/license/phantomcortex/distinctionos?style=flat-square)](./LICENSE) +[![Build](https://img.shields.io/github/actions/workflow/status/phantomcortex/distinctionos/build.yml?style=flat-square)](https://github.com/phantomcortex/distinctionos/actions) -**DistinctionOS** embraces the philosophy of abundance. a curated experience built for creators, gamers, and tinkerers who want it all, out of the box. +
--- -## 🚀 Quick Start +DistinctionOS is an opinionated layer on top of [Bazzite](https://github.com/ublue-os/bazzite) — a Fedora Atomic gaming desktop. It ships original system tooling, GNOME Shell extensions, and a curated application set so you have a complete, ready-to-use system with nothing left to hunt down. -Rebase your existing system to DistinctionOS: +Images are built every 5 days, signed with Cosign, and pushed to GHCR. -```bash -# Using bootc (recommended) -sudo bootc switch ghcr.io/phantomcortex/distinctionos +## Installation -# Using rpm-ostree -rpm-ostree rebase ostree-unverified-registry:docker://ghcr.io/phantomcortex/distinctionos:latest +```bash +sudo bootc switch ghcr.io/phantomcortex/distinctionos:latest ``` -After reboot, run the first-time setup: +After rebooting, run post-install setup: + ```bash ujust distinction-install ``` ---- - -## ✨ What's Included - -### 🎮 Gaming Enhancements -- **kernel-cachyos-lto** - Custom Kernel for the best gaming experience possible. +This installs Flatpaks, Homebrew packages, ZSH, and NvChad in a single command. -### 🎨 Creative Tools -- **Cider** - Premium Apple Music client ([requires license](https://cidercollective.itch.io/cider)) -- **Audacity Freeworld** - Full codec support for audio editing -- **dcraw** - RAW image format support with thumbnail generation -- **ImageMagick DDS thumbnailer** - Texture file previews +## Features -### 🛠️ Development & Productivity -- Docker & Docker Compose -- Flatpak Builder -- FreeRDP (for remote Windows applications) -- Pandoc (universal document converter) +| Component | Description | +|-----------|-------------| +| **Glycin BC7 Patch** | DDS texture thumbnails for BC4/5/6H/BC7 formats not covered by upstream | +| **Steam Linker** | Systemd service unifying all Steam library locations under `~/Games/Steamlibrary/` | +| **XWM Player** | Transparent playback of Bethesda game audio formats (`.xwm`, `.fuz`) | +| **ZSH System-Wide** | Oh My Zsh + Powerlevel10k configured at the system level | +| **GNOME Extensions** | Pre-installed: Dash to Dock, Burn My Windows, TopHat, and more | +| **Brave Browser** | Ships as the default browser | +| **Cider** | Premium Apple Music client (license required) | +| **CrossOver** | Commercial Wine implementation for Windows applications | +| **Rechunker** | Efficient layer compression for faster, smaller OTA updates | +| **Cosign Signing** | Every image signed and verifiable with the included public key | -### 📦 Quality of Life -- **zoxide** - Smart directory navigation (`z` command) -- **dysk** - Modern disk usage analyzer -- **advcp/advmv** - Progress bars for copy/move operations -- Video thumbnail generation -- HEIC image format support +## What Makes It Different ---- +### Glycin — BC7 DDS Patch -## 🔑 Key Features +Stock Fedora `glycin-*` packages are replaced with [`phantomcortex/glycin`](https://github.com/phantomcortex/glycin) release RPMs (`v2.1.1-bc7fix1`). Adds BC4, BC5, BC6H, and BC7 DDS compression support to `glycin-image-rs`, enabling thumbnail generation in Nautilus for compressed game textures. Upstream glycin ships BC1/BC2/BC3 only. The release version sorts above Fedora's `1.fc44` for a clean upgrade. -| Feature | Description | -|---------|-------------| -| **Immutable Base** | Built on Fedora Atomic for reliability and easy rollback | -| **Automatic Updates** | Fresh images built every 5 days with latest packages | -| **TPM Auto-Unlock** | Optional LUKS unlock with TPM 2.0 (setup via `ujust distinction-tpm-unlock-setup`) | -| **ZSH by Default** | Oh My Zsh + Powerlevel10k configured automatically | -| **Just Recipes** | Simple commands for common tasks (`ujust` for menu) | +### Steam Linker ---- +A systemd user service that parses Steam's `libraryfolders.vdf` and maintains a symlink tree under `~/Games/Steamlibrary/` covering all detected library paths. State-tracked in JSON for restoration after deletion. Exclusion patterns filter Proton runtimes, GE-Proton, and SteamLinuxRuntime by default. -## 🎯 Philosophy +```bash +ujust steam-link-enable # Run at login +ujust steam-link # Run now +ujust steam-link-status # Current state +``` -**DistinctionOS is for those who want:** -- A complete, ready-to-use system without manual configuration -- Gaming performance with development tools side-by-side -- Professional applications without hunting through repositories +See [docs/steam-linker.md](./docs/steam-linker.md). -If you prefer minimalism and building from scratch, this isn't your distribution—and that's perfectly fine. +### XWM Player ---- +`/usr/bin/xwm-player` converts `.xwm` (xWMA) and `.fuz` (Bethesda voice container) to WAV on the fly via FFmpeg and opens the result in the configured audio player. MIME types are registered for transparent file-manager double-click. Flatpak-aware; defaults to `org.gnome.Decibels`. FUZ handling strips the lip-sync header before passing the embedded xWMA to FFmpeg. -## 📚 Documentation +See [docs/xwm-player.md](./docs/xwm-player.md). -- **[Developer Documentation](./docs/developer.md)** - Build process, architecture, contributing -- **[AI Assistant Context](./docs/claude.md)** - Complete project context for AI tools +## Post-Install Recipes ---- +```bash +ujust distinction-install # Full setup +ujust distinction-install-flatpaks # Flatpaks only +ujust distinction-install-brews # Homebrew packages only +ujust distinction-install-custom-shell # ZSH + dotfiles +ujust distinction-install-nvchad # NvChad for user and root +ujust steam-link-enable # Steam Linker at login +``` -## 🙏 Acknowledgements +See [docs/ujust-recipes.md](./docs/ujust-recipes.md). -Special Thanks: +## Verification -- **[Fedora Project](https://fedoraproject.org)** - The foundation of it all -- **[Universal Blue](https://github.com/ublue-os)** - Bazzite, Bluefin, and the image template -- **[Bazzite](https://github.com/ublue-os/bazzite)** - The direct parent of this project -- **Community Examples**: [Amy OS](https://github.com/astrovm/amyos), [vst-name's Aurora DX](https://github.com/vst-name/ublue-aurora-dx), [m2OS](https://github.com/m2giles/m2os), [bOS](https://github.com/bsherman/bos), [Homer](https://github.com/bketelsen/homer/), [VeneOS](https://github.com/Venefilyn/veneos) +```bash +cosign verify --key cosign.pub ghcr.io/phantomcortex/distinctionos:latest +``` +## Documentation ---- +| Document | Contents | +|----------|----------| +| [Developer Guide](./docs/developer.md) | Build architecture, script reference, contributing | +| [ujust Recipes](./docs/ujust-recipes.md) | All `ujust` commands and their purpose | +| [Steam Linker](./docs/steam-linker.md) | Game library symlink manager | +| [XWM Player](./docs/xwm-player.md) | Bethesda audio format player | -
+## Acknowledgements -**Built with ❤️ by [phantomcortex](https://github.com/phantomcortex)** +- **[Universal Blue](https://github.com/ublue-os)** — Bazzite, Bluefin, and the image template +- **[Bazzite](https://github.com/ublue-os/bazzite)** — The direct upstream +- **[Fedora Project](https://fedoraproject.org)** — The foundation +- Community: [AmyOS](https://github.com/astrovm/amyos) · [Aurora DX](https://github.com/vst-name/ublue-aurora-dx) · [m2OS](https://github.com/m2giles/m2os) · [bOS](https://github.com/bsherman/bos) · [Homer](https://github.com/bketelsen/homer) · [VeneOS](https://github.com/Venefilyn/veneos) -*For those who want it all* +--- -
+
Built by phantomcortex
diff --git a/build_files/95-utility-functions.sh b/build_files/95-utility-functions.sh index 8ce40b7..15ed04b 100755 --- a/build_files/95-utility-functions.sh +++ b/build_files/95-utility-functions.sh @@ -242,21 +242,6 @@ run_silent_with_log() { # Counter Functions # ============================================================================ -# Initialize a counter variable -# Usage: counter_init -# Returns: 0 -counter_init() { - echo "0" -} - -# Increment a counter -# Usage: count=$(counter_increment "$count") -# Returns: incremented value -counter_increment() { - local current="${1:-0}" - echo $((current + 1)) -} - # Display counter result with appropriate singular/plural # Usage: counter_display "$count" "item" "items" "action" # Example: counter_display "$removed" "file" "files" "removed" @@ -301,6 +286,32 @@ get_kernel_version() { dnf5 repoquery --installed --queryformat='%{evr}.%{arch}' kernel 2>/dev/null | head -1 } +# ============================================================================ +# Guard: disallow `--allowerasing` +# ============================================================================ +# --allowerasing lets dnf silently remove packages to satisfy an install, which +# can clobber required packages. Wrap dnf/dnf5 so any future use is rejected. +dnf() { + local arg + for arg in "$@"; do + if [[ "$arg" == "--allowerasing" ]]; then + log_error "Refusing to run dnf with --allowerasing (disallowed in this build)" + return 1 + fi + done + command dnf "$@" +} + +dnf5() { + local arg + for arg in "$@"; do + if [[ "$arg" == "--allowerasing" ]]; then + log_error "Refusing to run dnf5 with --allowerasing (disallowed in this build)" + return 1 + fi + done + command dnf5 "$@" +} # ============================================================================ # Script Lifecycle Functions # ============================================================================ diff --git a/build_files/wine-installer.sh b/build_files/wine-installer.sh deleted file mode 100755 index dbcfbf6..0000000 --- a/build_files/wine-installer.sh +++ /dev/null @@ -1,283 +0,0 @@ -#!/bin/bash - -# To Whomst, it may concern... -# The code below was generated by claude sonnet 4.. -# yes I know, 'vibe code bad!' -# But I (at the time of writing) don't have a ton of scripting knowledge -# just ambition, and a passion to create something and learn along the way - - - -# Wine Build Downloader and Extractor -# Downloads latest wine-staging-tkg build from Kron4ek/Wine-Builds -# Extracts to /usr whilst excluding unwanted files - -set -euo pipefail - -# Configuration -readonly GITHUB_API_URL="https://api.github.com/repos/Kron4ek/Wine-Builds/releases" -readonly WINE_PATTERN="wine-.*-staging-tkg-amd64-wow64\.tar\.xz" -readonly EXTRACTION_TARGET="/usr" -readonly TEMP_DIR="/tmp/wine-build-$" -readonly EXCLUDED_FILE="wine-tkg-config.txt" - -# Version configuration -# Uncomment and set to install a specific version (e.g., "10.12") -# readonly WINE_VERSION="10.12" - -# Release candidate configuration -readonly USE_RELEASE_CANDIDATE=false - -# Logging functions -log_info() { - echo "[INFO] $*" >&2 -} - -log_error() { - echo "[ERROR] $*" >&2 -} - -log_warn() { - echo "[WARN] $*" >&2 -} - -# Cleanup function -cleanup() { - if [[ -d "${TEMP_DIR}" ]]; then - log_info "Cleaning up temporary directory: ${TEMP_DIR}" - rm -rf "${TEMP_DIR}" - fi -} - -# Set trap for cleanup -trap cleanup EXIT - -# Version comparison function (semantic versioning aware) -version_compare() { - local version1="$1" - local version2="$2" - - # Extract major and minor versions - local v1_major v1_minor v2_major v2_minor - IFS='.' read -r v1_major v1_minor <<< "$version1" - IFS='.' read -r v2_major v2_minor <<< "$version2" - - # Compare major versions - if [[ $v1_major -gt $v2_major ]]; then - return 0 # version1 > version2 - elif [[ $v1_major -lt $v2_major ]]; then - return 1 # version1 < version2 - fi - - # Major versions equal, compare minor versions - if [[ $v1_minor -gt $v2_minor ]]; then - return 0 # version1 > version2 - else - return 1 # version1 <= version2 - fi -} - -# Extract version from filename -extract_wine_version() { - local filename="$1" - echo "$filename" | sed -n 's/^wine-\([0-9]\+\.[0-9]\+\).*$/\1/p' -} -check_prerequisites() { - local missing_tools=() - - for tool in curl jq tar; do - if ! command -v "$tool" &> /dev/null; then - missing_tools+=("$tool") - fi - done - - if [[ ${#missing_tools[@]} -ne 0 ]]; then - log_error "Missing required tools: ${missing_tools[*]}" - log_error "Please install with: dnf5 install ${missing_tools[*]}" - exit 1 - fi - - if [[ $EUID -ne 0 ]]; then - log_error "This script requires root privileges to extract to ${EXTRACTION_TARGET}" - exit 1 - fi -} - -# Fetch latest Wine build information -fetch_wine_build_info() { - log_info "Fetching release information from GitHub API" - - local releases_data - if ! releases_data=$(curl -sf "$GITHUB_API_URL"); then - log_error "Failed to fetch releases information from GitHub API" - exit 1 - fi - - # Debug: Check if we got valid JSON - if ! echo "$releases_data" | jq empty 2>/dev/null; then - log_error "Invalid JSON received from GitHub API" - log_error "Response preview: $(echo "$releases_data" | head -c 200)..." - exit 1 - fi - - # Process all releases to find suitable wine builds - log_info "Processing releases to find Wine builds..." - local wine_candidates - wine_candidates=$(echo "$releases_data" | jq -c --arg pattern "$WINE_PATTERN" ' - [.[] | - .assets[]? | - select(.name | test($pattern)) | - select(.name | test("proton"; "i") | not) | - { - name: .name, - download_url: .browser_download_url, - is_rc: (.name | test("rc"; "i")), - version: (.name | capture("wine-(?[0-9]+\\.[0-9]+)"; "g") | .ver // "0.0") - }] | - sort_by(.version | split(".") | map(tonumber | . // 0)) | - reverse - ') - - # Debug output - local candidate_count - candidate_count=$(echo "$wine_candidates" | jq 'length') - log_info "Found $candidate_count Wine build candidates" - - if [[ "$candidate_count" -eq 0 ]]; then - log_error "No suitable Wine builds found matching pattern: $WINE_PATTERN" - # Debug: Show what assets we did find - log_info "Available assets for debugging:" - echo "$releases_data" | jq -r '.[] | .assets[]? | .name' | head -10 | while IFS= read -r asset_name; do - log_info " - $asset_name" - done - exit 1 - fi - - # Filter based on configuration - local selected_build - - # Check if specific version requested - if [[ -n "${WINE_VERSION:-}" ]]; then - log_info "Searching for specific Wine version: $WINE_VERSION" - selected_build=$(echo "$wine_candidates" | jq -c --arg version "$WINE_VERSION" ' - [.[] | select(.version == $version) | select(.is_rc == false)] | first // empty - ') - - if [[ -z "$selected_build" || "$selected_build" == "null" ]]; then - log_error "Specific Wine version $WINE_VERSION not found" - exit 1 - fi - else - # Select latest based on release candidate preference - if [[ "$USE_RELEASE_CANDIDATE" == "true" ]]; then - log_info "Selecting latest Wine build (including release candidates)" - selected_build=$(echo "$wine_candidates" | jq -c 'first // empty') - else - log_info "Selecting latest stable Wine build (excluding release candidates)" - selected_build=$(echo "$wine_candidates" | jq -c '[.[] | select(.is_rc == false)] | first // empty') - fi - - if [[ -z "$selected_build" || "$selected_build" == "null" ]]; then - log_error "No suitable Wine build found with current criteria" - exit 1 - fi - fi - - # Debug: Show selected build - local selected_name - selected_name=$(echo "$selected_build" | jq -r '.name') - log_info "Selected Wine build: $selected_name" - - echo "$selected_build" -} - -# Download Wine build -download_wine_build() { - local asset_info="$1" - local filename - local download_url - - filename=$(echo "$asset_info" | jq -r '.name') - download_url=$(echo "$asset_info" | jq -r '.download_url') - - log_info "Found Wine build: $filename" - log_info "Download URL: $download_url" - - mkdir -p "$TEMP_DIR" - local download_path="${TEMP_DIR}/${filename}" - - log_info "Downloading to: $download_path" - if ! curl -L -o "$download_path" "$download_url"; then - log_error "Failed to download Wine build" - exit 1 - fi - - echo "$download_path" -} - -# Extract Wine build -extract_wine_build() { - local archive_path="$1" - local filename - filename=$(basename "$archive_path") - local archive_root="${filename%.tar.xz}" - - log_info "Extracting Wine build to ${EXTRACTION_TARGET}" - - # Create extraction directory in temp - local extract_dir="${TEMP_DIR}/extract" - mkdir -p "$extract_dir" - - # Extract archive - if ! tar -xf "$archive_path" -C "$extract_dir"; then - log_error "Failed to extract archive" - exit 1 - fi - - # Verify expected structure - local wine_root="${extract_dir}/${archive_root}" - if [[ ! -d "$wine_root" ]]; then - log_error "Expected root directory not found: $archive_root" - exit 1 - fi - - # Copy contents whilst excluding unwanted files - log_info "Copying Wine files to ${EXTRACTION_TARGET}" - for item in "$wine_root"/*; do - local basename_item - basename_item=$(basename "$item") - - if [[ "$basename_item" == "$EXCLUDED_FILE" ]]; then - log_warn "Excluding file: $basename_item" - continue - fi - - log_info "Copying: $basename_item" - if ! cp -rf "$item" "$EXTRACTION_TARGET/"; then - log_error "Failed to copy $basename_item to $EXTRACTION_TARGET" - exit 1 - fi - done - - log_info "Wine build installation completed successfully" -} - -# Main execution -main() { - log_info "Starting Wine build installation process" - - check_prerequisites - - local asset_info - asset_info=$(fetch_wine_build_info) - - local archive_path - archive_path=$(download_wine_build "$asset_info") - - extract_wine_build "$archive_path" - - log_info "Wine build installation process completed" -} - -# Execute main function -main "$@" diff --git a/docs/claude.md b/docs/claude.md deleted file mode 100644 index 1775cfc..0000000 --- a/docs/claude.md +++ /dev/null @@ -1,662 +0,0 @@ -# Claude Context: DistinctionOS - -## Project Overview - -**DistinctionOS** is a custom immutable Linux image built upon the Bazzite foundation, leveraging Universal Blue's infrastructure and tooling. This repository represents a personalised gaming and development environment optimised for a fully-featured experience. - -### Key Characteristics -- **Base System**: Bazzite (gaming-focused Fedora Atomic variant) -- **Build System**: Built from Universal Blue's GitHub image template using GitHub Actions to build & push to GHCR -- **Target Audience**: Primarily personal use, with gaming and development focus -- **Deployment**: OCI container images via GitHub Container Registry -- **Philosophy**: "Swiss Army Knife" approach - versatile powerhouse over minimalism -- **Code Quality**: Professional-grade with standardized logging, comprehensive error handling, and utility function library - -## Repository Structure - -``` -DistinctionOS/ -├── build_files/ # Build-time execution scripts (numerically ordered) -│ ├── 00-kernel.sh # CachyOS LTO kernel installation - FIRST -│ ├── 01-kernel-modules.sh # Initramfs regeneration - SECOND -│ ├── 02-build.sh # Package management (RPM, repos, keys) - THIRD -│ ├── 03-cache-install.sh # Cache RPM install from OCI artifact - FOURTH -│ ├── 04-force-install.sh # Force-install RPMs from OCI artifact - FIFTH -│ ├── 05-remote-grabber.sh # GNOME Shell extension management - SIXTH -│ ├── 06-fix-opt.sh # /opt persistence configuration - SEVENTH -│ ├── 07-config.sh # System services and misc config - EIGHTH -│ ├── 08-validate.sh # Post-install environment validation - NINTH (FINAL) -│ ├── 95-utility-functions.sh # Shared utility functions library - SOURCED BY ALL -│ └── wine-installer.sh # Custom Wine builds (INACTIVE - not in build sequence) -│ -├── system_files/ # Static files overlaid onto the image at build time -│ ├── usr/ -│ │ ├── bin/ # Custom executables (xwm-player, xiso, advmv, advcp, chsh, etc.) -│ │ ├── lib/systemd/user/ # User SystemD services (steam-linker) -│ │ ├── share/distinctionos/ -│ │ │ ├── just/ # ujust recipe files -│ │ │ ├── lib/ # Shared housekeeper library -│ │ │ ├── steam-linker/ # Steam Linker script and config -│ │ │ └── xwm-player/ # XWM Player config and handlers -│ │ ├── share/fonts/ # Bundled Nerd Fonts (0xProto, CommitMono, FiraCode, etc.) -│ │ ├── share/icons/ # Cursor themes (capitaine, DeepinDark, DeepinWhite) -│ │ ├── share/themes/ # GTK theme (adw-gtk3-dark) -│ │ ├── share/applications/ # .desktop files -│ │ ├── share/mime/ # MIME type registrations -│ │ └── share/glib-2.0/schemas/ # GNOME schema overrides -│ └── etc/ -│ ├── zsh/ # System-wide ZSH configuration -│ ├── sudoers.d/ # Passwordless sudo for wheel group -│ ├── yum.repos.d/ # Pre-installed repository files -│ ├── profile.d/ # Shell environment scripts -│ └── systemd/ # System-level systemd config overrides -│ -├── repo_files/ # Resources for just recipes (hosted on GitHub) -│ ├── brews # Homebrew package list (for post-install) -│ ├── flatpaks # Flatpak application list (for post-install) -│ └── rpm/ # RPM-based resources -│ -├── disk_config/ # Configuration for bootable disk creation -│ ├── disk.toml # QCOW2/RAW VM disk configuration -│ └── iso.toml # ISO installer configuration -│ -├── docs/ # Project documentation -│ ├── claude.md # This file - AI assistant context -│ ├── developer.md # Comprehensive developer documentation -│ ├── steam-linker.md # Steam Linker housekeeper documentation -│ ├── xwm-player.md # XWM Player documentation -│ └── ujust-recipes.md # ujust recipe reference -│ -├── Containerfile # Custom container build instructions -├── Justfile # Local development tooling (build, test, lint) -├── cosign.pub # Image signing public key -└── .github/workflows/ # GitHub Actions (build.yml, build-mesa.yml, build-disk.yml) -``` - -## Housekeeper Architecture - -The Housekeeper Architecture is an ecosystem of simple automation services for home directory management. All housekeepers share common infrastructure: - -### Directory Convention -| Purpose | Location | -|---------|----------| -| System defaults | `/usr/share/distinctionos//` | -| Local overrides | `/usr/local/share/distinctionos//` | -| User overrides | `~/.config/distinctionos/` | -| Runtime state | `~/.local/share/distinctionos//` | -| Logs | `/var/log/distinctionos/` (500MiB limit with rotation) | - -### Shared Library -`/usr/share/distinctionos/lib/housekeeper-common.sh` provides: -- Standardised logging with automatic rotation -- Configuration management with override hierarchy -- State persistence utilities -- Symlink management functions -- Locking to prevent concurrent execution - -### Current Housekeepers -| Service | Purpose | Status | -|---------|---------|--------| -| Steam Linker | Unified game library symlinks | ✅ Complete | -| XWM Player | Bethesda audio format playback | ✅ Complete | - -## Steam Linker - -Automatically creates symlinks to `~/Games/Steamlibrary/` from all Steam library locations. - -### Features -- Auto-discovers libraries via `libraryfolders.vdf` -- Tracks managed symlinks for restoration if accidentally deleted -- Removes broken symlinks automatically -- Duplicate detection with warnings - -### Quick Commands -```bash -ujust steam-link # Update symlinks -ujust steam-link-status # Show status -ujust steam-link-enable # Enable at login -ujust steam-link-logs # View logs -``` - -### Files -- Script: `/usr/share/distinctionos/steam-linker/steam-linker.sh` -- Service: `/usr/lib/systemd/user/distinctionos-steam-linker.service` -- Recipes: `/usr/share/distinctionos/just/steam-linker.just` -- Docs: `docs/steam-linker.md` - -## XWM Player - -Transparent playback of Bethesda game audio formats (`.xwm`, `.fuz`) via FFmpeg conversion and a configurable audio player. - -### Features -- Double-click playback via MIME type registration -- Flatpak-aware player launching -- Extensible format handler system -- Hierarchical configuration (system → local → user) - -### Quick Commands -```bash -xwm-player music.xwm # Play a file -xwm-player --convert voice.fuz voice.ogg # Convert without playing -xwm-player --cleanup # Remove temp files -``` - -### Files -- Script: `/usr/bin/xwm-player` -- Config: `/usr/share/distinctionos/xwm-player/config` -- Handlers: `/usr/share/distinctionos/xwm-player/handlers/` -- Desktop: `/usr/share/applications/xwm-player.desktop` -- Docs: `docs/xwm-player.md` - -## Technical Architecture - -### Build Process Overview -1. **Base Layer**: Starts with Bazzite's gaming-optimized foundation -2. **Customization Layer**: Applies personal configurations and packages via numbered build scripts -3. **Distribution**: Publishes to GHCR for atomic updates with Rechunker optimization - -### Build Script Execution Order - -**CRITICAL**: Scripts execute in numerical order (00 → 08), with `95-utility-functions.sh` sourced by all scripts. Package installs (`02`–`05`) run first; `/opt` persistence (`06`) and system config (`07`) run afterward so they see every installed package, then validation (`08`) runs last. - -``` -Containerfile Execution: - │ - ├─→ COPY system_files/ → / (overlay static files) - ├─→ COPY --from=mesa-rpms / … (pre-built Mesa OCI artifact) - │ - ├─→ 00-kernel.sh - │ ├─ Remove stock Fedora/Bazzite kernel packages - │ ├─ Install CachyOS LTO kernel via COPR - │ └─ Version-lock the kernel - │ - ├─→ 01-kernel-modules.sh - │ ├─ Detect installed CachyOS kernel version - │ └─ Regenerate initramfs (dracut, zstd, ostree-compatible) - │ - ├─→ 02-build.sh - │ ├─ Source utility-functions.sh - │ ├─ Remove unwanted Bazzite packages - │ ├─ Install RPM packages with resilient best-effort strategy - │ ├─ Configure repositories (Brave, COPR, etc.) - │ └─ Validate critical packages - │ - ├─→ 03-cache-install.sh - │ ├─ Source utility-functions.sh - │ └─ dnf install /var/tmp/cache-rpms/*.rpm (clean-install OCI artifact) - │ - ├─→ 04-force-install.sh - │ ├─ Source utility-functions.sh - │ └─ rpm --force --nodeps -i /var/tmp/force-install-rpms/*.rpm - │ - ├─→ 05-remote-grabber.sh - │ ├─ Source utility-functions.sh - │ ├─ Download GNOME Shell extensions - │ └─ Compile gschemas for extensions - │ - ├─→ 06-fix-opt.sh - │ ├─ Source utility-functions.sh - │ ├─ Scan /var/opt directory (now sees CrossOver et al. from cache install) - │ ├─ Move directories to /usr/lib/opt - │ └─ Generate tmpfiles.d config for runtime persistence - │ - ├─→ 07-config.sh - │ ├─ Source utility-functions.sh - │ ├─ Configure default shell (ZSH) - │ ├─ Integrate Just recipes - │ ├─ Hide incompatible Bazzite recipes - │ ├─ Customize applications (Cider icon fix sees the cache-installed Cider) - │ ├─ Update system caches (MIME, desktop, glib schemas) - │ └─ Remove unwanted files (Waydroid, Wine utilities, Bazzite remnants) - │ - └─→ 08-validate.sh - ├─ Source utility-functions.sh - ├─ Verify ldconfig, icon caches, pixbuf loaders, schemas, fonts - └─ Hard-fail on broken environment (VALIDATION_SOFT=1 to bypass) -``` - -### Key Technologies -- **Fedora Atomic**: Immutable base system with atomic updates -- **rpm-ostree**: Package layer management (runtime) -- **dnf5**: Package management (build-time) -- **Podman/Buildah**: Container runtime and build system -- **GitHub Actions**: Automated CI/CD pipeline -- **OCI Images**: Distribution format -- **Rechunker**: Layer optimization for efficient updates - -## Customization Philosophy - -### Design Principles -- **Fully Featured OS**: Focus on versatility and completeness -- **Development-Friendly**: Include essential development tools -- **Reproducible**: Declarative configuration for consistent builds -- **Personal**: Tailored to individual workflow preferences -- **Professional Quality**: Industry-standard code practices, comprehensive documentation -- **Maintainable**: DRY principle, utility function library, standardized patterns - -### Package Management Strategy -- **System Packages (Build-time)**: Added during build via dnf5 for base image inclusion -- **User Packages (Runtime)**: Installed via Flatpak, Homebrew, or distrobox containers -- **Development Tools**: Integrated into base image for immediate availability - -## Configuration Areas - -### System Customizations -- **Desktop Environment**: GNOME with personal extensions and themes -- **Shell Configuration**: ZSH with Oh My Zsh and Powerlevel10k (installed via just recipe post-rebase) -- **Development Environment**: Pre-configured toolchains and editors -- **Gaming Optimizations**: Inherited from Bazzite base + xpadneo for Xbox controllers - -### User Experience Enhancements -- **Dotfiles Integration**: Automated personal configuration deployment -- **Theme Consistency**: Kora icon theme, coordinated visual styling -- **Workflow Optimization**: Shortcuts and automation for common tasks -- **First-Run Automation**: SystemD service runs ujust distinction-install on first boot - - -## Key Files and Their Purposes - -### Build Scripts (Executed at Build-Time) - -#### `00-kernel.sh` - CachyOS LTO Kernel Installation -**Purpose**: Replace the stock Bazzite kernel with the CachyOS LTO (Link-Time Optimized) kernel -**Key Features**: -- Removes stock kernel packages before installing the replacement -- Installs via the `bieszczaders/kernel-cachyos-lto` COPR -- Version-locks the kernel to prevent unintended upgrades - -**When to Edit**: -- Switching kernel variants -- Updating version lock - -#### `01-kernel-modules.sh` - Initramfs Regeneration -**Purpose**: Detect the installed CachyOS kernel and regenerate the initramfs -**Key Features**: -- Detects the kernel version from `/usr/lib/modules/` -- Runs `dracut` with zstd compression and ostree compatibility flags -- Runs after the kernel install, before package installation - -**When to Edit**: Rarely — only if dracut flags or initramfs composition needs changing. - -#### `02-build.sh` - Package Management & Repository Configuration -**Purpose**: Core package installation with resilient best-effort strategy -**Key Features**: -- Attempts bulk installation per-repo, falls back to per-package on failure -- Tracks succeeded/failed/skipped packages separately -- Organized package installation by repository -- Repository outage tolerance (openzfs, CrossOver, Cider have had outages) - -**When to Edit**: -- Adding new RPM packages -- Adding/removing repositories -- Modifying package removal list -- Updating version locks - -#### Disallowed: `dnf --allowerasing` -**Policy**: `--allowerasing` must **never** be passed to `dnf`/`dnf5` in any -build script. It lets dnf silently *remove* installed packages to satisfy an -install or upgrade, which on this image can clobber required packages (kernel, -mesa, steam, etc.) without the build failing — producing a broken image that -still builds "successfully". - -**Enforcement**: `02-build.sh` defines `dnf()` and `dnf5()` wrapper functions -that scan their arguments and refuse to run (logging an error and returning -non-zero) if `--allowerasing` is present, dispatching to the real binary via -`command` otherwise. If a genuine conflict requires erasing a package, remove -the conflicting package *explicitly* (e.g. add it to `REMOVE_PACKAGES`) rather -than reaching for `--allowerasing`. - -#### `06-fix-opt.sh` - /opt Directory Persistence -**Purpose**: Ensure packages in /opt persist across reboots on an immutable system -**Key Features**: -- Dynamically scans /var/opt -- Generates tmpfiles.d configuration -- Executed at runtime by systemd-tmpfiles - -**Technical Background**: On immutable systems, /opt can be ephemeral. This creates symlinks from /var/opt to /usr/lib/opt, ensuring packages like CrossOver remain accessible. - -**Ordering**: Runs *after* the cache/force install steps (03–05) so that `/opt` packages installed from the OCI artifacts (e.g. CrossOver) are present in `/var/opt` when this script scans it. - -**When to Edit**: Rarely needed — automatically handles all /opt packages. - -#### `07-config.sh` - System Configuration & Cleanup -**Purpose**: System service config, application customization, file cleanup -**Key Features**: -- Shell configuration (ZSH default via useradd) -- Just recipe integration -- Application .desktop file modifications -- System cache updates (MIME, desktop, glib schemas) -- Cleanup of unwanted Bazzite/Waydroid files (documented reasons for each) - -**Ordering**: Runs near the end (after cache/force install and remote-grabber) so application customizations (e.g. the Cider `.desktop` icon fix) act on packages installed from the OCI artifacts, and the system-cache refreshes capture every package's icons/schemas/MIME entries. Also removes the Bazzite `/usr/bin/dnf` wrapper — safe here because no later script (only `08-validate.sh`) needs `dnf`. - -**When to Edit**: -- Enabling/disabling SystemD services -- Adding Just recipe customizations -- Customizing application behavior -- Adding files to cleanup lists - -#### `05-mesa-install.sh` - Custom Mesa Stack Installation -**Purpose**: Install a pre-built Fedora SRPM Mesa with freeworld codec patches -**Key Features**: -- Reads from the `mesa-rpms` OCI artifact stage (built weekly by `build-mesa.yml`) -- All packages come from one SRPM guaranteeing version coherency -- Uses `rpm --force --nodeps` to override conflicting Bazzite mesa packages - -**When to Edit**: -- Never directly — the Mesa OCI stage is built separately -- To change Mesa packages, update `build-mesa.yml` - -#### `05-remote-grabber.sh` - GNOME Extension Management -**Purpose**: Install and configure GNOME Shell extensions system-wide -**Key Features**: -- Extension download and installation -- Gschema compilation -- System-wide enablement - -**When to Edit**: -- Adding/removing GNOME Shell extensions -- Updating extension sources - -#### `95-utility-functions.sh` - Shared Utility Library -**Purpose**: Centralized functions and constants for all build scripts -**Key Features**: -- 8 ANSI color codes -- 6 logging functions (header, section, success, warning, error, info) -- Debug tracing functions -- Package validation functions -- File management helpers -- Command execution wrappers -- Counter utilities -- System information helpers -- Script lifecycle functions (`script_start`, `script_complete`) - -**Usage**: `source /ctx/95-utility-functions.sh` at the start of every build script - -**Benefits**: -- Eliminates ~300 lines of code duplication across scripts -- Single source of truth for logging behavior -- Consistent formatting and error handling - -**CRITICAL**: Changes affect ALL build scripts — test thoroughly. - -#### `wine-installer.sh` - Custom Wine Build Installation (INACTIVE) -**Purpose**: Install Kron4ek Wine builds with specific features -**Status**: Currently inactive, not in build sequence -**When Active**: Installs wine-staging-tkg-ntsync-amd64-wow64 from GitHub releases - -**Note**: Remains separate from numbered sequence as it's intermittently used. - -### `Containerfile` - Container Build Instructions -**Purpose**: Define the multi-stage build process -**Key Features**: -- Base image selection (Bazzite GNOME) -- Build context layer for script access -- system_files/ overlay copy -- Sequential script execution (00-06) -- Pre-built Mesa OCI artifact stage -- Color-coded build progress output -- OSTree container commit - -**When to Edit**: -- Changing base image -- Modifying script execution order (rare) -- Enabling/disabling scripts (comment out execution line) -- Adjusting build optimizations - -### `system_files/` Directory -Contains custom files overlaid at build time (akin to BlueBuild's 'overlay'): -- **usr/bin/**: Custom executables — `xwm-player`, `xiso`, `advmv`, `advcp`, `chsh`, `rpm-ostree-search-hl`, `xdg-terminal-exec` -- **usr/lib/systemd/user/**: User SystemD services (`distinctionos-steam-linker.service`) -- **usr/share/distinctionos/**: DistinctionOS project files (just recipes, housekeeper scripts, configs) -- **usr/share/fonts/**: Bundled Nerd Fonts — 0xProto, CommitMono, FiraCode, DejaVuSansMono, and others -- **usr/share/icons/**: Cursor themes — capitaine-cursors, DeepinDark-cursors, DeepinWhite-cursors -- **usr/share/themes/**: GTK theme — adw-gtk3-dark (GTK3 + GTK4 + libadwaita) -- **usr/share/glib-2.0/schemas/**: GNOME schema overrides for desktop settings -- **usr/share/applications/**: Custom .desktop files -- **usr/share/mime/**: MIME type registrations (xwm-player, rom-properties) -- **usr/lib/bootc/install/**: bootc install config (`20-distinction.toml`) -- **usr/lib64/**: Blur effect shared library for GNOME Shell effects -- **etc/zsh/**: System-wide ZSH config (zshrc, zprofile, zlogin, zshenv, zlogout) -- **etc/sudoers.d/**: Passwordless sudo for wheel group -- **etc/yum.repos.d/**: Pre-installed repository configs (Brave, COPR repos) -- **etc/rpm-ostreed.conf.d/**: TPM configuration for rpm-ostree daemon -- **etc/systemd/logind.conf.d/**: Power button / lid behaviour overrides -- **etc/dracut.conf.d/**: Dracut configuration for initramfs -- **etc/sysctl.conf**: Kernel parameter overrides - -## Maintenance Considerations - -### Update Strategy -- **Base Image Updates**: Automatic rebuilds when Bazzite releases updates (every 5 days scheduled) -- **Security Updates**: Regular rebuilds for security patches -- **Feature Updates**: Manual integration of new customizations via feature branches - -### Testing Approach -- **Local Testing**: `just build` → `just run-vm-qcow2` for VM validation -- **Build Verification**: GitHub Actions logs for build-time issues -- **Functionality Testing**: Validate key features in VM before merge -- **Integration Testing**: Verify compatibility with upstream Bazzite changes - -### Debug Approaches -- **Build Logs**: Examine GitHub Actions output for build-time issues (color-coded logging) -- **System Logs**: Use `journalctl` for runtime problem diagnosis -- **Layer Inspection**: Analyze image layers with `podman history` -- **Local Debugging**: `podman run -it localhost/distinctionos:test /bin/bash` - -## Current Implementation Status - -### ✅ Completed Features - -#### Build System (2025-10-27 Major Refactoring) -- **Utility Functions Library**: Centralized logging, validation, and helper functions -- **Color-Coded Logging**: Consistent visual feedback across all build scripts -- **Comprehensive Error Handling**: Validation, error reporting, graceful degradation -- **Professional Code Quality**: DRY principle, documentation standards, consistent patterns -- **Script Organization**: Numerically ordered execution (01-06) - -#### Default Shell Configuration -- **ZSH System Config**: Full system-wide ZSH configuration via `/etc/zsh/` (zshrc, zprofile, zlogin, zshenv, zlogout) -- **User Shell**: Post-rebase `ujust distinction-install-custom-shell` changes login shell and installs dotfiles -- **Root Shell**: Also configured during post-rebase setup - -#### TPM Configuration -- **rpm-ostree TPM config**: `/etc/rpm-ostreed.conf.d/distinction.tpm.conf` provides TPM unlock hints to the rpm-ostree daemon -- **Status**: TPM auto-unlock system being redesigned — current implementation is partial; full ujust recipes and monitoring service are planned - -#### Just Recipe System -- **Main Recipe**: `distinction.just` — post-install setup (Flatpaks, Homebrews, shell, NvChad, Nautilus scripts) -- **Steam Linker Recipe**: `steam-linker.just` — full Steam library symlink management -- **See**: `docs/ujust-recipes.md` for complete recipe reference - -#### Security Configuration -- **Passwordless Sudo**: Configured for wheel group (user aware of security implications) -- Located at `/etc/sudoers.d/99-distinction-wheel-nopasswd` - -### 🚧 Known Issues -- **NvChad root installation**: May need verification after first run (`sudo nvim` to complete) -- **TPM auto-unlock**: System being redesigned — current state is partial (config file only, no ujust recipes or monitor service) - -## Maintenance Procedures - -### After System Updates -```bash -rpm-ostree upgrade -systemctl reboot -# If TPM auto-unlock breaks after reboot, re-enroll manually -# (full TPM ujust recipes are planned but not yet implemented) -``` - -### Adding Packages - -#### Build-Time Packages (in the image) -Edit `02-build.sh`. Packages are added to the repo-keyed arrays used by `install_packages_resilient`: -```bash -# Example: add to fedora repo array -install_packages_resilient "fedora" existing-packages new-package -``` - -#### Runtime Packages (post-install) -- **Flatpaks**: Add to `repo_files/flatpaks` on GitHub -- **Homebrews**: Add to `repo_files/brews` on GitHub -- Run: `ujust distinction-install` or specific recipe - - -## Future Roadmap - -### Short-Term Goals -- [ ] Revisit and redesign TPM auto-unlock (including ujust recipes and monitor service) -- [ ] Expand 'housekeeper' functionality (future: `.housekeeper` config files that maintain expansive directory structures) -- [ ] Update GitHub Actions to more closely match Bazzite (release pages with package changelogs per build) - -### Long-Term Goals -- [ ] **Standalone ISO**: Fully functional installer ISO (in progress via build-disk.yml) -- [ ] **Build Caching**: Implement layer caching for faster iteration - -### Completed Goals ✅ -- [x] Rechunker support for efficient updates -- [x] ZSH as default shell with system-wide config -- [x] Oh-my-zsh with Powerlevel10k (via post-install ujust) -- [x] Steam library symlink automation (Steam Linker housekeeper) -- [x] XWM Player — Bethesda audio format playback -- [x] CachyOS LTO kernel as default (`00-kernel.sh`) -- [x] Custom Mesa stack with freeworld codecs (`05-mesa-install.sh` + `build-mesa.yml`) -- [x] Build Script Refactoring: - - [x] Utility functions library (`95-utility-functions.sh`) - - [x] Color-coded logging and error handling across all scripts - - [x] Resilient best-effort package installation strategy - -## Notes for AI Assistants - -### Code Style Preferences -- **Shell Scripts**: Follow Google Shell Style Guide conventions -- **Utility Functions**: Always source `/ctx/utility-functions.sh` in build scripts -- **Logging**: Use utility function logging (log_header, log_section, log_success, etc.) -- **Error Handling**: Use utility wrappers (run_with_log, check_file_exists, etc.) -- **Validation**: Use validation functions (validate_critical_packages, etc.) -- **YAML Files**: 2-space indentation, explicit string quoting where beneficial -- **Documentation**: Clear, concise explanations with practical examples -- **Context Generation**: At end of session, update claude.md and developer.md - -### Project Philosophy -- Fully-featured experience prioritized over minimalism -- Native RPM packages preferred over Flatpaks where sensible -- Elegant solutions balancing functionality with maintainability -- Proactive problem prevention over reactive fixes -- Professional code quality with comprehensive documentation -- DRY principle - don't repeat yourself -- This is a personal project focused on creating an optimal Linux environment for both gaming and development work - -### Build Script Development Guidelines - -#### When Creating/Modifying Build Scripts: -1. **Always source utility functions first**: `source /ctx/utility-functions.sh` -2. **Use consistent logging**: log_header → log_section → log_success/error/warning -3. **Validate critical operations**: Use validation functions for important packages/files -4. **Document WHY, not just WHAT**: Inline comments should explain rationale -5. **Follow the established pattern**: Look at existing scripts for reference -6. **Test locally first**: `just build` before committing -7. **Use helper functions**: Take advantage of utility functions library (30+ functions) - -#### Script Structure Template: -```bash -#!/usr/bin/bash -set -euo pipefail - -# ============================================================================ -# Script Name -# ============================================================================ -# Purpose: Brief description -# Execution: Position in sequence -# ============================================================================ - -source /ctx/95-utility-functions.sh - -script_start "Script Name" "Brief description" - -log_section "Major operation" -# ... operations with logging ... -log_success "Operation complete" - -script_complete "Script Name" "Next step: ..." -exit 0 -``` - -### Important Distinctions - -#### Build-Time vs Runtime: -- **Build-Time**: Scripts in build_files/ execute during image creation - - Use `dnf5` for package management - - Use utility functions for logging/validation - - Changes require rebuild - - Files go into /usr (immutable) - -- **Runtime**: User operations after rebase - - Use `rpm-ostree` for system packages - - Use `flatpak` or `brew` for user packages - - Use `ujust` recipes for automation - - Changes persist in /var or /home - -#### Directory Naming: -- All directories use **underscores**: `build_files/`, `system_files/`, `repo_files/`, `disk_config/` - -### Housekeeper Standards -When creating new housekeepers: -1. Source `/usr/share/distinctionos/lib/housekeeper-common.sh` -2. Use `hk_init_logging` for standardised logging -3. Use `hk_load_config` for configuration hierarchy -4. Use `hk_lock` to prevent concurrent execution -5. Store state in `~/.local/share/distinctionos//` -6. Create corresponding `.just` recipe file - -### DistinctionOS file structure -Some DistinctionOS projects should have their own directories inside `/usr/share/distinctionos/` and should respect overrides in `/usr/local/share/distinctionos/` and `~/.local/share/distinctionos/` and use `/var/log/distinctionos/` for logs(respecting a 500MiB size limit); Use of these directories depends on if a project has functionality that needs organization and should include a documentation file per project (documentation file should also include a tree-view for all files relavent to a project). - - -### User Technical Level -- Intermediate Linux system administration skills -- Comfortable with containers, package management, system configuration -- Appreciates detailed technical explanations with practical application -- Values clean, maintainable code with comprehensive documentation -- Prefers professional-grade solutions over quick hacks - -### When Generating Context Updates -At the end of a session, user will request updated context files: -- **claude.md**: Comprehensive overview for AI assistants (this file) -- **developer.md**: Detailed technical documentation for human developers -- Include all architectural changes, new features, and rationale -- Update roadmap and completed goals -- Reflect current state accurately - ---- - -## Document Metadata - -**Version**: 3.5 -**Last Updated**: 2026-06-02 -**Major Changes**: -- Reorder build sequence so package installs run before configuration: - 00-kernel, 01-kernel-modules, 02-build, 03-cache-install, 04-force-install, - 05-remote-grabber, 06-fix-opt, 07-config, 08-validate. `fix-opt` and `config` - moved to the end so they see packages installed from the cache/force OCI - artifacts (fixes CrossOver /opt persistence and the Cider icon fix). -- Move CrossOver and Cider out of 02-build into the cache OCI artifact - (`.github/cache-builder/packages.txt`); add `url` source type to the cache builder. -- Add the "Disallowed: `dnf --allowerasing`" policy + `dnf`/`dnf5` guard wrappers. -- Resolve a committed merge conflict in the remote-grabber script (the - undefined `install_gnome_rounded_blur` call; gnome-rounded-blur ships via - the force-install artifact). - -**Earlier Changes (v3.4)**: -- Fix all directory names to use underscores (build_files, system_files, etc.) -- Update build script sequence: 00-kernel, 01-kernel-modules, 02-build, 03-fix-opt, 04-config, 05-mesa-install, 06-remote-grabber -- Remove ZFS references (no longer in use) -- Add 00-kernel.sh (CachyOS LTO kernel) and 05-mesa-install.sh (custom Mesa OCI) -- Add XWM Player to Housekeeper Architecture and Completed Goals -- Update system_files/ contents to reflect actual files (fonts, cursors, themes, MIME, etc.) -- Correct TPM status (partial/planned, not complete) -- Add docs/xwm-player.md and docs/ujust-recipes.md to structure -- Remove references to non-existent firstrun and tpm-monitor binaries - -**Maintainer**: phantomcortex -**Purpose**: Provide comprehensive context to AI assistants working with DistinctionOS From 7d01ff19a2d032859b3913dd0d779e1c7c51db4c Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:42:36 -0600 Subject: [PATCH 07/13] Align build scripts and Justfile with house style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 00-kernel.sh: shebang `/bin/bash` → `/usr/bin/bash`, quote $pkg in the rpm-erase loop (shellcheck SC2086). - 06-fix-opt.sh: replace raw `echo` status lines with `log_info` per the shell-conventions skill. - 07-config.sh: collapse the five-line `echo` summary block to a single `log_success` (the log_section banners above already trace what ran). - Justfile: `Virtal` → `Virtual` in the ten `[group(...)]` labels. - docs/developer.md: drop the wine-installer.sh entry from the directory layout to match the deletion in the prior commit. Co-Authored-By: Claude Opus 4.7 --- Justfile | 20 ++++++++++---------- build_files/00-kernel.sh | 8 ++++---- build_files/06-fix-opt.sh | 4 ++-- build_files/07-config.sh | 7 +------ docs/developer.md | 3 +-- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/Justfile b/Justfile index 5baed26..75a9c5b 100644 --- a/Justfile +++ b/Justfile @@ -231,27 +231,27 @@ _build-bib $target_image $tag $type $config: (_rootful_load_image target_image t _rebuild-bib $target_image $tag $type $config: (build target_image tag) && (_build-bib target_image tag type config) # Build a QCOW2 virtual machine image -[group('Build Virtal Machine Image')] +[group('Build Virtual Machine Image')] build-qcow2 $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "qcow2" "disk_config/disk.toml") # Build a RAW virtual machine image -[group('Build Virtal Machine Image')] +[group('Build Virtual Machine Image')] build-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "raw" "disk_config/disk.toml") # Build an ISO virtual machine image -[group('Build Virtal Machine Image')] +[group('Build Virtual Machine Image')] build-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_build-bib target_image tag "iso" "disk_config/iso.toml") # Rebuild a QCOW2 virtual machine image -[group('Build Virtal Machine Image')] +[group('Build Virtual Machine Image')] rebuild-qcow2 $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "qcow2" "disk_config/disk.toml") # Rebuild a RAW virtual machine image -[group('Build Virtal Machine Image')] +[group('Build Virtual Machine Image')] rebuild-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "raw" "disk_config/disk.toml") # Rebuild an ISO virtual machine image -[group('Build Virtal Machine Image')] +[group('Build Virtual Machine Image')] rebuild-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_rebuild-bib target_image tag "iso" "disk_config/iso.toml") # Run a virtual machine with the specified image type and configuration @@ -297,19 +297,19 @@ _run-vm $target_image $tag $type $config: podman run "${run_args[@]}" # Run a virtual machine from a QCOW2 image -[group('Run Virtal Machine')] +[group('Run Virtual Machine')] run-vm-qcow2 $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "qcow2" "disk_config/disk.toml") # Run a virtual machine from a RAW image -[group('Run Virtal Machine')] +[group('Run Virtual Machine')] run-vm-raw $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "raw" "disk_config/disk.toml") # Run a virtual machine from an ISO -[group('Run Virtal Machine')] +[group('Run Virtual Machine')] run-vm-iso $target_image=("localhost/" + image_name) $tag=default_tag: && (_run-vm target_image tag "iso" "disk_config/iso.toml") # Run a virtual machine using systemd-vmspawn -[group('Run Virtal Machine')] +[group('Run Virtual Machine')] spawn-vm rebuild="0" type="qcow2" ram="6G": #!/usr/bin/env bash diff --git a/build_files/00-kernel.sh b/build_files/00-kernel.sh index 5a010b6..62f65db 100755 --- a/build_files/00-kernel.sh +++ b/build_files/00-kernel.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/bash set -euo pipefail # ============================================================================ @@ -8,9 +8,9 @@ set -euo pipefail # Source utility functions source /ctx/95-utility-functions.sh -for pkg in kernel kernel-core kernel-modules kernel-devel-matched; do - rpm --erase $pkg --nodeps -done +for pkg in kernel kernel-core kernel-modules kernel-devel-matched; do + rpm --erase "$pkg" --nodeps +done pushd /usr/lib/kernel/install.d printf '%s\n' '#!/bin/sh' 'exit 0' > 05-rpmostree.install diff --git a/build_files/06-fix-opt.sh b/build_files/06-fix-opt.sh index 6055af5..3269c6a 100755 --- a/build_files/06-fix-opt.sh +++ b/build_files/06-fix-opt.sh @@ -28,12 +28,12 @@ for dir in /var/opt/*/; do [ -d "$dir" ] || continue dirname=$(basename "$dir") - echo " Processing: $dirname" + log_info "Processing: $dirname" mv "$dir" "/usr/lib/opt/$dirname" echo "L+ /var/opt/$dirname - - - - /usr/lib/opt/$dirname" >> /usr/lib/tmpfiles.d/distinction-opt-fix.conf done log_success "/opt persistence configured" -echo " ℹ Packages in /opt will persist across reboots via tmpfiles.d" +log_info "Packages in /opt will persist across reboots via tmpfiles.d" exit 0 diff --git a/build_files/07-config.sh b/build_files/07-config.sh index d72b5ba..0b6b906 100755 --- a/build_files/07-config.sh +++ b/build_files/07-config.sh @@ -276,11 +276,6 @@ log_info "Removing Bazzite dnf wrapper" log_header "System configuration phase complete" -log_info "Configuration summary:" -echo " • Default shell: ZSH" -echo " • Just recipes: Integrated" -echo " • Applications: Customized" -echo " • System caches: Updated" -echo " • Unwanted files: Removed" +log_success "System configuration complete" exit 0 diff --git a/docs/developer.md b/docs/developer.md index a44e5d7..7d24297 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -42,8 +42,7 @@ DistinctionOS/ │ ├── 06-fix-opt.sh # /opt persistence configuration │ ├── 07-config.sh # System services and misc config │ ├── 08-validate.sh # Post-install environment sanity checks -│ ├── 95-utility-functions.sh # Shared logging/utility library (sourced by all) -│ └── wine-installer.sh # Custom Wine builds (inactive) +│ └── 95-utility-functions.sh # Shared logging/utility library (sourced by all) │ ├── system_files/ # Static files overlaid onto the image at build time │ ├── usr/ From cb7234354dca5698bdaa32b5b493e197fdabfc1c Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:54:55 -0600 Subject: [PATCH 08/13] Manifest-driven 02-build; scoped sudo; build-secret auth; Claude context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_files/02-build.sh + build_files/manifests/*.list Package lists move out of the script into eight declarative `.list` manifests (fedora, fedora-multimedia, rpmfusion, terra, brave, critical, remove, plus a `coprs.list`). 02-build.sh now reads them via `read_manifest`, captures dnf output to a log file (`dnf_log_tail` on failure), and authenticates GitHub API calls through a BuildKit secret mount via the new `github_api_get` helper. The standalone `--allowerasing` guard wrappers were consolidated into 95-utility-functions.sh (prior commit). .github/workflows/build.yml Drop `system_files/**` from `paths-ignore` — those files are COPY'd into the image, so ignoring them silently shipped stale configs until the next scheduled rebuild. Also mounts `GITHUB_TOKEN` as a buildah `--secret` so build scripts can hit the GitHub API without sharing the anonymous runner-pool rate limit. Secret file is created with mode 0600 and removed post-build. system_files/etc/sudoers.d/99-distinction-wheel-nopasswd Replace blanket `%wheel NOPASSWD: ALL` with a scoped policy: passwordless for the atomic-system commands used daily (`rpm-ostree`, `bootc`, `ostree`, `systemctl`, `flatpak`), plus a 60-minute credential cache for everything else. Old Option-A behaviour preserved in comments for easy revert. CLAUDE.md Project-level Claude Code context: working agreement, commands, hard rules (no `--allowerasing`, manifests are the source of truth for package data, `dnf versionlock add` is a no-op on ostree and must not return). .claude/skills/ Three skill files loaded automatically by Claude Code: `build-pipeline` (Containerfile order, mount gotchas, manifest layout), `shell-conventions` (logging library, error-handling policy per script), `keep-it-simple` (minimal-diff guardrails). Co-Authored-By: Claude Fable 5 --- .claude/skills/build-pipeline/SKILL.md | 53 ++ .claude/skills/keep-it-simple/SKILL.md | 40 + .claude/skills/shell-conventions/SKILL.md | 48 ++ .github/workflows/build.yml | 12 +- CLAUDE.md | 36 + build_files/02-build.sh | 685 +++--------------- build_files/manifests/coprs.list | 8 + build_files/manifests/packages-brave.list | 2 + build_files/manifests/packages-critical.list | 26 + .../manifests/packages-fedora-multimedia.list | 2 + build_files/manifests/packages-fedora.list | 49 ++ build_files/manifests/packages-remove.list | 6 + build_files/manifests/packages-rpmfusion.list | 2 + build_files/manifests/packages-terra.list | 9 + .../sudoers.d/99-distinction-wheel-nopasswd | 26 +- 15 files changed, 432 insertions(+), 572 deletions(-) create mode 100644 .claude/skills/build-pipeline/SKILL.md create mode 100644 .claude/skills/keep-it-simple/SKILL.md create mode 100644 .claude/skills/shell-conventions/SKILL.md create mode 100644 CLAUDE.md create mode 100644 build_files/manifests/coprs.list create mode 100644 build_files/manifests/packages-brave.list create mode 100644 build_files/manifests/packages-critical.list create mode 100644 build_files/manifests/packages-fedora-multimedia.list create mode 100644 build_files/manifests/packages-fedora.list create mode 100644 build_files/manifests/packages-remove.list create mode 100644 build_files/manifests/packages-rpmfusion.list create mode 100644 build_files/manifests/packages-terra.list diff --git a/.claude/skills/build-pipeline/SKILL.md b/.claude/skills/build-pipeline/SKILL.md new file mode 100644 index 0000000..b248a9d --- /dev/null +++ b/.claude/skills/build-pipeline/SKILL.md @@ -0,0 +1,53 @@ +--- +name: build-pipeline +description: DistinctionOS image build architecture — execution order, Containerfile mounts, OCI cache artifacts, and build-container gotchas. Use when editing the Containerfile, anything in build_files/, the GitHub Actions workflows, or diagnosing a failed image build. +--- + +# DistinctionOS Build Pipeline + +## Execution order (Containerfile RUN chain — the single source of truth) + +| Step | Script | Purpose | Failure mode | +|------|--------|---------|--------------| +| 1 | `00-kernel.sh` | Swap stock kernel → kernel-cachyos-lto | Hard fail (`set -e`) | +| 2 | `01-kernel-modules.sh` | Regenerate initramfs via dracut | Hard fail | +| 3 | `02-build.sh` | Package install from manifests | **Best-effort** — deliberately no `set -e`; tracks FAILED_PACKAGES, only a missing `steam` fails the build | +| 4 | `03-cache-install.sh` | RPMs from `distinctionos-cache` OCI artifact | — | +| 5 | `04-force-install.sh` | `rpm --force --nodeps` RPMs from OCI artifact | — | +| 6 | `05-remote-grabber.sh` | GNOME Shell extensions (commit-pinned) | Hard fail on any extension | +| 7 | `06-fix-opt.sh` | /opt persistence via tmpfiles.d | Hard fail | +| 8 | `07-config.sh` | Services, shell defaults, misc | Hard fail | +| 9 | `08-validate.sh` | Environment sanity checks | Soft by default (`VALIDATION_SOFT=1`) | + +If you add/remove/reorder a step: update the Containerfile, this table, +and README.md in the same commit. + +## Containerfile mount gotchas + +- `/tmp` is a **tmpfs mount** — contents vanish; never stage files there + that must reach the image. Pre-cached RPMs live in `/var/tmp/` for this + exact reason. +- `/var/cache` and `/var/log` are **BuildKit cache mounts** — they persist + across builds. This is why 08-validate.sh wipes the ldconfig aux-cache; + treat anything read from these paths as potentially stale. +- Build scripts are bind-mounted at `/ctx/`, not copied into the image. + Manifests are therefore at `/ctx/manifests/`. +- An optional GitHub token is mounted at `/run/secrets/github_token` + (use the `github_api_get` helper in 02-build.sh, never raw curl to + api.github.com). + +## Package data lives in manifests + +`build_files/manifests/*.list` — one package per line, `#` comments. +`coprs.list` format: `owner/project pkg [pkg...]`. To add a package, edit +the manifest; touch 02-build.sh only for new *behaviour*. + +## CI facts + +- `paths-ignore` must NOT include `system_files/**` — those files are baked + into the image. +- Image builds with `sudo buildah bud`; the rechunker and cosign signing + run afterwards. Scheduled rebuild every 5 days. +- Cache and force-install RPMs come from separate workflows + (`build-cache.yml`, `build-force-install.yml`) pushed to GHCR; `:latest` + is rolling, `:YYYYMMDD` is pinned. diff --git a/.claude/skills/keep-it-simple/SKILL.md b/.claude/skills/keep-it-simple/SKILL.md new file mode 100644 index 0000000..aba28c6 --- /dev/null +++ b/.claude/skills/keep-it-simple/SKILL.md @@ -0,0 +1,40 @@ +--- +name: keep-it-simple +description: Guardrail against over-engineering. Use at the START of any task that involves writing or refactoring code, designing a fix, or proposing an implementation — before generating the solution. +--- + +# Keep It Simple + +This is a single-maintainer hobby OS, not a platform team's monorepo. Every +line added is a line the maintainer must understand at 2am when a build +breaks. Optimise for *deletability*, not extensibility. + +## Before writing any code, answer these + +1. What is the smallest diff that solves the stated problem? +2. Can an existing function/pattern/manifest absorb this instead of a new one? +3. Is any part of my plan solving a problem that wasn't asked about? + If yes — cut it, or mention it in one sentence at the end instead. + +## Hard limits + +- **No new abstractions** (wrapper functions, config layers, plugin systems, + generic frameworks) unless the user explicitly asked for one. +- **No speculative flexibility** — no "in case you later want to..." + parameters, no handling of inputs that cannot occur. +- **No drive-by refactors.** Fix the thing asked. Note other issues in one + line; do not touch them. +- **>30 lines or >2 files → stop.** Present a ≤5-line plan and wait. +- **Error handling proportionate to consequence**: a cosmetic step gets + `|| log_warning`, not a retry framework. + +## Smells that mean you are overcomplicating + +- A helper function with exactly one call site. +- An if/else ladder for cases the build can never produce. +- Introducing a new file when 10 lines in an existing one would do. +- Rewriting working code to be "cleaner" while fixing an unrelated bug. +- Explaining the solution takes longer than reading the diff. + +When in doubt, ship the boring version. The clever version can always be +requested; the boring version rarely needs to be reverted. diff --git a/.claude/skills/shell-conventions/SKILL.md b/.claude/skills/shell-conventions/SKILL.md new file mode 100644 index 0000000..a4d7546 --- /dev/null +++ b/.claude/skills/shell-conventions/SKILL.md @@ -0,0 +1,48 @@ +--- +name: shell-conventions +description: DistinctionOS shell scripting house style — logging functions, error-handling strategy, shellcheck requirements, and patterns to reuse. Use when writing or modifying any .sh file in build_files/ or system_files/. +--- + +# DistinctionOS Shell Conventions + +## Non-negotiables + +1. **Shebang**: `#!/usr/bin/bash` +2. **Must pass**: `shellcheck -S error -e SC1091 ` — CI gates on this. +3. **Build scripts** source the shared library first: + `source /ctx/95-utility-functions.sh` +4. **Error strategy is deliberate and per-script**: + - `set -euo pipefail` for scripts where any failure should abort. + - `set -uo pipefail` (no `-e`) ONLY for best-effort installers + (02-build.sh). Do not "fix" this by adding `-e`. + +## Use the library — do not reinvent + +Logging: `log_header`, `log_section`, `log_info`, `log_success`, +`log_warning`, `log_error`. Lifecycle: `script_start`, `script_complete`. +Helpers: `create_dir_with_log`, `counter_display`, `run_with_log`. + +Never `echo` raw status lines in build scripts; never invent a new logging +scheme. If a helper is missing, add it to 95-utility-functions.sh rather +than defining it locally in two places. + +## Established patterns to reuse (don't redesign) + +- **Resilient install**: bulk attempt → individual fallback → record in + SUCCEEDED_PACKAGES / FAILED_PACKAGES. See `install_packages_resilient`. +- **dnf output**: append to `$DNF_LOG`, surface with `dnf_log_tail` on + failure. Never `&>/dev/null` a dnf call. +- **GitHub API**: always via `github_api_get` (handles token + retries). +- **Downloads**: `curl -fL --retry 3 --retry-delay 2`, then verify the + artifact before use; failures log a warning, not silence. +- **Package data**: belongs in `build_files/manifests/`, read via + `read_manifest`. + +## Style + +- 2-space indent in build_files, 4-space in 05-remote-grabber.sh (match + the file you're in). +- Section banners: `# ===...===` for major, `# ───...───` for minor. +- Quote all expansions; arrays for command construction + (`local -a cmd=(...)` then `"${cmd[@]}"`). +- `readonly` for constants; `local` for everything inside functions. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01df3f7..e66cffa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,9 @@ on: - '**/claude.md' - 'repo_files/**' - 'docs/**' - - 'system_files/**' # modifcations to system files are usually small and don't warrant a full rebuild + # NOTE: system_files/** deliberately NOT ignored — those files are + # COPY'd into the image, so changes to them MUST trigger a rebuild + # or they sit undeployed until the next scheduled build. workflow_dispatch: inputs: handwritten: @@ -195,9 +197,16 @@ jobs: | jq -r '.Labels["org.opencontainers.image.version"] // empty' \ | grep -oP '^\d+' || echo "44") VERSION_DATE=$(date -u +%Y%m%d) + # GITHUB_TOKEN as a build secret: lets build scripts make + # authenticated GitHub API calls (e.g. Kora release lookup) without + # the anonymous per-IP rate limit shared across GHA runners. + # Secret mounts never persist in image layers, unlike --build-arg. + install -m 0600 /dev/null /tmp/.ghtoken + printf '%s' "${{ secrets.GITHUB_TOKEN }}" > /tmp/.ghtoken # Builds image in root store as root, to be picked up by Rechunker sudo buildah bud \ --format docker \ + --secret id=github_token,src=/tmp/.ghtoken \ --tag "localhost/${IMAGE_NAME}:${{ env.DEFAULT_TAG }}" \ --build-arg IMAGE_NAME="${IMAGE_NAME}" \ --build-arg IMAGE_VENDOR="${{ github.repository_owner }}" \ @@ -206,6 +215,7 @@ jobs: --build-arg VERSION_DATE="${VERSION_DATE}" \ --file Containerfile \ . + rm -f /tmp/.ghtoken - name: Remove source images run: | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..29429dc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,36 @@ +# DistinctionOS — Claude Code Context + +Custom Bazzite-based (Fedora Atomic) bootc image. Built via GitHub Actions → +GHCR. Single maintainer (phantomcortex). Deep context lives in skills under +`.claude/skills/` — they load automatically when relevant. Do NOT re-read +`docs/claude.md`; it is legacy and may be stale. + +## Working agreement (applies to EVERY task) + +- **Minimal diffs.** Change the fewest lines that solve the problem. No new + abstractions, wrappers, config layers, or "while we're here" refactors + unless explicitly requested. +- **Plan first on non-trivial work.** If a change would exceed ~30 lines or + touch more than two files, propose the approach in ≤5 lines and wait for + approval before writing code. +- **Match house style, don't invent one.** Build scripts source + `95-utility-functions.sh` and use its `log_*` functions. See the + `shell-conventions` skill. +- **Keep docs and build telling the same story.** If you change the + Containerfile or build sequence, update README.md in the same commit. + +## Commands + +- Lint shell exactly as CI does: `shellcheck -S error -e SC1091 build_files/*.sh` +- Local image build: `just build` (runs `check-syntax` first) +- VM test: `just build-qcow2 && just run-vm` + +## Hard rules + +- Never use `dnf --allowerasing` (guarded against in 02-build.sh). +- Package lists belong in `build_files/manifests/*.list`, not in scripts. +- Build scripts run in a container with `/tmp` as tmpfs — anything that must + survive into the image goes under `/var/tmp` or a real path. +- Third-party git clones in 05-remote-grabber.sh must be commit-pinned + (`url#sha`). +- `dnf versionlock add` is a no-op on ostree images — do not reintroduce it. diff --git a/build_files/02-build.sh b/build_files/02-build.sh index c217c01..227f29c 100755 --- a/build_files/02-build.sh +++ b/build_files/02-build.sh @@ -14,40 +14,56 @@ set -uo pipefail source /ctx/95-utility-functions.sh # ============================================================================ -# Guard: disallow `--allowerasing` -# ============================================================================ -# --allowerasing lets dnf silently remove packages to satisfy an install, which -# can clobber required packages. Wrap dnf/dnf5 so any future use is rejected. -# -# Narrow exception: the freeworld codec stack (libavcodec-freeworld, -# mesa-*-freeworld, gstreamer1-plugins-bad-freeworld) overlaps file paths -# with the Fedora "free" variants, so a clean install requires removing the -# free variant first. The dnf_codec_swap() function in the codec section -# bypasses these wrappers via `command dnf5` after validating that the -# swap is between a specific known-safe pair — see CODEC_SWAP_ALLOWLIST. - -dnf() { - local arg - for arg in "$@"; do - if [[ "$arg" == "--allowerasing" ]]; then - log_error "Refusing to run dnf with --allowerasing (disallowed in this build)" - return 1 - fi - done - command dnf "$@" +# Manifest Loading & Build Diagnostics +# ============================================================================ +# Package lists live in /ctx/manifests/*.list — data stays declarative, +# this script stays pure logic. dnf output is captured to a log file so +# failures are diagnosable from CI output instead of vanishing into /dev/null. + +readonly MANIFEST_DIR="/ctx/manifests" +readonly DNF_LOG="/tmp/distinction-dnf-build.log" +: > "$DNF_LOG" + +# Read a manifest file: strip comments and blank lines, emit tokens. +read_manifest() { + local file="$MANIFEST_DIR/$1" + if [[ ! -f "$file" ]]; then + log_error "Manifest not found: $file" + return 1 + fi + sed -e 's/#.*$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$file" \ + | grep -v '^$' } -dnf5() { - local arg - for arg in "$@"; do - if [[ "$arg" == "--allowerasing" ]]; then - log_error "Refusing to run dnf5 with --allowerasing (disallowed in this build)" - return 1 - fi - done - command dnf5 "$@" +# Show the tail of the dnf log after a failure (indented for readability). +dnf_log_tail() { + local lines="${1:-25}" + log_info "Last $lines lines of dnf output ($DNF_LOG):" + tail -n "$lines" "$DNF_LOG" | sed 's/^/ /' +} + +# Authenticated-if-possible GitHub API GET. Token sources (in order): +# 1. BuildKit secret mount (Containerfile: --mount=type=secret,id=github_token) +# 2. GITHUB_TOKEN env var (local builds) +# Falls back to anonymous (rate-limited) access if neither is present. +github_api_get() { + local url="$1" + local -a auth=() + if [[ -r /run/secrets/github_token ]]; then + auth=(-H "Authorization: Bearer $(< /run/secrets/github_token)") + elif [[ -n "${GITHUB_TOKEN:-}" ]]; then + auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}") + fi + curl -sfL --retry 3 --retry-delay 2 \ + -H "Accept: application/vnd.github+json" \ + "${auth[@]}" "$url" } +log_info "installing terra" +dnf5 -y install --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release +dnf5 -y install terra-release-extras +dnf5 -y install terra-release-mesa +dnf5 versionlock delete mesa # ============================================================================ # Package Installation Tracking # ============================================================================ @@ -79,13 +95,14 @@ install_packages_resilient() { [[ -n "$enable_opt" ]] && install_cmd+=("$enable_opt") install_cmd+=("${packages[@]}") - if "${install_cmd[@]}" &>/dev/null; then + if "${install_cmd[@]}" &>>"$DNF_LOG"; then log_success "Bulk installation from $repo succeeded" SUCCEEDED_PACKAGES+=("${packages[@]}") return 0 fi log_warning "Bulk installation from $repo failed, attempting individual installation" + dnf_log_tail 15 # Fall back to individual package installation local success_count=0 @@ -96,12 +113,13 @@ install_packages_resilient() { [[ -n "$enable_opt" ]] && single_install_cmd+=("$enable_opt") single_install_cmd+=("$pkg") - if "${single_install_cmd[@]}" &>/dev/null; then + if "${single_install_cmd[@]}" &>>"$DNF_LOG"; then log_success " ✓ $pkg" SUCCEEDED_PACKAGES+=("$pkg") ((success_count++)) else log_warning " ✗ $pkg (failed)" + dnf_log_tail 5 FAILED_PACKAGES+=("$pkg") ((fail_count++)) fi @@ -122,8 +140,9 @@ install_copr_resilient() { log_section "Installing from COPR: $copr_repo" # Enable COPR repository - if ! dnf5 -y copr enable "$copr_repo" &>/dev/null; then + if ! dnf5 -y copr enable "$copr_repo" &>>"$DNF_LOG"; then log_error "Failed to enable COPR: $copr_repo" + dnf_log_tail 10 SKIPPED_REPOS+=("copr:$copr_repo") FAILED_PACKAGES+=("${packages[@]}") return 1 @@ -132,26 +151,28 @@ install_copr_resilient() { log_info "Attempting to install ${#packages[@]} package(s)" # Attempt bulk installation - if dnf5 -y install "${packages[@]}" &>/dev/null; then + if dnf5 -y install "${packages[@]}" &>>"$DNF_LOG"; then log_success "Bulk installation from $copr_repo succeeded" SUCCEEDED_PACKAGES+=("${packages[@]}") - dnf5 -y copr disable "$copr_repo" &>/dev/null || true + dnf5 -y copr disable "$copr_repo" &>>"$DNF_LOG" || true return 0 fi log_warning "Bulk installation from $copr_repo failed, attempting individual installation" + dnf_log_tail 15 # Fall back to individual installation local success_count=0 local fail_count=0 for pkg in "${packages[@]}"; do - if dnf5 -y install "$pkg" &>/dev/null; then + if dnf5 -y install "$pkg" &>>"$DNF_LOG"; then log_success " ✓ $pkg" SUCCEEDED_PACKAGES+=("$pkg") ((success_count++)) else log_warning " ✗ $pkg (failed)" + dnf_log_tail 5 FAILED_PACKAGES+=("$pkg") ((fail_count++)) fi @@ -160,85 +181,7 @@ install_copr_resilient() { log_info "COPR $copr_repo: $success_count succeeded, $fail_count failed" # Disable COPR - dnf5 -y copr disable "$copr_repo" &>/dev/null || true -} - -# ============================================================================ -# RPM Fusion Fedora 43 Fallback Installation -# ============================================================================ -# Temporarily adds F43 rpmfusion repos to install packages missing from F44 - -install_rpmfusion_f43_fallback() { - local -a packages=("$@") - [[ ${#packages[@]} -eq 0 ]] && return 0 - - log_section "RPM Fusion F43 fallback for ${#packages[@]} package(s)" - - local tmp_free_repo="/etc/yum.repos.d/rpmfusion-free-f43-fallback.repo" - local tmp_nonfree_repo="/etc/yum.repos.d/rpmfusion-nonfree-f43-fallback.repo" - - tee "$tmp_free_repo" > /dev/null << 'EOF' -[rpmfusion-free-f43-fallback] -name=RPM Fusion for Fedora 43 - Free (F44 Fallback) -baseurl=https://mirrors.rpmfusion.org/free/fedora/43/$basearch/ -enabled=1 -gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-fedora-2020 -skip_if_unavailable=1 -EOF - - tee "$tmp_nonfree_repo" > /dev/null << 'EOF' -[rpmfusion-nonfree-f43-fallback] -name=RPM Fusion for Fedora 43 - NonFree (F44 Fallback) -baseurl=https://mirrors.rpmfusion.org/nonfree/fedora/43/$basearch/ -enabled=1 -gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020 -skip_if_unavailable=1 -EOF - - dnf5 makecache &>/dev/null || true - - local success_count=0 - local fail_count=0 - local -a still_failed=() - - for pkg in "${packages[@]}"; do - if dnf5 -y install \ - --enablerepo=rpmfusion-free-f43-fallback,rpmfusion-nonfree-f43-fallback \ - "$pkg" &>/dev/null; then - log_success " ✓ $pkg (via F43 fallback)" - SUCCEEDED_PACKAGES+=("$pkg") - ((success_count++)) - else - log_warning " ✗ $pkg (F43 fallback also failed)" - still_failed+=("$pkg") - ((fail_count++)) - fi - done - - # Rebuild FAILED_PACKAGES removing entries that succeeded via fallback - local -a updated_failed=() - for failed_pkg in "${FAILED_PACKAGES[@]}"; do - local still_in_failed=false - for sf in "${still_failed[@]}"; do - [[ "$failed_pkg" == "$sf" ]] && still_in_failed=true && break - done - # Keep if not one of the packages we just retried, or if it's still failing - local was_retried=false - for retried in "${packages[@]}"; do - [[ "$failed_pkg" == "$retried" ]] && was_retried=true && break - done - if ! $was_retried || $still_in_failed; then - updated_failed+=("$failed_pkg") - fi - done - FAILED_PACKAGES=("${updated_failed[@]}") - - rm -f "$tmp_free_repo" "$tmp_nonfree_repo" - dnf5 makecache &>/dev/null || true - - log_info "F43 fallback: $success_count succeeded, $fail_count failed" + dnf5 -y copr disable "$copr_repo" &>>"$DNF_LOG" || true } # ============================================================================ @@ -279,65 +222,6 @@ ensure_rpmfusion_keys() { fi } -# ============================================================================ -# Freeworld RPM Force-Install -# ============================================================================ -# Downloads freeworld RPMs from RPMFusion and installs via rpm to sidestep -# dnf dependency/conflict checks against Bazzite's terra-repo packages. -# -# libheif-freeworld --nodeps (F43 repo: not yet in F44) - -install_freeworld_rpms() { - log_section "Installing freeworld RPMs via rpm" - - local tmp_dir - tmp_dir=$(mktemp -d) - - # Shared helper: download one package from the given repo(s) and install it. - # Usage: _freeworld_pkg - _freeworld_pkg() { - local pkg="$1" repos="$2"; shift 2 - local rpm_flags=("$@") - - if dnf download --destdir="$tmp_dir" --enablerepo="$repos" "$pkg" &>/dev/null; then - local rpm_file - rpm_file=$(find "$tmp_dir" -name "${pkg}*.rpm" | sort -V | tail -1) - if [[ -n "$rpm_file" ]]; then - if rpm "${rpm_flags[@]}" -i "$rpm_file"; then - log_success "$pkg installed" - SUCCEEDED_PACKAGES+=("$pkg") - else - log_error "rpm install failed for $pkg" - FAILED_PACKAGES+=("$pkg") - fi - rm -f "$rpm_file" - else - log_error "Downloaded RPM not found for $pkg" - FAILED_PACKAGES+=("$pkg") - fi - else - log_warning "Failed to download $pkg" - FAILED_PACKAGES+=("$pkg") - fi - } - - # ── libheif-freeworld: not yet in F44 — pull from F43 repo temporarily ── - local f43_repo="/etc/yum.repos.d/rpmfusion-free-f43-freeworld-tmp.repo" - tee "$f43_repo" > /dev/null << 'EOF' -[rpmfusion-free-f43-freeworld-tmp] -name=RPM Fusion for Fedora 43 - Free (libheif-freeworld fallback) -baseurl=https://mirrors.rpmfusion.org/free/fedora/43/$basearch/ -enabled=1 -gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-fedora-2020 -skip_if_unavailable=1 -EOF - dnf5 makecache --repo=rpmfusion-free-f43-freeworld-tmp &>/dev/null || true - _freeworld_pkg "libheif-freeworld" "rpmfusion-free-f43-freeworld-tmp" --nodeps - rm -f "$f43_repo" - - rm -rf "$tmp_dir" -} # ============================================================================ # Resilient Single Package Installation @@ -377,12 +261,9 @@ log_info "Repositories and packages will be retried individually if bulk fails" log_section "Removing conflicting packages" # These packages MUST be removed (conflicts or unwanted) -readonly -a REMOVE_PACKAGES=( - "waydroid" # Not needed - "sunshine" # Not needed - "gnome-shell-extension-compiz-windows-effect" # Not needed - "openssh-askpass" # Not needed -) +# Edit build_files/manifests/packages-remove.list — not this script. +mapfile -t REMOVE_PACKAGES < <(read_manifest "packages-remove.list") +readonly -a REMOVE_PACKAGES removed_count=0 for pkg in "${REMOVE_PACKAGES[@]}"; do @@ -443,249 +324,24 @@ create_dir_with_log "/var/opt" "Package directory for 06-fix-opt.sh" log_section "Installing packages from configured repositories" -# Associative array mapping repositories to their packages -declare -A RPM_PACKAGES=( - # Core Fedora repositories - ["fedora"]="\ - zsh \ - zsh-syntax-highlighting \ - zsh-autosuggestions \ - neovim \ - file-roller \ - loupe \ - sassc \ - gstreamer1-plugins-good-extras \ - decibels \ - dconf \ - gtk-murrine-engine \ - perl-File-Copy \ - winetricks \ - lutris \ - sox \ - totem-video-thumbnailer \ - mediainfo \ - flatpak-builder \ - gnome-tweaks \ - freerdp \ - nss-mdns.i686 \ - pcsc-lite-libs.i686 \ - nmap-ncat \ - sane-backends-libs.i686 \ - sane-backends-libs.x86_64 \ - dcraw \ - perl-Image-ExifTool \ - libheif-tools \ - heif-pixbuf-loader \ - libgda \ - libgda-sqlite \ - libjxl-utils \ - foremost \ - filelight \ - clamav \ - diffpdf \ - id3v2 \ - lhasa \ - lzma \ - meld \ - pandoc-cli \ - rdfind \ - xorriso \ - optipng \ - glib2-devel \ - firejail" - - # vvenc and the freeworld codec stack are installed by the dedicated - # codec section below — they need atomic resolution against x265 and - # libavcodec-freeworld in the same transaction. - ["rpmfusion-free,rpmfusion-free-updates,rpmfusion-nonfree,rpmfusion-nonfree-updates"]="\ - gstreamer1-plugins-ugly" - - ["terra,terra-extras"]=" \ - gstreamer1-vaapi \ - mesa-libgbm \ - mesa-libEGL \ - mesa-libgbm-devel \ - mesa-libEGL-devel" - - # Fedora Multimedia (optimized multimedia packages) - ["fedora-multimedia"]="mpv" - - # Third-party repositories - ["brave-browser"]="brave-browser" +# Repository → manifest mapping. Package lists live in build_files/manifests/. +declare -A REPO_MANIFESTS=( + ["fedora"]="packages-fedora.list" + ["rpmfusion-free,rpmfusion-free-updates,rpmfusion-nonfree,rpmfusion-nonfree-updates"]="packages-rpmfusion.list" + ["terra,terra-extras"]="packages-terra.list" + ["fedora-multimedia"]="packages-fedora-multimedia.list" + ["brave-browser"]="packages-brave.list" ) # Install packages from standard repositories -for repo in "${!RPM_PACKAGES[@]}"; do - read -ra pkg_array <<<"${RPM_PACKAGES[$repo]}" - install_packages_resilient "$repo" "${pkg_array[@]}" -done - -# ────────────────────────────────────────────────────────────────────────── -# Codec Stack — atomic swap to RPMFusion freeworld variants -# ────────────────────────────────────────────────────────────────────────── -# Why this lives here, not in the force-install OCI artifact: -# -# libavcodec-freeworld, gstreamer1-plugins-bad-freeworld, and the -# mesa-*-freeworld pair link against versioned symbols from x265 and vvenc -# (e.g. x265_api_get_215, libvvenc.so.1.13). When these RPMs were snapshotted -# in the force-install artifact — which builds on a separate schedule from -# the main image — the captured .so files reference symbol versions that -# no longer exist on the rolling Bazzite base, producing runtime errors: -# -# ffmpeg: error while loading shared libraries: libvvenc.so.1.13 -# ffmpeg: symbol lookup error: ... undefined symbol: x265_api_get_215 -# -# Installing the whole freeworld codec stack in a single dnf transaction -# against the live repos co-resolves them with their current x265/vvenc -# dependencies — versions match by construction, no skew possible. - -log_section "Installing freeworld codec stack" - -# Narrowly-scoped escape hatch for the --allowerasing guard. -# Calls `command dnf5` directly (bypassing the wrapper) but only after -# validating that the swap pair is in CODEC_SWAP_ALLOWLIST. Any other use -# of --allowerasing remains blocked by the dnf/dnf5 wrappers above. -# -# Note: gstreamer1-plugins-bad-{free,freeworld} are NOT in this list — they -# are co-installable (bad-free ships the `va`/`festival` plugins; freeworld -# adds `x265enc`/`de265`/`rtmp`). Swapping them would silently lose `va` and -# break VAAPI HW decode. They are handled by a plain install below. -readonly -a CODEC_SWAP_ALLOWLIST=( - "ffmpeg-free|ffmpeg" - "libavcodec-free|libavcodec-freeworld" - "libavdevice-free|libavdevice-freeworld" - "libavfilter-free|libavfilter-freeworld" - "libavformat-free|libavformat-freeworld" - "libavutil-free|libavutil-freeworld" - "libpostproc-free|libpostproc-freeworld" - "libswresample-free|libswresample-freeworld" - "libswscale-free|libswscale-freeworld" - "mesa-va-drivers|mesa-va-drivers-freeworld" - "mesa-vulkan-drivers|mesa-vulkan-drivers-freeworld" -) - -dnf_codec_swap() { - local from="$1" to="$2" - # Optional third arg: arch suffix for the direct-install fallback path - # (e.g. "x86_64"). Some F44 RPMFusion freeworld packages ship a stale i686 - # build that requires a since-rotated x265 soname (libx265.so.215). The - # x86_64 build is current; constraining the install to x86_64 sidesteps - # the broken multilib pull on Bazzite (which enables i686 for Steam). - # Only applied when source isn't installed — the swap path leaves arch - # selection to dnf because it matches whatever arches the source had. - local arch_pref="${3:-}" - local pair="${from}|${to}" - - local allowed=false - for entry in "${CODEC_SWAP_ALLOWLIST[@]}"; do - [[ "$entry" == "$pair" ]] && allowed=true && break - done - - if ! $allowed; then - log_error " ✗ dnf_codec_swap: '$from' → '$to' not in allowlist" - FAILED_PACKAGES+=("$to") - return 1 - fi - - local err_file - err_file=$(mktemp) - - # If source isn't installed, fall back to a plain install of the target. - # This handles bases that already ship the freeworld variant, or where - # the -free counterpart was never installed. - if ! rpm -q "$from" &>/dev/null; then - local install_target="$to${arch_pref:+.$arch_pref}" - if command dnf5 -y install "$install_target" >/dev/null 2>"$err_file"; then - log_success " ✓ $install_target (direct install — no $from present)" - SUCCEEDED_PACKAGES+=("$to") - rm -f "$err_file" - return 0 - fi - log_warning " ✗ $install_target (direct install failed; dnf says:)" - sed 's/^/ /' "$err_file" | head -5 - FAILED_PACKAGES+=("$to") - rm -f "$err_file" - return 1 - fi - - # Source installed → swap. --allowerasing lets dnf remove the source - # if the target's file list overlaps. The allowlist above bounds the - # blast radius to known-safe codec pairs. - if command dnf5 -y swap "$from" "$to" --allowerasing >/dev/null 2>"$err_file"; then - log_success " ✓ $from → $to" - SUCCEEDED_PACKAGES+=("$to") - rm -f "$err_file" - return 0 - fi - - log_warning " ✗ $from → $to (swap failed; dnf says:)" - sed 's/^/ /' "$err_file" | head -5 - FAILED_PACKAGES+=("$to") - rm -f "$err_file" - return 1 -} - -# Order: top-level meta-packages first (ffmpeg pulls the libav* family), -# then GPU driver swaps. libavcodec-freeworld is a parallel-library package -# only meaningful when the system runs Fedora's `ffmpeg-free` — Bazzite -# ships RPMFusion's `ffmpeg` which bundles patent codecs in its own -# libavcodec, making libavcodec-freeworld redundant (and uninstallable -# because of file conflicts with the bundled libs). The swap below is a -# best-effort no-op in that common case; it only does work on bases that -# actually shipped Fedora's `libavcodec-free`. -dnf_codec_swap "ffmpeg-free" "ffmpeg" || true -dnf_codec_swap "mesa-va-drivers" "mesa-va-drivers-freeworld" || true -dnf_codec_swap "mesa-vulkan-drivers" "mesa-vulkan-drivers-freeworld" || true -# libavcodec-freeworld constrained to x86_64: F44 RPMFusion's i686 build -# (8.0.1-6.fc44) requires libx265.so.215, but the live x265 has moved to .216. -# No 32-bit userland needs the patent-codec libavcodec; Steam et al. use -# system ffmpeg-libs.i686 from base Fedora. -dnf_codec_swap "libavcodec-free" "libavcodec-freeworld" "x86_64" || true - -# gstreamer1-plugins-bad-freeworld: additive install (NOT a swap — see comment -# above CODEC_SWAP_ALLOWLIST). Provides x265enc, de265, rtmp. The base bad-free -# package (which ships `va`/VAAPI) must stay installed. -# -# Pinned to .x86_64 for the same reason as libavcodec-freeworld above — -# the F44 i686 RPM (1.28.1-3.fc44) is stale against the current x265 ABI. -log_info "Installing gstreamer1-plugins-bad-freeworld.x86_64 (additive on top of bad-free)" -gst_err=$(mktemp) -if command dnf5 -y install gstreamer1-plugins-bad-freeworld.x86_64 >/dev/null 2>"$gst_err"; then - log_success " ✓ gstreamer1-plugins-bad-freeworld.x86_64" - SUCCEEDED_PACKAGES+=("gstreamer1-plugins-bad-freeworld") -else - log_warning " ✗ gstreamer1-plugins-bad-freeworld.x86_64 (dnf says:)" - sed 's/^/ /' "$gst_err" | head -5 - FAILED_PACKAGES+=("gstreamer1-plugins-bad-freeworld") -fi -rm -f "$gst_err" - -# vvenc and svt-av1 round out the encoder set. RPMFusion-only (no -free -# counterpart). Pre-filter against rpm -q so dnf5 doesn't abort the whole -# transaction with "Failed to resolve" when packages are already installed -# as transitive deps of ffmpeg (which is the common case on Bazzite). -log_info "Installing standalone codec libraries (vvenc, svt-av1)" -vvenc_pkgs=() -for pkg in vvenc vvenc-libs svt-av1-libs; do - if ! rpm -q "$pkg" &>/dev/null; then - vvenc_pkgs+=("$pkg") +for repo in "${!REPO_MANIFESTS[@]}"; do + mapfile -t pkg_array < <(read_manifest "${REPO_MANIFESTS[$repo]}") + if [[ ${#pkg_array[@]} -eq 0 ]]; then + log_warning "Manifest ${REPO_MANIFESTS[$repo]} is empty — skipping $repo" + continue fi + install_packages_resilient "$repo" "${pkg_array[@]}" done -if [[ ${#vvenc_pkgs[@]} -eq 0 ]]; then - log_success " ✓ vvenc, vvenc-libs, svt-av1-libs (already installed via deps)" - SUCCEEDED_PACKAGES+=("vvenc" "vvenc-libs" "svt-av1-libs") -else - vvenc_err=$(mktemp) - if command dnf5 -y install "${vvenc_pkgs[@]}" >/dev/null 2>"$vvenc_err"; then - log_success " ✓ ${vvenc_pkgs[*]}" - SUCCEEDED_PACKAGES+=("${vvenc_pkgs[@]}") - else - log_warning " ✗ ${vvenc_pkgs[*]} (dnf says:)" - sed 's/^/ /' "$vvenc_err" | head -5 - FAILED_PACKAGES+=("${vvenc_pkgs[@]}") - fi - rm -f "$vvenc_err" -fi # ────────────────────────────────────────────────────────────────────────── # COPR Repository Packages @@ -694,44 +350,12 @@ fi log_section "Installing packages from COPR repositories" # COPR packages (handled separately due to enable/disable requirement) -declare -A COPR_PACKAGES=( - ["ilyaz/LACT"]="lact" # AMD GPU control - ["fernando-debian/dysk"]="dysk" # Disk usage analyzer - ["atim/heroic-games-launcher"]="heroic-games-launcher-bin" # Epic/GOG launcher - ["sergiomb/clonezilla"]="clonezilla" # Disk cloning utility - ["alternateved/eza"]="eza" # Modern ls replacement - #["monkeygold/nautilus-open-any-terminal"]="nautilus-open-any-terminal" -) - -for copr_repo in "${!COPR_PACKAGES[@]}"; do - read -ra pkg_array <<<"${COPR_PACKAGES[$copr_repo]}" +# Edit build_files/manifests/coprs.list — format: [pkg ...] +while read -r copr_repo copr_pkgs; do + [[ -z "$copr_repo" ]] && continue + read -ra pkg_array <<<"$copr_pkgs" install_copr_resilient "$copr_repo" "${pkg_array[@]}" -done - -# ────────────────────────────────────────────────────────────────────────── -# RPM Fusion F43 Fallback -# ────────────────────────────────────────────────────────────────────────── -# RPM Fusion F44 repos may not be fully populated yet; retry failures via F43 - -readonly -a RPMFUSION_PACKAGES=( - "libheif-freeworld" -) - -rpmfusion_failed=() -for pkg in "${RPMFUSION_PACKAGES[@]}"; do - for failed in "${FAILED_PACKAGES[@]}"; do - if [[ "$pkg" == "$failed" ]]; then - rpmfusion_failed+=("$pkg") - break - fi - done -done - -if [[ ${#rpmfusion_failed[@]} -gt 0 ]]; then - install_rpmfusion_f43_fallback "${rpmfusion_failed[@]}" -else - log_info "No RPM Fusion packages require F43 fallback" -fi +done < <(read_manifest "coprs.list") # ============================================================================ # Special Package Installations @@ -745,24 +369,28 @@ log_section "Installing special packages" log_info "Installing Kora icon theme (latest release)" -# Get latest release URL -kora_url=$(curl -s https://api.github.com/repos/phantomcortex/kora/releases/latest 2>/dev/null | \ - grep "browser_download_url.*\.rpm" | \ - cut -d '"' -f 4) +# Get latest release URL — authenticated when a token is available (see +# github_api_get), since anonymous API calls share a per-IP rate limit pool +# on CI runners and fail intermittently. +kora_json=$(github_api_get "https://api.github.com/repos/phantomcortex/kora/releases/latest" || true) + +kora_url="" +if [[ -n "$kora_json" ]]; then + if command -v jq &>/dev/null; then + kora_url=$(jq -r '.assets[].browser_download_url | select(endswith(".rpm"))' <<<"$kora_json" | head -1) + else + # Fallback parse if jq is unavailable for any reason + kora_url=$(grep "browser_download_url.*\.rpm" <<<"$kora_json" | cut -d '"' -f 4 | head -1) + fi +fi if [[ -n "$kora_url" ]]; then install_single_package_resilient "$kora_url" "kora-icon-theme" else - log_warning "Failed to retrieve Kora theme URL (GitHub API may be down)" + log_warning "Failed to retrieve Kora theme URL (GitHub API unavailable or rate-limited)" FAILED_PACKAGES+=("kora-icon-theme") fi -# ────────────────────────────────────────────────────────────────────────── -# Freeworld RPMs (libheif-freeworld from F43 — not yet in F44) -# ────────────────────────────────────────────────────────────────────────── - -install_freeworld_rpms - # ============================================================================ # Critical Package Validation @@ -771,41 +399,9 @@ install_freeworld_rpms log_section "Validating critical packages" -readonly -a CRITICAL_PACKAGES=( - "zsh" - "podman" - "steam.i686" - "steam-devices" - "gnome-shell" - "gnome-session" - "vulkan-headers" - "mesa-vulkan-drivers-freeworld" # post-swap: replaces mesa-vulkan-drivers - "mesa-va-drivers-freeworld" # post-swap: replaces mesa-va-drivers - "mesa-filesystem" - "mesa-dri-drivers" - "mesa-libGL" - "mesa-libEGL" - "mesa-libgbm" - "mesa-libgbm-devel" - "mesa-libEGL" - "mesa-libEGL-devel" - "ffmpeg" - # libavcodec-freeworld is intentionally NOT here: when ffmpeg is the - # RPMFusion freeworld variant (Bazzite default), it bundles its own - # libavcodec with patent codecs, making libavcodec-freeworld redundant - # and uninstallable due to file conflicts. The codec validator in - # 08-validate.sh asserts the actual encoders exist regardless of which - # package provides them. - "vvenc-libs" # H.266/VVC encoder runtime - "gstreamer1-plugins-bad-freeworld" - "mutter" - "wayland-devel" - "ScopeBuddy" - "terra-gamescope" - "SDL3" - "glib2" - "glib2-devel" -) +# Edit build_files/manifests/packages-critical.list — not this script. +mapfile -t CRITICAL_PACKAGES < <(read_manifest "packages-critical.list") +readonly -a CRITICAL_PACKAGES validation_failures=0 for pkg in "${CRITICAL_PACKAGES[@]}"; do @@ -836,78 +432,33 @@ else fi # ============================================================================ -# Wine Fallback Installation +# Inter Font (pinned v4.0) # ============================================================================ -# If Wine failed earlier, try one more time with --skip-broken -if ! rpm -q wine &>/dev/null; then - log_section "Wine fallback installation" - log_info "Attempting Wine installation with --skip-broken" - - if dnf5 -y install wine --skip-broken &>/dev/null; then - log_success "Wine installed via fallback method" - SUCCEEDED_PACKAGES+=("wine") +log_section "Installing Inter font v4.0" + +readonly INTER_URL="https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" + +if curl -fL --retry 3 --retry-delay 2 "$INTER_URL" -o /tmp/Inter.zip; then + mkdir -p /usr/share/fonts/Inter/ + if unzip -j -o /tmp/Inter.zip "InterVariable.ttf" "InterVariable-Italic.ttf" \ + -d /usr/share/fonts/Inter/ &>>"$DNF_LOG"; then + fc-cache -f &>/dev/null || true + log_success "Inter font installed" else - log_warning "Wine installation failed even with --skip-broken" + log_warning "Inter zip extraction failed (non-critical)" fi + rm -f /tmp/Inter.zip +else + log_warning "Inter font download failed (non-critical)" fi -curl -L "https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" -o /tmp/Inter.zip -mkdir -p /usr/share/fonts/Inter/ -unzip -j /tmp/Inter.zip "InterVariable.ttf" "InterVariable-Italic.ttf" -d /usr/share/fonts/Inter/ -rm /tmp/Inter.zip && fc-cache -f - -# ============================================================================ -# versionlock -# ============================================================================ -# while this isn't entirely neccessary, it's good peace of mind - -readonly -a VERSIONLOCK_PACKAGES=( - "zsh" - "podman" - "lact" - "file-roller" - "dcraw" - "sassc" - "steam" - "steam-devices" - "gnome-shell" - "gnome-session" - "vulkan-headers" - "mesa-vulkan-drivers-freeworld" # post-swap name - "mesa-va-drivers-freeworld" # post-swap name - "mesa-filesystem" - "mesa-dri-drivers" - "mesa-libGL" - "mesa-libEGL" - "mesa-libgbm" - "mesa-libgbm-devel" - "mesa-libEGL" - "mesa-libEGL-devel" - "ffmpeg" - # libavcodec-freeworld omitted — see CRITICAL_PACKAGES for rationale - "vvenc" - "vvenc-libs" - "gstreamer1-plugins-bad-freeworld" - "mutter" - "wayland-devel" - "ScopeBuddy" - "terra-gamescope" - "gcc" - "SDL3" - "glib2" - "glib2-devel" -) -log_info "versionlock section" -versionlock_failures=0 -for pkg in "${VERSIONLOCK_PACKAGES[@]}"; do - if dnf versionlock add "$pkg" &>/dev/null; then - log_success " ✓ $pkg" - else - log_warning " ✗ $pkg (unable to add versionlock)" - ((versionlock_failures++)) - fi -done +# NOTE: the former "versionlock add" section was removed deliberately. +# dnf versionlock has no effect on a deployed rpm-ostree/bootc system — +# updates arrive as whole image rebuilds from a fresh base, so the locks +# were never consulted. (The `versionlock clear` near the top remains, as +# it removes any locks inherited from the base image that could block +# installs during THIS build.) # ============================================================================ # Cleanup diff --git a/build_files/manifests/coprs.list b/build_files/manifests/coprs.list new file mode 100644 index 0000000..216f879 --- /dev/null +++ b/build_files/manifests/coprs.list @@ -0,0 +1,8 @@ +# COPR repositories and their packages. +# Format: [package ...] +ilyaz/LACT lact # AMD GPU control +fernando-debian/dysk dysk # Disk usage analyzer +atim/heroic-games-launcher heroic-games-launcher-bin # Epic/GOG launcher +sergiomb/clonezilla clonezilla # Disk cloning utility +alternateved/eza eza # Modern ls replacement +#monkeygold/nautilus-open-any-terminal nautilus-open-any-terminal diff --git a/build_files/manifests/packages-brave.list b/build_files/manifests/packages-brave.list new file mode 100644 index 0000000..babe835 --- /dev/null +++ b/build_files/manifests/packages-brave.list @@ -0,0 +1,2 @@ +# repo: brave-browser +brave-browser diff --git a/build_files/manifests/packages-critical.list b/build_files/manifests/packages-critical.list new file mode 100644 index 0000000..2861fcb --- /dev/null +++ b/build_files/manifests/packages-critical.list @@ -0,0 +1,26 @@ +# Critical packages validated at the end of 02-build.sh. +# Missing entries are reported but do not fail the build (steam excepted — +# see the hard requirement in 02-build.sh). +zsh +podman +steam.i686 +steam-devices +gnome-shell +gnome-session +vulkan-headers +mesa-vulkan-drivers +mesa-filesystem +mesa-dri-drivers +mesa-libGL +mesa-libEGL +mesa-libgbm +mesa-libgbm-devel +mesa-libEGL-devel +ffmpeg +mutter +wayland-devel +ScopeBuddy +terra-gamescope +SDL3 +glib2 +glib2-devel diff --git a/build_files/manifests/packages-fedora-multimedia.list b/build_files/manifests/packages-fedora-multimedia.list new file mode 100644 index 0000000..218ce58 --- /dev/null +++ b/build_files/manifests/packages-fedora-multimedia.list @@ -0,0 +1,2 @@ +# repo: fedora-multimedia (optimized multimedia packages) +mpv diff --git a/build_files/manifests/packages-fedora.list b/build_files/manifests/packages-fedora.list new file mode 100644 index 0000000..26762ab --- /dev/null +++ b/build_files/manifests/packages-fedora.list @@ -0,0 +1,49 @@ +# Core Fedora repository packages +zsh +zsh-syntax-highlighting +zsh-autosuggestions +neovim +file-roller +loupe +sassc +gstreamer1-plugins-good-extras +gstreamer1-vaapi +decibels +dconf +gtk-murrine-engine +perl-File-Copy +winetricks +lutris +sox +totem-video-thumbnailer +mediainfo +flatpak-builder +gnome-tweaks +freerdp +nss-mdns.i686 +pcsc-lite-libs.i686 +nmap-ncat +sane-backends-libs.i686 +sane-backends-libs.x86_64 +dcraw +perl-Image-ExifTool +libheif-tools +heif-pixbuf-loader +libgda +libgda-sqlite +libjxl-utils +foremost +filelight +clamav +diffpdf +id3v2 +lhasa +lzma +meld +pandoc-cli +rdfind +xorriso +optipng +glib2-devel +firejail +jq # JSON parsing (used by the build itself for GitHub API) diff --git a/build_files/manifests/packages-remove.list b/build_files/manifests/packages-remove.list new file mode 100644 index 0000000..3e82a8e --- /dev/null +++ b/build_files/manifests/packages-remove.list @@ -0,0 +1,6 @@ +# Packages removed from the base image (conflicts or unwanted) +# One package per line; '#' begins a comment. +waydroid # Not needed +sunshine # Not needed +gnome-shell-extension-compiz-windows-effect # Not needed +openssh-askpass # Not needed diff --git a/build_files/manifests/packages-rpmfusion.list b/build_files/manifests/packages-rpmfusion.list new file mode 100644 index 0000000..c2afe04 --- /dev/null +++ b/build_files/manifests/packages-rpmfusion.list @@ -0,0 +1,2 @@ +# repo: rpmfusion-free,rpmfusion-free-updates,rpmfusion-nonfree,rpmfusion-nonfree-updates +gstreamer1-plugins-ugly diff --git a/build_files/manifests/packages-terra.list b/build_files/manifests/packages-terra.list new file mode 100644 index 0000000..b36baf0 --- /dev/null +++ b/build_files/manifests/packages-terra.list @@ -0,0 +1,9 @@ +# repo: terra,terra-extras +mesa-filesystem +mesa-va-drivers +mesa-vulkan-drivers +mesa-dri-drivers +mesa-libgbm +mesa-libEGL +mesa-libgbm-devel +mesa-libEGL-devel diff --git a/system_files/etc/sudoers.d/99-distinction-wheel-nopasswd b/system_files/etc/sudoers.d/99-distinction-wheel-nopasswd index 8d1cb17..6b610d7 100644 --- a/system_files/etc/sudoers.d/99-distinction-wheel-nopasswd +++ b/system_files/etc/sudoers.d/99-distinction-wheel-nopasswd @@ -1,4 +1,22 @@ -# DistinctionOS - Passwordless sudo for wheel group -# WARNING: This reduces security - members of wheel group won't need passwords for sudo -# If you share this computer with others, You should remove other users from the wheel group -%wheel ALL=(ALL:ALL) NOPASSWD: ALL +# DistinctionOS — sudo policy for the wheel group +# +# OPTION A (current behaviour): full passwordless sudo. Maximum convenience, +# minimum defence — any compromised process in your session is root. +# %wheel ALL=(ALL:ALL) NOPASSWD: ALL +# +# OPTION B (recommended middle path, active below): +# 1. Passwordless sudo scoped to the system-management commands used daily +# on an atomic system — covers ~95% of routine sudo invocations. +# 2. A 60-minute credential cache for everything else, so a password is +# asked at most once an hour rather than per command. +# +# To revert to Option A: comment the lines below and uncomment the line above. + +Defaults timestamp_timeout=60 + +Cmnd_Alias DISTINCTION_ATOMIC = /usr/bin/rpm-ostree, /usr/bin/bootc, /usr/bin/ostree +Cmnd_Alias DISTINCTION_SVC = /usr/bin/systemctl +Cmnd_Alias DISTINCTION_PKG = /usr/bin/flatpak + +%wheel ALL=(ALL:ALL) NOPASSWD: DISTINCTION_ATOMIC, DISTINCTION_SVC, DISTINCTION_PKG +%wheel ALL=(ALL:ALL) ALL From df39475ba6428f9249399416722f15f8414542eb Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:16:09 -0600 Subject: [PATCH 09/13] Small consistency fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - distinction-functions.sh: the CYAN constant was \033[031m, which is red, not cyan. Switch to \033[36m so the variable name matches what the terminal renders (used in the rm-confirm prompt and the `naut` helper). - 05-remote-grabber.sh: shebang `/bin/bash` → `/usr/bin/bash`, the last hold-out among build_files/*.sh. - Rename .github/workflows/clean-images.yaml → .yml so all workflow files share one extension. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/{clean-images.yaml => clean-images.yml} | 0 build_files/05-remote-grabber.sh | 2 +- system_files/etc/profile.d/distinction-functions.sh | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{clean-images.yaml => clean-images.yml} (100%) diff --git a/.github/workflows/clean-images.yaml b/.github/workflows/clean-images.yml similarity index 100% rename from .github/workflows/clean-images.yaml rename to .github/workflows/clean-images.yml diff --git a/build_files/05-remote-grabber.sh b/build_files/05-remote-grabber.sh index 40b6bc9..d555d50 100755 --- a/build_files/05-remote-grabber.sh +++ b/build_files/05-remote-grabber.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/bash # ============================================================================= # GNOME Shell Extensions — Intelligent Installer diff --git a/system_files/etc/profile.d/distinction-functions.sh b/system_files/etc/profile.d/distinction-functions.sh index d054ca2..4649093 100644 --- a/system_files/etc/profile.d/distinction-functions.sh +++ b/system_files/etc/profile.d/distinction-functions.sh @@ -69,7 +69,7 @@ extract() { rm() { if [[ -d "$1" ]]; then local file_count=$(find "$1" -type f | wc -l) - local CYAN="\033[031m" + local CYAN="\033[36m" local NC="\033[0m" echo -e "This is a directory containing $CYAN$file_count$NC files." echo -n "Are you quite certain you wish to delete it? [y/N] " @@ -120,7 +120,7 @@ naut() { echo "Directory '$target_dir' does not exist." return 1 fi - local CYAN="\033[031m" + local CYAN="\033[36m" local NC="\033[0m" nautilus "$target_dir" >/dev/null 2>&1 & disown From 6d905bd7364214fa8665f0d67dbc7d4e8e404c5f Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:21:42 -0600 Subject: [PATCH 10/13] Dedup OCI-artifact build workflows behind a reusable workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-cache.yml and build-force-install.yml were ~95% identical: same checkout/login, same skopeo inspect, same version-diff decision logic, same buildx push. Move the shared body into a new _build-oci-artifact.yml that exposes `image`, `builder_dir`, `version_check_tag`, `max_age_days`, and `force_rebuild` as workflow_call inputs. The callers shrink to ~30 lines apiece — they keep their own cron, workflow_dispatch, and workflow_call triggers (preserving any external cross-repo callers), set permissions, and forward inputs to the reusable workflow with `secrets: inherit`. Other changes folded in: - Action versions in the reusable workflow are SHA-pinned with `# vN` annotations, matching build.yml's hardening style (build-cache / build-force-install previously used floating `@v6` / `@v4` / `@v7` tags, so dependabot can now pin minor bumps the same way it does for build.yml). - Documented the 15-day cadence on build-cache.yml: build.yml and build-force-install.yml run every 10 days; cache runs every 15 because its upstream sources rarely change, and the workflow's own inspect→decide step is the real freshness gate (cron is just the heartbeat). Co-Authored-By: Claude Opus 4.7 --- .github/workflows/_build-oci-artifact.yml | 152 ++++++++++++++++++++++ .github/workflows/build-cache.yml | 132 +++---------------- .github/workflows/build-force-install.yml | 126 ++---------------- 3 files changed, 178 insertions(+), 232 deletions(-) create mode 100644 .github/workflows/_build-oci-artifact.yml diff --git a/.github/workflows/_build-oci-artifact.yml b/.github/workflows/_build-oci-artifact.yml new file mode 100644 index 0000000..80990fb --- /dev/null +++ b/.github/workflows/_build-oci-artifact.yml @@ -0,0 +1,152 @@ +name: Build OCI artifact (internal) + +# Internal reusable workflow. Shared body of build-cache.yml and +# build-force-install.yml — those workflows are thin callers that pass +# the artifact-specific inputs (image, builder context, version-check +# tag) and inherit secrets. Not meant to be triggered directly; invoke a +# caller workflow instead. + +on: + workflow_call: + inputs: + image: + description: 'Full registry path of the artifact to publish (no tag).' + type: string + required: true + builder_dir: + description: 'Directory containing the artifact Containerfile.' + type: string + required: true + version_check_tag: + description: 'Local docker tag for the version-check stage build.' + type: string + required: true + max_age_days: + description: 'Rebuild if the existing artifact is older than this many days.' + type: number + default: 15 + force_rebuild: + description: 'Rebuild even if versions match and age <= max_age_days.' + type: boolean + default: false + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + + - name: Install tooling + run: | + sudo apt-get update -q + sudo apt-get install -y -q skopeo jq + + - name: Log in to GHCR + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Inspect published artifact + id: current + env: + IMAGE: ${{ inputs.image }} + run: | + if skopeo inspect "docker://${IMAGE}:latest" > current.json 2>/dev/null; then + jq -S -c '.Labels["dev.distinctionos.versions"] // "{}" | fromjson? // {}' current.json > current-versions.json + jq -r '.Created' current.json > current-created.txt + echo "found=true" >> "$GITHUB_OUTPUT" + echo "Current artifact:" + echo " Created: $(cat current-created.txt)" + echo " Versions: $(cat current-versions.json)" + else + echo '{}' > current-versions.json + echo '1970-01-01T00:00:00Z' > current-created.txt + echo "found=false" >> "$GITHUB_OUTPUT" + echo "No artifact published at ${IMAGE}:latest yet" + fi + + - name: Query upstream versions + id: upstream + env: + BUILDER_DIR: ${{ inputs.builder_dir }} + CHECK_TAG: ${{ inputs.version_check_tag }} + run: | + docker build \ + --target version-check \ + -t "${CHECK_TAG}" \ + -f "${BUILDER_DIR}/Containerfile" \ + "${BUILDER_DIR}" + docker run --rm "${CHECK_TAG}" > upstream-versions.json + echo "Upstream versions: $(cat upstream-versions.json)" + + - name: Decide whether to rebuild + id: decide + env: + MAX_AGE_DAYS: ${{ inputs.max_age_days }} + run: | + BUILD=false + REASON="" + + if [[ "${{ inputs.force_rebuild }}" == "true" ]]; then + BUILD=true; REASON="force_rebuild input is true" + elif [[ "${{ steps.current.outputs.found }}" == "false" ]]; then + BUILD=true; REASON="no existing artifact" + elif ! diff -q current-versions.json upstream-versions.json >/dev/null 2>&1; then + BUILD=true; REASON="upstream versions changed" + echo "Diff (current → upstream):" + diff -u current-versions.json upstream-versions.json || true + else + NOW=$(date +%s) + CREATED=$(date -d "$(cat current-created.txt)" +%s) + AGE_DAYS=$(( (NOW - CREATED) / 86400 )) + if [[ $AGE_DAYS -gt $MAX_AGE_DAYS ]]; then + BUILD=true; REASON="artifact is ${AGE_DAYS}d old (max ${MAX_AGE_DAYS}d)" + else + REASON="artifact is ${AGE_DAYS}d old, versions match; skipping" + fi + fi + + echo "build=${BUILD}" >> "$GITHUB_OUTPUT" + echo "reason=${REASON}" >> "$GITHUB_OUTPUT" + echo "::notice::Decision: build=${BUILD} (${REASON})" + + - name: Prepare build metadata + if: steps.decide.outputs.build == 'true' + id: meta + run: | + VERSIONS=$(jq -c '.' upstream-versions.json) + DATE=$(date -u +%Y%m%d) + echo "versions=${VERSIONS}" >> "$GITHUB_OUTPUT" + echo "date=${DATE}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + if: steps.decide.outputs.build == 'true' + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 + + - name: Build and push artifact + if: steps.decide.outputs.build == 'true' + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 + with: + context: ${{ inputs.builder_dir }} + file: ${{ inputs.builder_dir }}/Containerfile + target: artifact + push: true + tags: | + ${{ inputs.image }}:latest + ${{ inputs.image }}:${{ steps.meta.outputs.date }} + labels: | + dev.distinctionos.versions=${{ steps.meta.outputs.versions }} + org.opencontainers.image.source=https://github.com/${{ github.repository }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Report skip + if: steps.decide.outputs.build == 'false' + run: echo "::notice::No rebuild needed — ${{ steps.decide.outputs.reason }}" diff --git a/.github/workflows/build-cache.yml b/.github/workflows/build-cache.yml index 5f46b1e..85487f7 100644 --- a/.github/workflows/build-cache.yml +++ b/.github/workflows/build-cache.yml @@ -2,15 +2,17 @@ name: Build Cache RPM Artifact on: schedule: - # Every 5 days at 06:00 UTC. The job inspects the published artifact and - # only rebuilds when (a) the manifest's upstream versions changed, or - # (b) the published artifact is older than MAX_AGE_DAYS. So this cron - # is a *check* cadence — it almost always exits without building. + # Every 15 days at 06:00 UTC — deliberately slower than build.yml / + # build-force-install.yml (both 10d). The cache artifact's upstream + # sources rarely change, and the reusable workflow self-checks + # (inspect → decide) before rebuilding regardless of cron, so this + # is just the "look and probably skip" heartbeat. `max_age_days` + # below is the real freshness ceiling. - cron: '0 6 */15 * *' workflow_dispatch: inputs: force_rebuild: - description: 'Rebuild even if versions match and age <= MAX_AGE_DAYS' + description: 'Rebuild even if versions match and age <= max_age_days' type: boolean default: false workflow_call: @@ -23,117 +25,13 @@ permissions: contents: read packages: write -env: - IMAGE: ghcr.io/phantomcortex/distinctionos-cache - BUILDER_DIR: .github/cache-builder - MAX_AGE_DAYS: 15 - jobs: build: - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Install tooling - run: | - sudo apt-get update -q - sudo apt-get install -y -q skopeo jq - - - name: Log in to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Inspect published artifact - id: current - run: | - if skopeo inspect "docker://${IMAGE}:latest" > current.json 2>/dev/null; then - jq -S -c '.Labels["dev.distinctionos.versions"] // "{}" | fromjson? // {}' current.json > current-versions.json - jq -r '.Created' current.json > current-created.txt - echo "found=true" >> "$GITHUB_OUTPUT" - echo "Current artifact:" - echo " Created: $(cat current-created.txt)" - echo " Versions: $(cat current-versions.json)" - else - echo '{}' > current-versions.json - echo '1970-01-01T00:00:00Z' > current-created.txt - echo "found=false" >> "$GITHUB_OUTPUT" - echo "No artifact published at ${IMAGE}:latest yet" - fi - - - name: Query upstream versions - id: upstream - run: | - docker build \ - --target version-check \ - -t cache-version-check \ - -f "${BUILDER_DIR}/Containerfile" \ - "${BUILDER_DIR}" - docker run --rm cache-version-check > upstream-versions.json - echo "Upstream versions: $(cat upstream-versions.json)" - - - name: Decide whether to rebuild - id: decide - run: | - BUILD=false - REASON="" - - if [[ "${{ inputs.force_rebuild }}" == "true" ]]; then - BUILD=true; REASON="force_rebuild input is true" - elif [[ "${{ steps.current.outputs.found }}" == "false" ]]; then - BUILD=true; REASON="no existing artifact" - elif ! diff -q current-versions.json upstream-versions.json >/dev/null 2>&1; then - BUILD=true; REASON="upstream versions changed" - echo "Diff (current → upstream):" - diff -u current-versions.json upstream-versions.json || true - else - NOW=$(date +%s) - CREATED=$(date -d "$(cat current-created.txt)" +%s) - AGE_DAYS=$(( (NOW - CREATED) / 86400 )) - if [[ $AGE_DAYS -gt $MAX_AGE_DAYS ]]; then - BUILD=true; REASON="artifact is ${AGE_DAYS}d old (max ${MAX_AGE_DAYS}d)" - else - REASON="artifact is ${AGE_DAYS}d old, versions match; skipping" - fi - fi - - echo "build=${BUILD}" >> "$GITHUB_OUTPUT" - echo "reason=${REASON}" >> "$GITHUB_OUTPUT" - echo "::notice::Decision: build=${BUILD} (${REASON})" - - - name: Prepare build metadata - if: steps.decide.outputs.build == 'true' - id: meta - run: | - VERSIONS=$(jq -c '.' upstream-versions.json) - DATE=$(date -u +%Y%m%d) - echo "versions=${VERSIONS}" >> "$GITHUB_OUTPUT" - echo "date=${DATE}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - if: steps.decide.outputs.build == 'true' - uses: docker/setup-buildx-action@v4 - - - name: Build and push artifact - if: steps.decide.outputs.build == 'true' - uses: docker/build-push-action@v7 - with: - context: ${{ env.BUILDER_DIR }} - file: ${{ env.BUILDER_DIR }}/Containerfile - target: artifact - push: true - tags: | - ${{ env.IMAGE }}:latest - ${{ env.IMAGE }}:${{ steps.meta.outputs.date }} - labels: | - dev.distinctionos.versions=${{ steps.meta.outputs.versions }} - org.opencontainers.image.source=https://github.com/${{ github.repository }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Report skip - if: steps.decide.outputs.build == 'false' - run: echo "::notice::No rebuild needed — ${{ steps.decide.outputs.reason }}" + uses: ./.github/workflows/_build-oci-artifact.yml + with: + image: ghcr.io/phantomcortex/distinctionos-cache + builder_dir: .github/cache-builder + version_check_tag: cache-version-check + max_age_days: 15 + force_rebuild: ${{ inputs.force_rebuild || false }} + secrets: inherit diff --git a/.github/workflows/build-force-install.yml b/.github/workflows/build-force-install.yml index c0a23de..42d94de 100644 --- a/.github/workflows/build-force-install.yml +++ b/.github/workflows/build-force-install.yml @@ -2,13 +2,13 @@ name: Build Force-Install RPM Artifact on: schedule: - # Every 5 days at 06:30 UTC (offset from cache to avoid contention). - # See build-cache.yml for the rebuild-decision logic — same pattern here. + # Every 10 days at 06:30 UTC (offset 25min from build.yml to avoid + # contention). - cron: '30 6 */10 * *' workflow_dispatch: inputs: force_rebuild: - description: 'Rebuild even if versions match and age <= MAX_AGE_DAYS' + description: 'Rebuild even if versions match and age <= max_age_days' type: boolean default: false workflow_call: @@ -21,117 +21,13 @@ permissions: contents: read packages: write -env: - IMAGE: ghcr.io/phantomcortex/distinctionos-force-install - BUILDER_DIR: .github/force-install-builder - MAX_AGE_DAYS: 15 - jobs: build: - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Install tooling - run: | - sudo apt-get update -q - sudo apt-get install -y -q skopeo jq - - - name: Log in to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Inspect published artifact - id: current - run: | - if skopeo inspect "docker://${IMAGE}:latest" > current.json 2>/dev/null; then - jq -S -c '.Labels["dev.distinctionos.versions"] // "{}" | fromjson? // {}' current.json > current-versions.json - jq -r '.Created' current.json > current-created.txt - echo "found=true" >> "$GITHUB_OUTPUT" - echo "Current artifact:" - echo " Created: $(cat current-created.txt)" - echo " Versions: $(cat current-versions.json)" - else - echo '{}' > current-versions.json - echo '1970-01-01T00:00:00Z' > current-created.txt - echo "found=false" >> "$GITHUB_OUTPUT" - echo "No artifact published at ${IMAGE}:latest yet" - fi - - - name: Query upstream versions - id: upstream - run: | - docker build \ - --target version-check \ - -t force-install-version-check \ - -f "${BUILDER_DIR}/Containerfile" \ - "${BUILDER_DIR}" - docker run --rm force-install-version-check > upstream-versions.json - echo "Upstream versions: $(cat upstream-versions.json)" - - - name: Decide whether to rebuild - id: decide - run: | - BUILD=false - REASON="" - - if [[ "${{ inputs.force_rebuild }}" == "true" ]]; then - BUILD=true; REASON="force_rebuild input is true" - elif [[ "${{ steps.current.outputs.found }}" == "false" ]]; then - BUILD=true; REASON="no existing artifact" - elif ! diff -q current-versions.json upstream-versions.json >/dev/null 2>&1; then - BUILD=true; REASON="upstream versions changed" - echo "Diff (current → upstream):" - diff -u current-versions.json upstream-versions.json || true - else - NOW=$(date +%s) - CREATED=$(date -d "$(cat current-created.txt)" +%s) - AGE_DAYS=$(( (NOW - CREATED) / 86400 )) - if [[ $AGE_DAYS -gt $MAX_AGE_DAYS ]]; then - BUILD=true; REASON="artifact is ${AGE_DAYS}d old (max ${MAX_AGE_DAYS}d)" - else - REASON="artifact is ${AGE_DAYS}d old, versions match; skipping" - fi - fi - - echo "build=${BUILD}" >> "$GITHUB_OUTPUT" - echo "reason=${REASON}" >> "$GITHUB_OUTPUT" - echo "::notice::Decision: build=${BUILD} (${REASON})" - - - name: Prepare build metadata - if: steps.decide.outputs.build == 'true' - id: meta - run: | - VERSIONS=$(jq -c '.' upstream-versions.json) - DATE=$(date -u +%Y%m%d) - echo "versions=${VERSIONS}" >> "$GITHUB_OUTPUT" - echo "date=${DATE}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - if: steps.decide.outputs.build == 'true' - uses: docker/setup-buildx-action@v4 - - - name: Build and push artifact - if: steps.decide.outputs.build == 'true' - uses: docker/build-push-action@v7 - with: - context: ${{ env.BUILDER_DIR }} - file: ${{ env.BUILDER_DIR }}/Containerfile - target: artifact - push: true - tags: | - ${{ env.IMAGE }}:latest - ${{ env.IMAGE }}:${{ steps.meta.outputs.date }} - labels: | - dev.distinctionos.versions=${{ steps.meta.outputs.versions }} - org.opencontainers.image.source=https://github.com/${{ github.repository }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Report skip - if: steps.decide.outputs.build == 'false' - run: echo "::notice::No rebuild needed — ${{ steps.decide.outputs.reason }}" + uses: ./.github/workflows/_build-oci-artifact.yml + with: + image: ghcr.io/phantomcortex/distinctionos-force-install + builder_dir: .github/force-install-builder + version_check_tag: force-install-version-check + max_age_days: 15 + force_rebuild: ${{ inputs.force_rebuild || false }} + secrets: inherit From e167ef275ebe7f57eb002e3f1023f53dd16739f0 Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:42:23 -0600 Subject: [PATCH 11/13] Drop BIB fork pin, dead mkcd branches, Nautilus override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build-disk.yml: switch BIB_IMAGE from the lorbuschris fork pin (`:20250608`) back to upstream `quay.io/centos-bootc/bootc-image-builder:latest`. osbuild#954 — the reason for the fork — merged 2025-06-13. - distinction-functions.sh `mkcd()`: collapse the three-branch case "$SHELL_TYPE" to a single printf. SHELL_TYPE was never set anywhere, so the zsh and bash branches were unreachable; the printf fallback produces the same output and works in either shell. (Not addressed here: the file still uses zsh-only `read -q` in rm() while declaring `#!/bin/sh` — separate sweep.) - distinctionos-steam-linker.service: replace the placeholder `Documentation=https://github.com/yourusername/DistinctionOS` with the real repo URL. - Nautilus: delete the 320-line org.gnome.Nautilus.desktop override (mostly a stale translation snapshot drifting against upstream) and move the single real customisation — `Name=Files` → `Name=Nautilus` in the C locale — into 07-config.sh as a sed step, mirroring the existing Cider / Winetricks patches. Localised Name[xx]= entries are deliberately left alone, matching the prior override. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build-disk.yml | 2 +- build_files/07-config.sh | 19 ++ .../etc/profile.d/distinction-functions.sh | 14 +- .../user/distinctionos-steam-linker.service | 2 +- .../applications/org.gnome.Nautilus.desktop | 318 ------------------ 5 files changed, 22 insertions(+), 333 deletions(-) delete mode 100644 system_files/usr/share/applications/org.gnome.Nautilus.desktop diff --git a/.github/workflows/build-disk.yml b/.github/workflows/build-disk.yml index a7b55df..be2de59 100644 --- a/.github/workflows/build-disk.yml +++ b/.github/workflows/build-disk.yml @@ -26,7 +26,7 @@ env: IMAGE_NAME: ${{ github.event.repository.name }} # output of build.yml, keep in sync IMAGE_REGISTRY: "ghcr.io/${{ github.repository_owner }}" # do not edit DEFAULT_TAG: "latest" - BIB_IMAGE: "ghcr.io/lorbuschris/bootc-image-builder:20250608" # "quay.io/centos-bootc/bootc-image-builder:latest" - see https://github.com/osbuild/bootc-image-builder/pull/954 + BIB_IMAGE: "quay.io/centos-bootc/bootc-image-builder:latest" # was pinned to lorbuschris fork while osbuild#954 was open; merged 2025-06-13 concurrency: group: ${{ github.workflow }}-${{ github.ref || github.run_id }} diff --git a/build_files/07-config.sh b/build_files/07-config.sh index 0b6b906..596d2a1 100755 --- a/build_files/07-config.sh +++ b/build_files/07-config.sh @@ -110,6 +110,25 @@ else log_info "Cider not installed, skipping icon fix" fi +# ────────────────────────────────────────────────────────────────────────── +# Nautilus Display Name +# ────────────────────────────────────────────────────────────────────────── +# Preference: show the file manager as "Nautilus" rather than the upstream +# "Files" in en-US locales. Patching the shipped .desktop here means we +# don't have to maintain a 320-line full-file override under system_files/ +# that drifts against upstream translations on every nautilus release. +# Localised Name[xx]= entries are left alone (they remain the localised +# word for "Files"), matching the prior override's behaviour. + +readonly NAUTILUS_DESKTOP="/usr/share/applications/org.gnome.Nautilus.desktop" +if [[ -f "$NAUTILUS_DESKTOP" ]]; then + log_info "Renaming Nautilus to 'Nautilus' (C-locale)" + sed -i 's/^Name=Files$/Name=Nautilus/' "$NAUTILUS_DESKTOP" + log_success "Nautilus display name updated" +else + log_info "Nautilus not installed, skipping display-name override" +fi + # ────────────────────────────────────────────────────────────────────────── # Winetricks Debug Output Suppression # ────────────────────────────────────────────────────────────────────────── diff --git a/system_files/etc/profile.d/distinction-functions.sh b/system_files/etc/profile.d/distinction-functions.sh index 4649093..73f17cd 100644 --- a/system_files/etc/profile.d/distinction-functions.sh +++ b/system_files/etc/profile.d/distinction-functions.sh @@ -13,19 +13,7 @@ mkcd() { if mkdir -p "$1"; then cd "$1" || return 1 - - # Shell-specific colour handling - case "$SHELL_TYPE" in - zsh) - print -P "%F{cyan}✓%f Entered: %F{cyan}$1%f" - ;; - bash) - echo -e "\033[36m\033[0m Entered: \033[36m$1\033[0m" - ;; - *) - printf "\033[36m\033[0m Entered: \033[36m%s\033[0m\n" "$1" - ;; - esac + printf "\033[36m\033[0m Entered: \033[36m%s\033[0m\n" "$1" else echo "Failed to create directory: $1" >&2 return 1 diff --git a/system_files/usr/lib/systemd/user/distinctionos-steam-linker.service b/system_files/usr/lib/systemd/user/distinctionos-steam-linker.service index fc9d5e5..a92b722 100644 --- a/system_files/usr/lib/systemd/user/distinctionos-steam-linker.service +++ b/system_files/usr/lib/systemd/user/distinctionos-steam-linker.service @@ -1,6 +1,6 @@ [Unit] Description=DistinctionOS Steam Library Symlinker -Documentation=https://github.com/yourusername/DistinctionOS +Documentation=https://github.com/phantomcortex/DistinctionOS After=default.target # Only run if Steam appears to be installed diff --git a/system_files/usr/share/applications/org.gnome.Nautilus.desktop b/system_files/usr/share/applications/org.gnome.Nautilus.desktop deleted file mode 100644 index f1768a7..0000000 --- a/system_files/usr/share/applications/org.gnome.Nautilus.desktop +++ /dev/null @@ -1,318 +0,0 @@ -[Desktop Entry] -Name[af]=Lêers -Name[an]=Fichers -Name[ar]=الملفات -Name[as]=ফাইলসমূহ -Name[ast]=Ficheros -Name[be]=Файлы -Name[bg]=Файлове -Name[bn]=ফাইল -Name[bn_IN]=ফাইল -Name[bs]=Datoteke -Name[ca]=Fitxers -Name[ca@valencia]=Fitxers -Name[ckb]=پەڕگەکان -Name[crh]=Dosyeler -Name[cs]=Soubory -Name[da]=Filer -Name[de]=Dateien -Name[el]=Αρχεία -Name[en_CA]=Files -Name[en_GB]=Files -Name[eo]=Dosieroj -Name[es]=Archivos -Name[et]=Failid -Name[eu]=Fitxategiak -Name[fa]=پرونده‌ها -Name[fi]=Tiedostot -Name[fo]=Fílur -Name[fr]=Fichiers -Name[fur]=Files -Name[ga]=Comhaid -Name[gd]=Faidhlichean -Name[gl]=Ficheiros -Name[gu]=ફાઇલો -Name[he]=קבצים -Name[hi]=फाइल्स -Name[hr]=Datoteke -Name[hu]=Fájlok -Name[ia]=Files -Name[id]=Berkas -Name[ie]=Files -Name[is]=Skrár -Name[it]=File -Name[ja]=ファイル -Name[ka]=ფაილები -Name[kab]=Ifuyla -Name[kk]=Файлдар -Name[kn]=ಕಡತಗಳು -Name[ko]=파일 -Name[ky]=Файлдар -Name[ln]=Ba Fisyé -Name[lt]=Failai -Name[lv]=Datnes -Name[mjw]=Files -Name[mk]=Датотеки -Name[ml]=ഫയലുകള്‍ -Name[mr]=फाइल्स् -Name[ms]=Fail -Name[my]=ဖိုင် -Name[nb]=Filer -Name[ne]=फाइलहरू -Name[nl]=Bestanden -Name[nn]=Filer -Name[oc]=Fichièrs -Name[or]=ଫାଇଲଗୁଡିକ -Name[pa]=ਫ਼ਾਇਲਾਂ -Name[pl]=Pliki -Name[pt]=Ficheiros -Name[pt_BR]=Arquivos -Name[ro]=Fișiere -Name[ru]=Файлы -Name[sk]=Súbory -Name[sl]=Datoteke -Name[sr]=Датотеке -Name[sr@latin]=Datoteke -Name[sv]=Filer -Name[ta]=கோப்புகள் -Name[te]=దస్త్రాలు -Name[tg]=Файлҳо -Name[th]=ไฟล์ -Name[tr]=Dosyalar -Name[ug]=ھۆججەت -Name[uk]=Файли -Name[uz]=Fayllar -Name[vi]=Tập tin -Name[zh_CN]=文件 -Name[zh_HK]=檔案 -Name[zh_TW]=檔案 -Name=Nautilus -Comment[af]=Besoek/Deurblaai en organiseer lêers -Comment[an]=Accedir a os fichers y organizar-los -Comment[ar]=نظم الملفات وصِل إليها -Comment[as]=অভিগম কৰক আৰু ফাইলসমূহ আয়োজিত কৰক -Comment[ast]=Acceder a los ficheros y organizalos -Comment[be]=Доступ і кіраванне файламі -Comment[bg]=Достъп и управление на файлове -Comment[bn]=ফাইলে ব্যবাহর এবং সাজানো -Comment[bn_IN]=ফাইলগুলি অ্যাক্সেস এবং সংগঠিত করুন -Comment[bs]=Pristupite i organizujte datoteke -Comment[ca]=Organitzeu i accediu a fitxers -Comment[ca@valencia]=Organitzeu i accediu a fitxers -Comment[ckb]=چونەناو و ڕێکخستن -Comment[crh]=Dosyelerge iriş ve olarnı tertiple -Comment[cs]=Přístup k souborům a jejich správa -Comment[da]=Tilgå og organisér filer -Comment[de]=Auf Dateien zugreifen und diese organisieren -Comment[el]=Προσπελάστε και οργανώστε αρχεία -Comment[en_CA]=Access and organize files -Comment[en_GB]=Access and organise files -Comment[eo]=Atingi kaj organizi dosierojn -Comment[es]=Acceder a archivos y organizarlos -Comment[et]=Ligipääs failidele ning failipuu korrastamine -Comment[eu]=Atzitu eta antolatu fitxategiak -Comment[fa]=دسترسی و سازماندهی پرونده‌ها -Comment[fi]=Käsittele ja järjestä tiedostoja -Comment[fo]=Far til og skipa fílur -Comment[fr]=Accéder aux fichiers et les organiser -Comment[fur]=Dopre e organize i files -Comment[ga]=Déan rochtain ar chomhaid agus eagraigh iad -Comment[gd]=Faigh cothrom air faidhlichean is rianaich iad -Comment[gl]=Acceda e organice os seus ficheiros -Comment[gu]=ફાઇલોને વાપરો અને સંચાલિત કરો -Comment[he]=גישה לקבצים וארגונם -Comment[hi]=फाइलों तक पहुंचें और व्यवस्थित करें -Comment[hr]=Pristupite datotekama i organizirajte ih -Comment[hu]=Fájlok elérése és rendszerezése -Comment[ia]=Accede e organisa files -Comment[id]=Mengakses dan mengelola berkas -Comment[ie]=Accesse e ordina files -Comment[is]=Aðgangur og skipulag skráa -Comment[it]=Accede ai file e li organizza -Comment[ja]=ファイルにアクセスして整理 -Comment[ka]=ფაილებზე წვდომა და დალაგება -Comment[kab]=Kcem udiɣ suddes ifuyla -Comment[kk]=Файлдарға қатынау және оларды реттеу -Comment[kn]=ಕಡತಗಳನ್ನು ನಿಲುಕಿಸಿಕೊಳ್ಳಿ ಹಾಗು ವ್ಯವಸ್ಥಿತವಾಗಿ ಜೋಡಿಸಿ -Comment[ko]=파일 조작 및 정리 -Comment[ln]=Koyíngela mpe kobɔngisa ya kásá -Comment[lt]=Atverti ir tvarkyti failus -Comment[lv]=Piekļūt un organizēt datnes -Comment[mk]=Пристапувајте и организирајте датотеки -Comment[ml]=ഫയലുകള്‍ ലഭ്യമാക്കി ക്രമത്തിലാക്കുക -Comment[mr]=फाइल्स्ला प्रवेश द्वया व संघटित करा -Comment[ms]=Capai dan urus fail -Comment[my]=ဖိုင်များစီမံရန် -Comment[nb]=Finn og organiser filer -Comment[ne]=फाइलहरूको पहुँच र संगठन -Comment[nl]=Gebruik en organiseer bestanden -Comment[nn]=Aksesser og organiser filer -Comment[oc]=Accedir als fichièrs e los organizar -Comment[or]=ଫାଇଲମାନଙ୍କୁ ଅଭିଗମ କରନ୍ତୁ ଏବଂ ସଙ୍ଗଠନ କରନ୍ତୁ -Comment[pa]=ਫ਼ਾਇਲਾਂ ਲਈ ਪਹੁੰਚ ਅਤੇ ਇੰਤਜ਼ਾਮ -Comment[pl]=Otwieranie i organizowanie plików -Comment[pt]=Aceder e organizar ficheiros -Comment[pt_BR]=Acesse e organize arquivos -Comment[ro]=Accesează și organizează fișiere -Comment[ru]=Управление файлами -Comment[sk]=Pristupuje k súborom a organizuje ich -Comment[sl]=Dostop in razvrščanje datotek -Comment[sr]=Приступите датотекама и организујте их -Comment[sr@latin]=Pristupite datotekama i organizujte ih -Comment[sv]=Kom åt och organisera filer -Comment[ta]=கோப்புகளை அணுகு மற்றும் ஒழுங்கு படுத்து -Comment[te]=దస్త్రాలను నిర్వహించండి మరియు ప్రాప్తించండి -Comment[tg]=Кушодан ва мураттабсозии файлҳо -Comment[th]=เข้าถึงและจัดระเบียบไฟล์ -Comment[tr]=Dosyalara erişin ve düzenleyin -Comment[ug]=ھۆججەتلەرنى تەشكىللەش ۋە زىيارەت -Comment[uk]=Доступ до файлів -Comment[uz]=Fayllarga kirish va tartibga solish -Comment[vi]=Truy cập và tổ chức tập tin -Comment[zh_CN]=访问和组织文件 -Comment[zh_HK]=存取與組織檔案 -Comment[zh_TW]=存取與管理檔案 -Comment=Access and organize files -# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! -Keywords[ar]=مجلد;مدير;ملفات;قرص;تصفح;نطام الملفات;نوتلس; -Keywords[be]=folder;manager;explore;disk;filesystem;nautilus;папка;тэчка;праваднік;кіраўнік;менеджар;агляд:прагляд;дыск;файлавая сістэма;наўтылус; -Keywords[bg]=папка;файл;навигация;директория;система;диск;устройство;управление;мениджър;наутилус;folder;manager;explore;disk;filesystem;directory;file;nautilus; -Keywords[bn_IN]=ফোল্ডার;পরিচালক;এক্সপ্লোর;ডিস্ক;ফাইলসিস্টেম;নটিলাস -Keywords[ca]=carpeta;gestor;explora;disc;sistema de fitxers;nautilus; -Keywords[cs]=složka;správce;správa;prohlížení;procházení;disk;souborový systém;systém souborů;nautilus; -Keywords[da]=mappe;håndtering;udforsk;gennemse;disk;filsystem;nautilus; -Keywords[de]=Ordner;Verwaltung;Laufwerk;Festplatte;Dateisystem;Dateien;Dateiverwaltung;nautilus; -Keywords[el]=φάκελος;διαχειριστής;εξερεύνηση;δίσκος;σύστημα αρχείων;nautilus;folder;manager;explore;disk;filesystem;nautilus; -Keywords[en_GB]=folder;manager;explore;disk;filesystem;nautilus; -Keywords[eo]=dosierujo;administrilo;esplori;disko;dosiersistemo;naŭtilo; -Keywords[es]=carpeta;gestor;explorar;disco;sistema de archivos;nautilus; -Keywords[eu]=karpeta;kudeatzailea;arakatu;diskoa;fitxategi-sistema;nautilus; -Keywords[fa]=folder;manager;explore;disk;filesystem;nautilus;پرونده;شاخه;پوشه;مدیر;کشف;دیسک;سامانه‌پرونده;ناتیلوس; -Keywords[fi]=folder;manager;explore;disk;filesystem;nautilus;kansio;hakemisto;tiedostonhallinta;tiedostoselain;levy;tiedostojärjestelmä; -Keywords[fo]=skjátta;mappa;viðger;handfar;kaga;kanna;diskil;fíluskipan;nautilus; -Keywords[fr]=dossier;gestionnaire;explorer;disque;système de fichiers;nautilus; -Keywords[fur]=cartele;gjestôr;esplore;disc;filesystem;nautilus; -Keywords[gl]=cartafol;xestor;explorar;disco;sistema de ficheiros;nautilus; -Keywords[he]=תיקייה;מנהל;עיון;סיור;כונן;מערכת קבצים;נאוטילוס; -Keywords[hi]=फोल्डर;प्रबंधक;अन्वेषण करें;डिस्क;फाइलप्रणाली;नॉटिलस; -Keywords[hr]=mapa;upravitelj;istraži;disk;datotečni sustav;nautilus; -Keywords[hu]=mappa;könyvtár;kezelés;kezelő;intéző;lemez;fájlrendszer;commander;nautilus; -Keywords[id]=folder;pengelola;jelajahi;diska;sistem berkas;nautilus; -Keywords[ie]=fólder;gerente;explorar;navigar;disco;unité;file;sistema;nautilus; -Keywords[is]=mappa;stjórnun;skoða;diskur;skráakerfi;nautilus; -Keywords[it]=cartella;gestore;esplora;disco;file system;nautilus; -Keywords[ja]=folder;manager;explore;disk;filesystem;nautilus;フォルダー;マネージャー;エクスプローラー;ディスク;ファイルシステム;ノーチラス; -Keywords[ka]=folder;manager;explore;disk;filesystem;nautilus; -Keywords[kab]=akaram;aḍebsi;anagraw n yifuyla;ifuyla;nautilus -Keywords[kk]=folder;manager;explore;disk;filesystem;nautilus;бума;басқарушы;шолу;диск;файлдық жүйе; -Keywords[ko]=folder;폴더;manager;관리;explore;찾아보기;disk;디스크;filesystem;파일;시스템;nautilus;노틸러스; -Keywords[lt]=aplankas;tvarkytuvė;naršyti;diskas;failų sistema;nautilus; -Keywords[lv]=mape;pārvaldnieks;pārlūkot;disks;datņu sistēma;datne;nautilus; -Keywords[my]=ဖိုင်တွဲ;မန်နေဂျာ;ရှာဖွေ;သိမ်းဆည်း;နေရာ;နောတီလပ်;folder;manager;explore;disk;filesystem;nautilus; -Keywords[nb]=filer;filutforsker;filbehandler;filsystem;filhåndtering;utforsk;mapper;nautilus; -Keywords[ne]=फोल्डर;प्रबन्धक;अन्वेषण;डिस्क;फाइल प्रणाली;नटलस; -Keywords[nl]=folder;manager;explore;disk;filesystem;nautilus;map;beheer;verkenner;schijf;bestandssysteem; -Keywords[oc]=dorsièr;gestionari;explorar;disc;sistèma de fichièrs; -Keywords[pa]=ਫੋਲਡਰ;ਮੈਨੇਜਰ;ਛਾਣਬੀਣ;ਡਿਸਕ;ਫ਼ਾਇਲ-ਸਿਸਟਮ;ਨਟੀਲਸ; -Keywords[pl]=katalog;folder;menedżer;menadżer;manadżer;manedżer;manager;eksploruj;eksplorator;dysk;system plików;nautilus; -Keywords[pt]=pasta;gestor;explorar;disco;sistema de ficheiros;ficheiros;nautilus; -Keywords[pt_BR]=pasta;gerenciador;explorar;disco;sistema de arquivos;nautilus; -Keywords[ro]=folder;manager;explore;disk;filesystem;nautilus;dosar;administrator;explorează;disc;sistem de fișiere; -Keywords[ru]=папка;менеджер;обзор;диск;файловая система;nautilus; -Keywords[sk]=priečinok;správca;prehliadať;disk;systém súborov;nautilus; -Keywords[sl]=mapa;upravljalnik;datoteke;raziskovalec;datotečni sistem;disk;iskanje;urejanje;nautilus;file;folder; -Keywords[sr]=фасцикла;фолдер;управник;менаџер;истражи;диск;систем датотека;фајлсистем;наутилус;fascikla;folder;upravnik;menadžer;menadzer;istraži;istrazi;sistem datoteka;fajlsistem;folder;manager;explore;disk;filesystem;nautilus; -Keywords[sr@latin]=fascikla;folder;upravnik;menadžer;istraži;disk;sistem datoteka;fajlsistem;nautilus;fascikla;folder;upravnik;menadžer;menadzer;istraži;istrazi;sistem datoteka;fajlsistem;folder;manager;explore;disk;filesystem;nautilus; -Keywords[sv]=mapp;hanterare;utforskare;disk;filsystem;nautilus; -Keywords[th]=โฟลเดอร์;เครื่องมือจัดการ;สำรวจ;ดิสก์;ระบบไฟล์;nautilus; -Keywords[tr]=folder;manager;explore;disk;filesystem;nautilus;klasör;dizin;yönetici;gezgin;keşfet;keşif;gözat;göz at;disk;dosya sistemi; -Keywords[ug]=folder;manager;explore;disk;filesystem;nautilus;قىسقۇچ؛باشقۇرغۇچ؛كۆز يۈگۈرتۈش؛دىسكا؛ھۆججەت سىستېمىسى؛ھۆججەت باشقۇرغۇچ؛ھۆججەت سىستېمىسى؛ -Keywords[uk]=тека;менеджер;папка;диск;файл;керування;folder;manager;explore;disk;filesystem;nautilus;наутилус; -Keywords[uz]=papka;menejer;tadqiq qilish;disk;fayl tizimi;nautilus; -Keywords[vi]=folder;thư;mục;thu;muc;manager;quản;lý;quan;ly;explore;khám;phá;kham;pha;disk;đĩa;dia;filesystem;hệ;thống;tập;tin;he;thong;tap;nautilus; -Keywords[zh_CN]=folder;manager;explore;disk;filesystem;目录;文件夹;管理器;浏览;磁盘;硬盘;文件系统; -Keywords[zh_TW]=folder;manager;explore;disk;filesystem;nautilus;資料夾;管理員;磁碟;檔案系統;檔案總管;文件;管理器; -Keywords=folder;manager;explore;disk;filesystem;nautilus; -Exec=nautilus --new-window %U -# Translators: Do NOT translate or transliterate this text (this is an icon file name)! -Icon=org.gnome.Nautilus -Terminal=false -Type=Application -DBusActivatable=true -StartupNotify=true -Categories=GNOME;GTK;Utility;Core;FileManager; -MimeType=inode/directory;application/x-7z-compressed;application/x-7z-compressed-tar;application/x-bzip;application/x-bzip-compressed-tar;application/x-compress;application/x-compressed-tar;application/x-cpio;application/x-gzip;application/x-lha;application/x-lzip;application/x-lzip-compressed-tar;application/x-lzma;application/x-lzma-compressed-tar;application/x-tar;application/x-tarz;application/x-xar;application/x-xz;application/x-xz-compressed-tar;application/zip;application/gzip;application/bzip2;application/x-bzip2-compressed-tar;application/vnd.rar;application/zstd;application/x-zstd-compressed-tar -X-GNOME-UsesNotifications=true -Actions=new-window; -X-Purism-FormFactor=Workstation;Mobile; - -[Desktop Action new-window] -Name[af]=Nuwe venster -Name[ar]=نافذة جديدة -Name[be]=Новае акно -Name[bg]=Нов прозорец -Name[bn_IN]=নতুন উইন্ডো -Name[ca]=Finestra nova -Name[ca@valencia]=Finestra nova -Name[ckb]=پەنجەریەکی نوێ -Name[cs]=Nové okno -Name[da]=Nyt vindue -Name[de]=Neues Fenster -Name[el]=Νέο παράθυρο -Name[en_GB]=New Window -Name[eo]=Nova fenestro -Name[es]=Ventana nueva -Name[eu]=Leiho berria -Name[fa]=پنجرهٔ جدید -Name[fi]=Uusi ikkuna -Name[fo]=Nýggjan glugga -Name[fr]=Nouvelle fenêtre -Name[fur]=Gnûf barcon -Name[gd]=Uinneag ùr -Name[gl]=Nova xanela -Name[he]=חלון חדש -Name[hi]=नई विंडो -Name[hr]=Novi prozor -Name[hu]=Új ablak -Name[ia]=Nove fenestra -Name[id]=Jendela Baru -Name[ie]=Nov fenestre -Name[is]=Nýr gluggi -Name[it]=Nuova finestra -Name[ja]=新しいウィンドウ -Name[ka]=ახალი ფანჯარა -Name[kab]=Asfaylu amaynut -Name[kk]=Жаңа терезе -Name[ko]=새 창 -Name[ln]=Lininísa ya sika -Name[lt]=Naujas langas -Name[lv]=Jauns logs -Name[mjw]=Kimi Window -Name[ml]=പുതിയ ജാലകം -Name[ms]=Tetingkap Baharu -Name[my]=ဝင်းဒိုးအသစ် -Name[nb]=Nytt vindu -Name[ne]=नयाँ सञ्झ्याल -Name[nl]=Nieuw venster -Name[oc]=Fenèstra novèla -Name[pa]=ਨਵੀਂ ਵਿੰਡੋ -Name[pl]=Nowe okno -Name[pt]=Nova janela -Name[pt_BR]=Nova janela -Name[ro]=Fereastră nouă -Name[ru]=Создать окно -Name[sk]=Nové okno -Name[sl]=Novo okno -Name[sr]=Нови прозор -Name[sr@latin]=Novi prozor -Name[sv]=Nytt fönster -Name[th]=หน้าต่างใหม่ -Name[tr]=Yeni Pencere -Name[ug]=يېڭى كۆزنەك -Name[uk]=Нове вікно -Name[uz]=Yangi oyna -Name[vi]=Cửa sổ mới -Name[zh_CN]=新建窗口 -Name[zh_TW]=新視窗 -Name=New Window -Exec=nautilus --new-window From 288ad120c84903dc34fc76bff521c846f8e329cc Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:49:58 -0600 Subject: [PATCH 12/13] Make distinction-functions.sh bash-and-zsh safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The file is sourced from /etc/profile (bash login) and /etc/zsh/zshrc (every zsh interactive shell), so it has to work in both. The shebang claimed `#!/bin/sh` but the body has always used `[[ ]]`, `local`, `=~`, and `disown` — none POSIX. The actual runtime break was rm(): it used zsh-only `read -q "REPLY?"` to read a single keystroke into REPLY. Under bash, `read -q` is not a real flag, so the prompt-and-confirm path errored out. - Shebang: `/bin/sh ` (trailing space) → `/usr/bin/bash`. Adds a header comment naming the two sourcing paths and noting the bash/zsh tolerance is intentional. - rm(): switch to plain `read -r REPLY` (press Enter). Loses the single-keystroke convenience zsh got from `read -q`, but the rest of the confirm logic (`[[ "$REPLY" =~ ^[Yy]$ ]]`) keeps the same semantics and now works in both shells. Drops the now-unneeded trailing `echo` since the user already pressed Enter. Verified by sourcing the file under `bash -c` and `zsh -c`, listing the five defined functions, and running rm() on a fixture directory with both `n` (kept) and `y` (deleted) input. shellcheck -S error clean. Co-Authored-By: Claude Opus 4.7 --- system_files/etc/profile.d/distinction-functions.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/system_files/etc/profile.d/distinction-functions.sh b/system_files/etc/profile.d/distinction-functions.sh index 73f17cd..6f85e6e 100644 --- a/system_files/etc/profile.d/distinction-functions.sh +++ b/system_files/etc/profile.d/distinction-functions.sh @@ -1,4 +1,7 @@ -#!/bin/sh +#!/usr/bin/bash +# Sourced from /etc/profile (bash login) and /etc/zsh/zshrc (every zsh +# interactive shell). Never executed under pure sh — bash/zsh-only +# syntax (`[[ ]]`, `local`, `=~`, `disown`) is intentional. alias ls="eza --icons=always --classify=always --mounts" alias lf="eza --icons=always --classify=always --long --almost-all --sort=size --git" alias lfe="eza --icons=always --classify=always --long --almost-all --sort=size --git --total-size --show-symlinks" @@ -61,9 +64,8 @@ rm() { local NC="\033[0m" echo -e "This is a directory containing $CYAN$file_count$NC files." echo -n "Are you quite certain you wish to delete it? [y/N] " - read -q "REPLY?" - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then + read -r REPLY + if [[ "$REPLY" =~ ^[Yy]$ ]]; then command rm -rf "$@" fi else From 584658ac8c7b197eb45a4e13cb1d47bccd617364 Mon Sep 17 00:00:00 2001 From: phantomcortex <137958098+phantomcortex@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:52:36 -0600 Subject: [PATCH 13/13] remove redunant terra repo commands --- build_files/02-build.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build_files/02-build.sh b/build_files/02-build.sh index 227f29c..fee4f91 100755 --- a/build_files/02-build.sh +++ b/build_files/02-build.sh @@ -59,10 +59,6 @@ github_api_get() { "${auth[@]}" "$url" } -log_info "installing terra" -dnf5 -y install --nogpgcheck --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' terra-release -dnf5 -y install terra-release-extras -dnf5 -y install terra-release-mesa dnf5 versionlock delete mesa # ============================================================================ # Package Installation Tracking