From 813816af8b669af170d8ca5660ab2515abeffcc9 Mon Sep 17 00:00:00 2001 From: repires Date: Wed, 23 Sep 2026 17:40:05 +0000 Subject: [PATCH 1/6] feat(wallpaper): add Damask first-run setup hook and non-destructive defaults Configure first-run user setup hook for Damask (app.drey.Damask) to pre-seed GLib keyfile settings non-destructively: - Set refresh-interval to 86400 (24h), automatic refresh enabled, and run in background. - Set active-source='none' to avoid overwriting GNOME's native timed day/night slideshow until explicitly enabled by user. - Pre-seed slideshow plugin folder URI to /usr/share/backgrounds/bluefin and random sort order. - Enable user unit damask.service via systemctl --user enable. - Guard with version-script damask-setup user 1 and preserve existing keyfile if present. - Add Bats unit test suite and register in Justfile and CI workflow. - Update dconf-consistency skill doc with Flatpak keyfile defaults guidance. Closes projectbluefin/common#1161 Hive-Run: projectbluefin/common#1161 Hive-Plan: damask-first-run-setup Hive-Spec: wallpaper-enhancements#1161 Assisted-by: Gemini via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: repires --- .github/workflows/unit-tests.yml | 3 + Justfile | 1 + docs/skills/dconf-consistency.md | 14 +- docs/skills/index.json | 4 +- .../user-setup.hooks.d/25-damask-setup.sh | 29 ++++ tests/test_damask_setup.bats | 125 ++++++++++++++++++ 6 files changed, 172 insertions(+), 4 deletions(-) create mode 100755 system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh create mode 100644 tests/test_damask_setup.bats diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7c5766b95..fc5f31fe6 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -131,6 +131,9 @@ jobs: - name: Run bats (oem-brew hook) run: bats tests/test_oem_brew.bats + - name: Run bats (damask-setup hook) + run: bats tests/test_damask_setup.bats + - name: Run bats (bonedigger report) run: bats tests/test_bonedigger_report.bats diff --git a/Justfile b/Justfile index 68cf9a6dc..082b4907d 100644 --- a/Justfile +++ b/Justfile @@ -36,6 +36,7 @@ test: bats tests/test_hardware_hooks.bats bats tests/test_theming_hook.bats bats tests/test_dynamic_wallpaper_hook.bats + bats tests/test_damask_setup.bats bats tests/test_nvidia_flatpak_sync.bats bats tests/test_system_just.bats bats tests/test_brew_tap_trust.bats diff --git a/docs/skills/dconf-consistency.md b/docs/skills/dconf-consistency.md index ee2fdf079..c24d11498 100644 --- a/docs/skills/dconf-consistency.md +++ b/docs/skills/dconf-consistency.md @@ -1,7 +1,7 @@ --- name: dconf-consistency -version: "1.0" -last_updated: "2026-06-23" +version: "1.1" +last_updated: "2026-09-23" id: dconf-consistency one_line_purpose: Keep GSettings overrides and dconf lock files in parity. entry_point: docs/skills/dconf-consistency.md @@ -126,3 +126,13 @@ This means any `gsettings get org.gnome.shell enabled-extensions` call in a test `enabled-extensions` for `org.gnome.shell` is set in `system_files/bluefin/usr/share/glib-2.0/schemas/zz0-bluefin-modifications.gschema.override` — this sets the schema DEFAULT. It is NOT in `distro.d/` and is not a locked key, so users can override it. The CI's `local.d/00-ci-testing` write overrides it in every test VM. Tests must use `get_default_value()` to validate this config, not `gsettings get`. + +## Flatpak application defaults via GKeyfileSettingsBackend + +Flatpak applications run sandboxed and typically use `GKeyfileSettingsBackend` rather than the host system's dconf database. + +Key differences from host GSettings: +- Config is stored in a keyfile at `~/.var/app//config/glib-2.0/settings/keyfile`. +- Schema separation: root application preferences are separated from plugin sources (e.g., `[app/drey/Damask]` vs `[app/drey/Damask/sources/slideshow]`). +- Pre-seeding defaults cannot be done via `/etc/dconf/db` or `gschema.override`. Instead, pre-seed defaults in `system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/` using the `version-script` contract. +- Non-destructive activation: check `[[ ! -f "${KEYFILE}" ]]` before writing so existing user configurations are never overwritten, and set inactive or dormant defaults (e.g., `active-source='none'`) when activating by default would override host user desktop settings or timed wallpaper slideshows. diff --git a/docs/skills/index.json b/docs/skills/index.json index 879b511b4..f6a940d47 100644 --- a/docs/skills/index.json +++ b/docs/skills/index.json @@ -154,8 +154,8 @@ "configuration" ], "description": "GSettings override and dconf lock file parity rules \u2014 must edit both files together for locked settings. Use when changing any GSettings key, dconf override, or lock file in system_files/.", - "version": "1.0", - "last_updated": "2026-06-23", + "version": "1.1", + "last_updated": "2026-09-23", "doc_type": "reference" }, { diff --git a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh new file mode 100755 index 000000000..d83d8bc10 --- /dev/null +++ b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# shellcheck disable=SC1091 +source /usr/lib/ublue/setup-services/libsetup.sh + +version-script damask-setup user 1 || exit 0 + +SETTINGS_DIR="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings" +KEYFILE="${SETTINGS_DIR}/keyfile" + +mkdir -p "${SETTINGS_DIR}" + +if [[ ! -f "${KEYFILE}" ]]; then + cat > "${KEYFILE}" << 'EOF' +[app/drey/Damask] +refresh-interval='86400' +enable-automatic-refresh=true +run-in-background=true +active-source='none' + +[app/drey/Damask/sources/slideshow] +folder-uri='file:///usr/share/backgrounds/bluefin' +sort-by='random' +EOF + chmod 0644 "${KEYFILE}" +fi + +systemctl --user enable damask.service diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats new file mode 100644 index 000000000..56bb6fbe7 --- /dev/null +++ b/tests/test_damask_setup.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats +# Tests for system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh +# +# Run: bats tests/test_damask_setup.bats + +REPO_ROOT="$BATS_TEST_DIRNAME/.." +DAMASK_HOOK="${REPO_ROOT}/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh" +LIBSETUP_REAL="${REPO_ROOT}/system_files/shared/usr/lib/ublue/setup-services/libsetup.sh" + +WORKDIR="" +PATCHED_HOOK="" + +setup() { + WORKDIR="$(mktemp -d)" + mkdir -p "${WORKDIR}/bin" "${WORKDIR}/home" + + # Exit codes the mocks below read, so a test can make dependencies fail. + echo 0 > "${WORKDIR}/systemctl.rc" + + cat > "${WORKDIR}/bin/systemctl" << MOCK +#!/bin/bash +echo "\$*" >> "${WORKDIR}/systemctl.log" +exit "\$(cat "${WORKDIR}/systemctl.rc")" +MOCK + chmod +x "${WORKDIR}/bin/systemctl" + + # Patch the absolute source path so hook runs against the real libsetup.sh in the repo. + PATCHED_HOOK="${WORKDIR}/25-damask-setup.sh" + sed \ + -e "s|source /usr/lib/ublue/setup-services/libsetup.sh|source ${LIBSETUP_REAL}|g" \ + "${DAMASK_HOOK}" > "${PATCHED_HOOK}" + chmod +x "${PATCHED_HOOK}" + + export PATH="${WORKDIR}/bin:${PATH}" + export HOME="${WORKDIR}/home" + unset XDG_CONFIG_HOME + export SETUP_CHECKER_FILE="${WORKDIR}/setup_versioning.json" +} + +teardown() { + rm -rf "${WORKDIR}" +} + +@test "25-damask-setup: first run creates keyfile with expected content and permissions" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + local keyfile="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings/keyfile" + [ -f "${keyfile}" ] + + # Verify file permissions are 0644 + local perms + perms="$(stat -c "%a" "${keyfile}")" + [ "${perms}" = "644" ] + + # Verify root preferences section + grep -qx "\[app/drey/Damask\]" "${keyfile}" + grep -qx "refresh-interval='86400'" "${keyfile}" + grep -qx "enable-automatic-refresh=true" "${keyfile}" + grep -qx "run-in-background=true" "${keyfile}" + grep -qx "active-source='none'" "${keyfile}" + + # Verify slideshow plugin source section + grep -qx "\[app/drey/Damask/sources/slideshow\]" "${keyfile}" + grep -qx "folder-uri='file:///usr/share/backgrounds/bluefin'" "${keyfile}" + grep -qx "sort-by='random'" "${keyfile}" +} + +@test "25-damask-setup: first run enables damask.service for current user" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + grep -qx -- "--user enable damask.service" "${WORKDIR}/systemctl.log" + ! grep -q -- "--system" "${WORKDIR}/systemctl.log" +} + +@test "25-damask-setup: records version 1 under user service namespace" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + [ "$(jq -r '.version.user."damask-setup"' "${SETUP_CHECKER_FILE}")" = "1" ] +} + +@test "25-damask-setup: version-script gate makes a second run a no-op" { + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + # Modify the keyfile to verify second run doesn't overwrite it + local keyfile="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings/keyfile" + echo "# user modified" >> "${keyfile}" + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + # systemctl should only have been called once + [ "$(wc -l < "${WORKDIR}/systemctl.log")" -eq 1 ] + + # user modification should still be present + grep -qx "# user modified" "${keyfile}" +} + +@test "25-damask-setup: non-destructive when keyfile already exists before first run" { + local keyfile_dir="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings" + mkdir -p "${keyfile_dir}" + cat > "${keyfile_dir}/keyfile" << 'EXISTING' +[app/drey/Damask] +active-source='wallhaven' +EXISTING + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + # Existing content preserved + grep -qx "active-source='wallhaven'" "${keyfile_dir}/keyfile" + ! grep -qx "active-source='none'" "${keyfile_dir}/keyfile" + + # systemctl was still called + grep -qx -- "--user enable damask.service" "${WORKDIR}/systemctl.log" +} + +@test "25-damask-setup: a failing systemctl causes the hook to exit with failure" { + echo 1 > "${WORKDIR}/systemctl.rc" + + run bash "${PATCHED_HOOK}" + [ "${status}" -ne 0 ] +} From 2a2a10d5f51f5abb930097885442d0104d67cb51 Mon Sep 17 00:00:00 2001 From: repires Date: Fri, 25 Sep 2026 00:25:55 +0000 Subject: [PATCH 2/6] fix(wallpaper): guard damask.service enablement with unit file existence check - Check `systemctl --user list-unit-files damask.service` before calling enable, ensuring the hook safely tolerates environments where damask.service has not yet been installed (such as during independent staging before #1187 merges). - Update Bats test suite with a test verifying graceful skip when the unit is absent. --- .../user-setup.hooks.d/25-damask-setup.sh | 4 +++- tests/test_damask_setup.bats | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh index d83d8bc10..86fee2eb8 100755 --- a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh +++ b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh @@ -26,4 +26,6 @@ EOF chmod 0644 "${KEYFILE}" fi -systemctl --user enable damask.service +if systemctl --user list-unit-files damask.service &>/dev/null; then + systemctl --user enable damask.service +fi diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats index 56bb6fbe7..3c0823c55 100644 --- a/tests/test_damask_setup.bats +++ b/tests/test_damask_setup.bats @@ -14,12 +14,15 @@ setup() { WORKDIR="$(mktemp -d)" mkdir -p "${WORKDIR}/bin" "${WORKDIR}/home" - # Exit codes the mocks below read, so a test can make dependencies fail. echo 0 > "${WORKDIR}/systemctl.rc" + echo 0 > "${WORKDIR}/systemctl-list.rc" cat > "${WORKDIR}/bin/systemctl" << MOCK #!/bin/bash echo "\$*" >> "${WORKDIR}/systemctl.log" +if [[ "\$*" == *"list-unit-files damask.service"* ]]; then + exit "\$(cat "${WORKDIR}/systemctl-list.rc" 2>/dev/null || echo 0)" +fi exit "\$(cat "${WORKDIR}/systemctl.rc")" MOCK chmod +x "${WORKDIR}/bin/systemctl" @@ -123,3 +126,14 @@ EXISTING run bash "${PATCHED_HOOK}" [ "${status}" -ne 0 ] } + +@test "25-damask-setup: gracefully skips enable if damask.service does not exist" { + echo 1 > "${WORKDIR}/systemctl-list.rc" + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + grep -q -- "list-unit-files damask.service" "${WORKDIR}/systemctl.log" + ! grep -q -- "--user enable damask.service" "${WORKDIR}/systemctl.log" +} + From 29f37a89df39dfed1a55609d515d1b95e416224a Mon Sep 17 00:00:00 2001 From: "sec-check[bot]" Date: Thu, 24 Sep 2026 23:00:24 -0400 Subject: [PATCH 3/6] test(damask): count enable calls, not every systemctl invocation The no-op assertion counted all lines in the systemctl mock log, but a single hook run logs two calls: the list-unit-files probe and the enable. The gate was already correct; the assertion could never pass. Count the enable and probe calls separately instead, and drop the trailing blank line that end-of-file-fixer rejected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: sec-check[bot] --- tests/test_damask_setup.bats | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats index 3c0823c55..41048d4ff 100644 --- a/tests/test_damask_setup.bats +++ b/tests/test_damask_setup.bats @@ -94,8 +94,11 @@ teardown() { run bash "${PATCHED_HOOK}" [ "${status}" -eq 0 ] - # systemctl should only have been called once - [ "$(wc -l < "${WORKDIR}/systemctl.log")" -eq 1 ] + # The hook body must not run again: exactly one enable, and no second + # unit-file probe. Counting every logged call would also count the + # list-unit-files probe the first run makes. + [ "$(grep -c -x -- "--user enable damask.service" "${WORKDIR}/systemctl.log")" -eq 1 ] + [ "$(grep -c -x -- "--user list-unit-files damask.service" "${WORKDIR}/systemctl.log")" -eq 1 ] # user modification should still be present grep -qx "# user modified" "${keyfile}" @@ -136,4 +139,3 @@ EXISTING grep -q -- "list-unit-files damask.service" "${WORKDIR}/systemctl.log" ! grep -q -- "--user enable damask.service" "${WORKDIR}/systemctl.log" } - From 2d992c47b222ee1477e917ac707292db8acdd579 Mon Sep 17 00:00:00 2001 From: castrojo Date: Fri, 25 Sep 2026 19:17:17 -0400 Subject: [PATCH 4/6] test(damask): wrap intermediate negative assertion with run (SC2314) --- tests/test_damask_setup.bats | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats index 41048d4ff..f3343098b 100644 --- a/tests/test_damask_setup.bats +++ b/tests/test_damask_setup.bats @@ -117,7 +117,8 @@ EXISTING # Existing content preserved grep -qx "active-source='wallhaven'" "${keyfile_dir}/keyfile" - ! grep -qx "active-source='none'" "${keyfile_dir}/keyfile" + run grep -qx "active-source='none'" "${keyfile_dir}/keyfile" + [ "${status}" -ne 0 ] # systemctl was still called grep -qx -- "--user enable damask.service" "${WORKDIR}/systemctl.log" From b346d5de83f2234de9b5ecf5c5ec950b12f6e946 Mon Sep 17 00:00:00 2001 From: castrojo Date: Fri, 25 Sep 2026 19:52:01 -0400 Subject: [PATCH 5/6] fix(wallpaper): point slideshow folder-uri to /run/host/usr path --- .../usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh | 2 +- tests/test_damask_setup.bats | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh index 86fee2eb8..2c6b4d726 100755 --- a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh +++ b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh @@ -20,7 +20,7 @@ run-in-background=true active-source='none' [app/drey/Damask/sources/slideshow] -folder-uri='file:///usr/share/backgrounds/bluefin' +folder-uri='file:///run/host/usr/share/backgrounds/bluefin' sort-by='random' EOF chmod 0644 "${KEYFILE}" diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats index f3343098b..0e7a586fa 100644 --- a/tests/test_damask_setup.bats +++ b/tests/test_damask_setup.bats @@ -65,7 +65,7 @@ teardown() { # Verify slideshow plugin source section grep -qx "\[app/drey/Damask/sources/slideshow\]" "${keyfile}" - grep -qx "folder-uri='file:///usr/share/backgrounds/bluefin'" "${keyfile}" + grep -qx "folder-uri='file:///run/host/usr/share/backgrounds/bluefin'" "${keyfile}" grep -qx "sort-by='random'" "${keyfile}" } From c7a291c402083948ba2954a0b64e0ba542ca0dab Mon Sep 17 00:00:00 2001 From: castrojo <1264109+castrojo@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:24:24 -0400 Subject: [PATCH 6/6] fix(damask): gate user-setup hook on Damask being installed Gate on flatpak info app.drey.Damask before the version-script call so users who install Damask later still receive default setup. --- .../user-setup.hooks.d/25-damask-setup.sh | 2 ++ tests/test_damask_setup.bats | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh index 2c6b4d726..33958633a 100755 --- a/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh +++ b/system_files/bluefin/usr/share/ublue-os/user-setup.hooks.d/25-damask-setup.sh @@ -4,6 +4,8 @@ set -euo pipefail # shellcheck disable=SC1091 source /usr/lib/ublue/setup-services/libsetup.sh +flatpak info app.drey.Damask &>/dev/null || exit 0 + version-script damask-setup user 1 || exit 0 SETTINGS_DIR="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings" diff --git a/tests/test_damask_setup.bats b/tests/test_damask_setup.bats index 0e7a586fa..ec5fd5611 100644 --- a/tests/test_damask_setup.bats +++ b/tests/test_damask_setup.bats @@ -26,6 +26,13 @@ fi exit "\$(cat "${WORKDIR}/systemctl.rc")" MOCK chmod +x "${WORKDIR}/bin/systemctl" + echo 0 > "${WORKDIR}/flatpak.rc" + cat > "${WORKDIR}/bin/flatpak" << MOCK +#!/bin/bash +echo "\$*" >> "${WORKDIR}/flatpak.log" +exit "\$(cat "${WORKDIR}/flatpak.rc")" +MOCK + chmod +x "${WORKDIR}/bin/flatpak" # Patch the absolute source path so hook runs against the real libsetup.sh in the repo. PATCHED_HOOK="${WORKDIR}/25-damask-setup.sh" @@ -140,3 +147,14 @@ EXISTING grep -q -- "list-unit-files damask.service" "${WORKDIR}/systemctl.log" ! grep -q -- "--user enable damask.service" "${WORKDIR}/systemctl.log" } + +@test "25-damask-setup: exits cleanly without creating keyfile or stamping when Damask is not installed" { + echo 1 > "${WORKDIR}/flatpak.rc" + + run bash "${PATCHED_HOOK}" + [ "${status}" -eq 0 ] + + local keyfile="${HOME}/.var/app/app.drey.Damask/config/glib-2.0/settings/keyfile" + [ ! -f "${keyfile}" ] + [ ! -f "${SETUP_CHECKER_FILE}" ] +}