aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xarchsetup37
-rw-r--r--tests/installer-steps/test_grub_cmdline.py42
-rw-r--r--tests/installer-steps/test_hyprlock_pam.py85
-rw-r--r--tests/installer-steps/test_orchestrators.py2
4 files changed, 166 insertions, 0 deletions
diff --git a/archsetup b/archsetup
index 98740f8..96ef231 100755
--- a/archsetup
+++ b/archsetup
@@ -2307,6 +2307,30 @@ wayland() {
}
### Hyprland Window Manager
+# Write a complete PAM stack for hyprlock. $1 is the file, defaulting to the
+# system path so the step runs against fixtures. The hyprlock package ships only
+# `auth include login`, leaving account and session uninitialised; pam_end()
+# then crashes on cleanup of handles that were never set up, one of the
+# documented hyprlock lockout causes (hyprlock#953). Completing the stack via
+# `login` inherits the keyring and session handling the graphical login already
+# uses. Written wholesale each run, so it's idempotent and repairs a reverted
+# package default on a re-install.
+configure_hyprlock_pam() {
+ local pam="${1:-/etc/pam.d/hyprlock}"
+ action="writing a complete PAM stack for hyprlock" && display "task" "$action"
+ backup_system_file "$pam"
+ cat > "$pam" << 'EOF' || error_warn "$action" "$?"
+#%PAM-1.0
+# hyprlock unlock. The packaged default is only `auth include login`, which
+# leaves the account and session phases uninitialised and crashes pam_end() on
+# cleanup -- a hyprlock lockout cause (hyprlock#953). All three phases via
+# `login` inherit the keyring and session handling the graphical login uses.
+auth include login
+account include login
+session include login
+EOF
+}
+
hyprland() {
action="Hyprland Compositor Dependencies" && display "subtitle" "$action"
for software in polkit-kde-agent qt5-wayland qt6-wayland; do
@@ -2317,6 +2341,7 @@ hyprland() {
pacman_install hyprland
pacman_install hypridle
pacman_install hyprlock
+ configure_hyprlock_pam
action="Hyprland Utilities" && display "subtitle" "$action"
aur_install pyprland # scratchpads, magnify, expose (fixes special workspace issues)
@@ -3528,6 +3553,18 @@ merge_grub_cmdline() {
update_grub_cmdline() {
local file="${1:-/etc/default/grub}"
local desired="rw loglevel=2 rd.systemd.show_status=auto rd.udev.log_level=2 nvme.noacpi=1 mem_sleep_default=deep nowatchdog random.trust_cpu=off quiet splash"
+
+ # AMD integrated Radeon invalidates the GPU resources a Wayland lock client
+ # (hyprlock) holds when the display power-cycles via DPMS, so the lock crashes
+ # and the compositor wedges locked with no prompt (hyprlock#953,
+ # Hyprland#5822; hit on this Strix Halo box 2026-07-24). Disabling GPU
+ # runtime power management keeps those resources valid across a display
+ # cycle. AMD only -- a no-op cost on a desktop iGPU, and it must not touch
+ # Intel/NVIDIA machines.
+ if printf '%s\n' "$(detect_gpu_vendors)" | grep -qx amd; then
+ desired="$desired amdgpu.runpm=0"
+ fi
+
local existing merged tok key
existing=$(sed -n 's/^[[:space:]]*GRUB_CMDLINE_LINUX_DEFAULT="\{0,1\}\([^"]*\)"\{0,1\}[[:space:]]*$/\1/p' "$file" | head -1)
merged=$(merge_grub_cmdline "$existing" "$desired")
diff --git a/tests/installer-steps/test_grub_cmdline.py b/tests/installer-steps/test_grub_cmdline.py
index 8c0b5b0..4c2d202 100644
--- a/tests/installer-steps/test_grub_cmdline.py
+++ b/tests/installer-steps/test_grub_cmdline.py
@@ -67,6 +67,48 @@ class MergeGrubCmdline(unittest.TestCase):
self.assertIn("zfs=zroot/ROOT/default", out)
+def run_with_gpu(body, gpu):
+ # detect_gpu_vendors is stubbed to a chosen vendor list so the AMD-only
+ # amdgpu.runpm=0 addition can be exercised without real /sys reads.
+ stub = 'detect_gpu_vendors() { printf "%s\\n" %s; }\n' % ("%s", "'" + gpu + "'")
+ stub = 'detect_gpu_vendors() { printf "%s" "$GPU"; [ -n "$GPU" ] && echo; }\n'
+ script = ('logfile=/dev/null\nerror_warn() { echo "WARN: $1"; return 1; }\n'
+ + stub + EXTRACT + "\n" + body + "\n")
+ return subprocess.run(["bash", "-c", script], capture_output=True, text=True,
+ timeout=10, env={**os.environ, "GPU": gpu})
+
+
+class AmdRunpmCmdline(unittest.TestCase):
+ """amdgpu.runpm=0 is added on AMD machines only: the AMD iGPU invalidates
+ hyprlock's GPU resources on a display power cycle, wedging the lock
+ (hyprlock#953). Disabling GPU runtime PM keeps them valid. A no-op on Intel
+ or NVIDIA."""
+
+ def _file(self, gpu):
+ import tempfile
+ fd, path = tempfile.mkstemp(prefix="grub-runpm-")
+ os.close(fd)
+ with open(path, "w") as f:
+ f.write('GRUB_CMDLINE_LINUX_DEFAULT="rw quiet"\n')
+ self.addCleanup(os.remove, path)
+ run_with_gpu(f'update_grub_cmdline "{path}"', gpu)
+ with open(path) as f:
+ return f.read()
+
+ def test_amd_gets_runpm_disabled(self):
+ self.assertIn("amdgpu.runpm=0", self._file("amd"))
+
+ def test_intel_does_not(self):
+ self.assertNotIn("amdgpu.runpm", self._file("intel"))
+
+ def test_no_gpu_detected_does_not(self):
+ self.assertNotIn("amdgpu.runpm", self._file(""))
+
+ def test_amd_with_nvidia_still_gets_it(self):
+ # A hybrid box with an AMD part present still wants the fix.
+ self.assertIn("amdgpu.runpm=0", self._file("amd\nnvidia"))
+
+
class UpdateGrubCmdline(unittest.TestCase):
def run_update(self, grub_body):
with tempfile.TemporaryDirectory() as d:
diff --git a/tests/installer-steps/test_hyprlock_pam.py b/tests/installer-steps/test_hyprlock_pam.py
new file mode 100644
index 0000000..8d7295f
--- /dev/null
+++ b/tests/installer-steps/test_hyprlock_pam.py
@@ -0,0 +1,85 @@
+"""Tests for configure_hyprlock_pam in the archsetup installer.
+
+hyprlock's packaged /etc/pam.d/hyprlock ships only `auth include login`, with no
+account or session sections. pam_end() then crashes cleaning up handles that
+were never initialised -- one of the documented causes of the hyprlock lockout
+where the session wedges with no prompt (hyprlock#953). configure_hyprlock_pam
+writes a complete stack so the account and session phases run.
+
+These tests exercise the REAL function body, extracted from the `archsetup`
+script at run time, against a fixture PAM path.
+
+Run from repo root:
+ python3 -m unittest tests.installer-steps.test_hyprlock_pam
+"""
+
+import os
+import subprocess
+import tempfile
+import unittest
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
+
+
+def run(path):
+ script = (
+ 'logfile=/dev/null\n'
+ 'display() { :; }\n'
+ 'backup_system_file() { :; }\n'
+ 'error_warn() { echo "WARN:$1"; return 1; }\n'
+ "source <(sed -n '/^configure_hyprlock_pam() {/,/^}/p' \"%s\")\n"
+ 'configure_hyprlock_pam "%s"\n'
+ ) % (ARCHSETUP, path)
+ return subprocess.run(["bash", "-c", script], capture_output=True,
+ text=True, timeout=10)
+
+
+class ConfigureHyprlockPam(unittest.TestCase):
+ def _write(self, initial="auth include login\n"):
+ fd, path = tempfile.mkstemp(prefix="pam-hyprlock-")
+ os.close(fd)
+ with open(path, "w") as f:
+ f.write(initial)
+ self.addCleanup(os.remove, path)
+ return path
+
+ def _stack(self, path):
+ with open(path) as f:
+ return [l.split()[0] for l in f if l.strip()
+ and not l.lstrip().startswith("#")]
+
+ # ---------------------------------------------------------- normal ----
+ def test_writes_all_three_phases(self):
+ path = self._write()
+ run(path)
+ phases = set(self._stack(path))
+ self.assertEqual(phases, {"auth", "account", "session"},
+ "the complete stack must carry all three PAM phases")
+
+ def test_account_and_session_are_the_added_ones(self):
+ path = self._write()
+ run(path)
+ stack = self._stack(path)
+ self.assertIn("account", stack)
+ self.assertIn("session", stack)
+
+ # -------------------------------------------------------- boundary ----
+ def test_idempotent_on_rerun(self):
+ path = self._write()
+ run(path)
+ first = open(path).read()
+ run(path)
+ self.assertEqual(open(path).read(), first,
+ "a second run must not stack duplicate lines")
+
+ def test_overwrites_the_incomplete_package_default(self):
+ # The package default is exactly `auth include login`; a re-run replaces
+ # it wholesale rather than appending to it.
+ path = self._write("auth include login\n")
+ run(path)
+ self.assertEqual(self._stack(path).count("auth"), 1)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/installer-steps/test_orchestrators.py b/tests/installer-steps/test_orchestrators.py
index 8de75cc..a2b235b 100644
--- a/tests/installer-steps/test_orchestrators.py
+++ b/tests/installer-steps/test_orchestrators.py
@@ -169,6 +169,8 @@ CALL_SITES = {
"configure_snapshots": ["configure_zfs_snapshots", "configure_btrfs_snapshots"],
"configure_zfs_snapshots": ["zfs_scrub_timer_units"],
"install_maintenance_config": ["install_maintenance_thresholds"],
+ "hyprland": ["configure_hyprlock_pam"],
+ "update_grub_cmdline": ["detect_gpu_vendors"],
}