From f03bf06742a2ed4535d326e3220c685bca5aa0ed Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Fri, 9 Oct 2026 23:26:20 +0200 Subject: [PATCH 1/4] Stop the boot from waiting on a black screen for the console to answer About 1 boot in 10 of an installed system stops on a black screen at "Starting Switch Root" until a key is pressed. systemd 257 and later asks the console for its size with ANSI escape sequences when PID 1 starts and waits for the reply; with Plymouth holding the VT the reply sometimes never comes. Measured in QEMU on repeated boots of one install: 3 hangs in 30 and 1 in 40 on the prebuilt UKI, 7 in 40 on the stock initramfs, every one of them ended by pressing Enter; none in 40 with a serial console, none in 40 with plymouth.enable=0, and none in 80 with systemd.tty.term.console=dumb, which makes systemd skip the query. systemd fixed two variants of this in v261; Arch's 261.2 still shows it. The parameter goes where the last Plymouth boot fix went (3f725531, initramfs_async=0): into the packaged omarchy-defaults.conf for fresh installs, and a migration rebuilds the boot image on existing systems, once per machine, only where the booted command line lacks it. Nothing visible changes under "quiet splash". Refs #2931 --- etc/limine-entry-tool.d/omarchy-defaults.conf | 9 +++ migrations/1791581004.sh | 31 +++++++++ .../console-no-ansi-query-migration-test.sh | 66 +++++++++++++++++++ test/shell.d/limine-defaults-test.sh | 4 ++ 4 files changed, 110 insertions(+) create mode 100644 migrations/1791581004.sh create mode 100644 test/shell.d/console-no-ansi-query-migration-test.sh diff --git a/etc/limine-entry-tool.d/omarchy-defaults.conf b/etc/limine-entry-tool.d/omarchy-defaults.conf index ec0fa96f..1e421365 100644 --- a/etc/limine-entry-tool.d/omarchy-defaults.conf +++ b/etc/limine-entry-tool.d/omarchy-defaults.conf @@ -9,6 +9,15 @@ KERNEL_CMDLINE[default]+=" quiet splash loglevel=0 systemd.show_status=false rd. # is fixed upstream. KERNEL_CMDLINE[default]+=" initramfs_async=0" +# systemd 257 and later asks the console for its size with ANSI escape +# sequences when PID 1 starts, and waits for the reply. With Plymouth holding +# the VT the reply sometimes never comes, and the boot sits on a black screen +# at "Starting Switch Root" until a key is pressed: about 1 boot in 10. A dumb +# $TERM for the console makes systemd skip the query. Nothing visible changes +# under "quiet splash". Drop it once a systemd release boots clean in a +# repeated boot test. +KERNEL_CMDLINE[default]+=" systemd.tty.term.console=dumb" + CUSTOM_UKI_NAME="omarchy" ENABLE_LIMINE_FALLBACK=yes diff --git a/migrations/1791581004.sh b/migrations/1791581004.sh new file mode 100644 index 00000000..c95a74ff --- /dev/null +++ b/migrations/1791581004.sh @@ -0,0 +1,31 @@ +echo "Stop the boot from waiting on a black screen for the console to answer" + +# systemd asks the console for its size when PID 1 starts and waits for the +# reply. With Plymouth holding the screen the reply sometimes never comes, and +# about 1 boot in 10 sits on a black screen until a key is pressed. +# omarchy-defaults.conf now carries systemd.tty.term.console=dumb, which makes +# systemd skip the query. The package has already written that file by the +# time this runs; the boot image still holds the old command line until it is +# rebuilt. + +defaults_conf="${OMARCHY_LIMINE_DEFAULTS_CONF:-/etc/limine-entry-tool.d/omarchy-defaults.conf}" +running_cmdline="${OMARCHY_RUNNING_CMDLINE:-/proc/cmdline}" +rebuild_marker="${OMARCHY_LIMINE_REBUILD_MARKER:-/var/lib/omarchy/migrations/1791581004}" +param="systemd.tty.term.console=dumb" + +omarchy-cmd-present limine-mkinitcpio || exit 0 +[[ -f $defaults_conf && -r $running_cmdline ]] || exit 0 + +# The running kernel keeps its old command line until reboot, so a marker +# records the machine-wide rebuild: another user's migration must not repeat +# it before then, while a missing marker still retries an interrupted rebuild. +[[ ! -e $rebuild_marker ]] || exit 0 + +# A machine that boots with it already, or whose defaults were edited to leave +# it out, has nothing to rebuild. +[[ " $(<"$running_cmdline") " != *" $param "* ]] || exit 0 +grep -Eq "^KERNEL_CMDLINE\[default\]\+=\".*${param//./\\.}.*\"" "$defaults_conf" || exit 0 + +echo "The booted kernel is missing $param; rebuilding the boot image" +sudo limine-mkinitcpio +sudo install -Dm644 /dev/null "$rebuild_marker" diff --git a/test/shell.d/console-no-ansi-query-migration-test.sh b/test/shell.d/console-no-ansi-query-migration-test.sh new file mode 100644 index 00000000..d0c2b1d0 --- /dev/null +++ b/test/shell.d/console-no-ansi-query-migration-test.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# +# The migration that puts systemd.tty.term.console=dumb into effect on an +# existing install: the package has already written the parameter into +# omarchy-defaults.conf, so what is left is rebuilding the boot image, once per +# machine, and only where it is needed. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +migration="$ROOT/migrations/1791581004.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT +stub_bin="$test_dir/bin" +mkdir -p "$stub_bin" + +# sudo just drops the prefix; limine-mkinitcpio records that it ran. +printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo" +printf '#!/bin/bash\necho rebuilt >>"${REBUILDS:?}"\n' >"$stub_bin/limine-mkinitcpio" +chmod +x "$stub_bin"/* + +old_cmdline="root=PARTUUID=1 rw quiet splash initramfs_async=0" +new_cmdline="$old_cmdline systemd.tty.term.console=dumb" + +run() { # booted cmdline, defaults conf + : >"$test_dir/rebuilds" + printf '%s\n' "$1" >"$test_dir/cmdline" + PATH="$stub_bin:$ROOT/bin:$PATH" REBUILDS="$test_dir/rebuilds" \ + OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$2" \ + OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" bash "$migration" >/dev/null +} +rebuilds() { wc -l <"$test_dir/rebuilds"; } + +run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" +[[ $(rebuilds) == 1 && -e $test_dir/marker ]] || + fail "a machine booted without the parameter did not get its boot image rebuilt" +pass "a machine booted without the parameter gets its boot image rebuilt, with the packaged defaults" + +run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" +[[ $(rebuilds) == 0 ]] || fail "the rebuild ran a second time before a reboot" +pass "the rebuild runs once per machine, not once per user" + +rm -f "$test_dir/marker" +run "$new_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" +[[ $(rebuilds) == 0 && ! -e $test_dir/marker ]] || fail "a machine that already boots with the parameter was rebuilt" +pass "a machine that already boots with the parameter is left alone" + +grep -v "tty.term.console" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" >"$test_dir/edited.conf" +run "$old_cmdline" "$test_dir/edited.conf" +[[ $(rebuilds) == 0 ]] || fail "a machine whose defaults leave the parameter out was rebuilt anyway" +pass "a machine whose defaults were edited to leave the parameter out is left alone" + +# No Limine: the stand-in goes, and the PATH holds only what the migration +# needs, so a limine-mkinitcpio installed on the machine running this test is +# not found either. +rm "$stub_bin/limine-mkinitcpio" +mkdir -p "$test_dir/tools" +for tool in bash grep install cat; do ln -s "$(command -v "$tool")" "$test_dir/tools/$tool"; done +: >"$test_dir/rebuilds" +printf '%s\n' "$old_cmdline" >"$test_dir/cmdline" +PATH="$stub_bin:$ROOT/bin:$test_dir/tools" REBUILDS="$test_dir/rebuilds" \ + OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" \ + OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" "$test_dir/tools/bash" "$migration" >/dev/null +[[ ! -e $test_dir/marker ]] || fail "a machine without Limine was marked as rebuilt" +pass "a machine without Limine is left alone" diff --git a/test/shell.d/limine-defaults-test.sh b/test/shell.d/limine-defaults-test.sh index 21adc94f..f69e9535 100644 --- a/test/shell.d/limine-defaults-test.sh +++ b/test/shell.d/limine-defaults-test.sh @@ -10,6 +10,10 @@ grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" || fail "the packaged Limine defaults still unpack the initramfs synchronously" pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt" +grep -Fxq 'KERNEL_CMDLINE[default]+=" systemd.tty.term.console=dumb"' "$packaged_defaults" || + fail "the packaged Limine defaults let PID 1 wait for the console to answer a size query" +pass "packaged Limine defaults keep PID 1 from waiting on the console" + grep -Fxq 'BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' "$packaged_defaults" || fail "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere" pass "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere" From 295a926ffdd8115edf1a3ecd710e43856fae5dbf Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Fri, 9 Oct 2026 23:52:07 +0200 Subject: [PATCH 2/4] Test the boot image migration as omarchy-migrate runs it, and a failed rebuild The test ran the migration with plain bash. omarchy-migrate runs it with bash -euo pipefail, and the migration depends on that: it is what stops a failed limine-mkinitcpio before the marker is written, so the next update tries again. The stand-in rebuild could not fail, so nothing held that promise. The test now uses the same options and adds the case: a rebuild that fails ends the migration with an error and leaves no marker, and the next run rebuilds and marks it. --- .../console-no-ansi-query-migration-test.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/shell.d/console-no-ansi-query-migration-test.sh b/test/shell.d/console-no-ansi-query-migration-test.sh index d0c2b1d0..e087b0c4 100644 --- a/test/shell.d/console-no-ansi-query-migration-test.sh +++ b/test/shell.d/console-no-ansi-query-migration-test.sh @@ -15,9 +15,10 @@ trap 'rm -rf "$test_dir"' EXIT stub_bin="$test_dir/bin" mkdir -p "$stub_bin" -# sudo just drops the prefix; limine-mkinitcpio records that it ran. +# sudo just drops the prefix; limine-mkinitcpio records that it ran, and fails +# when told to. printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo" -printf '#!/bin/bash\necho rebuilt >>"${REBUILDS:?}"\n' >"$stub_bin/limine-mkinitcpio" +printf '#!/bin/bash\necho rebuilt >>"${REBUILDS:?}"\n[[ -z ${REBUILD_FAILS:-} ]]\n' >"$stub_bin/limine-mkinitcpio" chmod +x "$stub_bin"/* old_cmdline="root=PARTUUID=1 rw quiet splash initramfs_async=0" @@ -28,14 +29,22 @@ run() { # booted cmdline, defaults conf printf '%s\n' "$1" >"$test_dir/cmdline" PATH="$stub_bin:$ROOT/bin:$PATH" REBUILDS="$test_dir/rebuilds" \ OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$2" \ - OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" bash "$migration" >/dev/null + OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" bash -euo pipefail "$migration" >/dev/null } rebuilds() { wc -l <"$test_dir/rebuilds"; } +# omarchy-migrate runs a migration with bash -euo pipefail, and so does run: +# that is what stops a failed rebuild before the marker is written. +if REBUILD_FAILS=1 run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"; then + fail "a rebuild that failed was reported as a success" +fi +[[ $(rebuilds) == 1 && ! -e $test_dir/marker ]] || fail "a failed rebuild was marked as done" +pass "a rebuild that fails stops the migration and leaves no marker" + run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" [[ $(rebuilds) == 1 && -e $test_dir/marker ]] || fail "a machine booted without the parameter did not get its boot image rebuilt" -pass "a machine booted without the parameter gets its boot image rebuilt, with the packaged defaults" +pass "the next run rebuilds the boot image, with the packaged defaults, and marks it" run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" [[ $(rebuilds) == 0 ]] || fail "the rebuild ran a second time before a reboot" @@ -61,6 +70,6 @@ for tool in bash grep install cat; do ln -s "$(command -v "$tool")" "$test_dir/t printf '%s\n' "$old_cmdline" >"$test_dir/cmdline" PATH="$stub_bin:$ROOT/bin:$test_dir/tools" REBUILDS="$test_dir/rebuilds" \ OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" \ - OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" "$test_dir/tools/bash" "$migration" >/dev/null + OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" "$test_dir/tools/bash" -euo pipefail "$migration" >/dev/null [[ ! -e $test_dir/marker ]] || fail "a machine without Limine was marked as rebuilt" pass "a machine without Limine is left alone" From 3824577c123bcaa0badd821338127c52a32ac620 Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Fri, 9 Oct 2026 23:54:17 +0200 Subject: [PATCH 3/4] Mark the boot image migration done only when a boot entry has the parameter limine-mkinitcpio carries on past a kernel it could not build, with a full boot partition for one, and still exits 0. The migration then wrote its marker over an unchanged boot image, and no later update would try again. After the rebuild it now looks at the boot menu: if the menu has command lines and none carries the parameter, the migration fails and stays pending. A menu with no command lines in it, where they are inside the boot images, cannot be checked this way and is accepted as before. --- migrations/1791581004.sh | 14 +++++++++++++ .../console-no-ansi-query-migration-test.sh | 20 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/migrations/1791581004.sh b/migrations/1791581004.sh index c95a74ff..2fda0f4f 100644 --- a/migrations/1791581004.sh +++ b/migrations/1791581004.sh @@ -12,6 +12,8 @@ defaults_conf="${OMARCHY_LIMINE_DEFAULTS_CONF:-/etc/limine-entry-tool.d/omarchy- running_cmdline="${OMARCHY_RUNNING_CMDLINE:-/proc/cmdline}" rebuild_marker="${OMARCHY_LIMINE_REBUILD_MARKER:-/var/lib/omarchy/migrations/1791581004}" param="systemd.tty.term.console=dumb" +esp=$(sed -n 's/^ESP_PATH=["'\'']\?\([^"'\'']*\).*/\1/p' /etc/default/limine 2>/dev/null | tail -n 1) +limine_conf="${OMARCHY_LIMINE_CONF:-${esp:-/boot}/limine.conf}" omarchy-cmd-present limine-mkinitcpio || exit 0 [[ -f $defaults_conf && -r $running_cmdline ]] || exit 0 @@ -28,4 +30,16 @@ grep -Eq "^KERNEL_CMDLINE\[default\]\+=\".*${param//./\\.}.*\"" "$defaults_conf" echo "The booted kernel is missing $param; rebuilding the boot image" sudo limine-mkinitcpio + +# limine-mkinitcpio carries on past a kernel it could not build, a full boot +# partition for one, and still exits 0. Done means a boot entry has the +# parameter: without that the migration stays pending and the next update +# tries again. A menu with no command lines in it (they are inside the boot +# images then) has nothing to look at, and is taken at its word. +if sudo grep -Eq '^[[:space:]]*cmdline:' "$limine_conf" 2>/dev/null && + ! sudo grep -Eq "^[[:space:]]*cmdline:.*[[:space:]]${param//./\\.}([[:space:]]|\$)" "$limine_conf"; then + echo "The boot menu still has no entry with $param after the rebuild" >&2 + exit 1 +fi + sudo install -Dm644 /dev/null "$rebuild_marker" diff --git a/test/shell.d/console-no-ansi-query-migration-test.sh b/test/shell.d/console-no-ansi-query-migration-test.sh index e087b0c4..3c977546 100644 --- a/test/shell.d/console-no-ansi-query-migration-test.sh +++ b/test/shell.d/console-no-ansi-query-migration-test.sh @@ -18,7 +18,14 @@ mkdir -p "$stub_bin" # sudo just drops the prefix; limine-mkinitcpio records that it ran, and fails # when told to. printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo" -printf '#!/bin/bash\necho rebuilt >>"${REBUILDS:?}"\n[[ -z ${REBUILD_FAILS:-} ]]\n' >"$stub_bin/limine-mkinitcpio" +# A rebuild writes the new command line into the boot menu, unless it is the +# kind that builds nothing and still exits 0. +cat >"$stub_bin/limine-mkinitcpio" <<'STUB' +#!/bin/bash +echo rebuilt >>"${REBUILDS:?}" +[[ -z ${REBUILD_FAILS:-} ]] || exit 1 +[[ -n ${REBUILD_BUILDS_NOTHING:-} ]] || printf ' cmdline: %s\n' "${NEW_CMDLINE:?}" >"${LIMINE_CONF:?}" +STUB chmod +x "$stub_bin"/* old_cmdline="root=PARTUUID=1 rw quiet splash initramfs_async=0" @@ -27,7 +34,9 @@ new_cmdline="$old_cmdline systemd.tty.term.console=dumb" run() { # booted cmdline, defaults conf : >"$test_dir/rebuilds" printf '%s\n' "$1" >"$test_dir/cmdline" + printf ' cmdline: %s\n' "$1" >"$test_dir/limine.conf" PATH="$stub_bin:$ROOT/bin:$PATH" REBUILDS="$test_dir/rebuilds" \ + LIMINE_CONF="$test_dir/limine.conf" NEW_CMDLINE="$new_cmdline" OMARCHY_LIMINE_CONF="$test_dir/limine.conf" \ OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$2" \ OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" bash -euo pipefail "$migration" >/dev/null } @@ -41,6 +50,13 @@ fi [[ $(rebuilds) == 1 && ! -e $test_dir/marker ]] || fail "a failed rebuild was marked as done" pass "a rebuild that fails stops the migration and leaves no marker" +# limine-mkinitcpio exits 0 past a kernel it could not build. +if REBUILD_BUILDS_NOTHING=1 run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"; then + fail "a rebuild that changed nothing was reported as a success" +fi +[[ ! -e $test_dir/marker ]] || fail "a rebuild that changed nothing was marked as done" +pass "a rebuild that exits 0 without changing the boot menu leaves the migration pending" + run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" [[ $(rebuilds) == 1 && -e $test_dir/marker ]] || fail "a machine booted without the parameter did not get its boot image rebuilt" @@ -65,7 +81,7 @@ pass "a machine whose defaults were edited to leave the parameter out is left al # not found either. rm "$stub_bin/limine-mkinitcpio" mkdir -p "$test_dir/tools" -for tool in bash grep install cat; do ln -s "$(command -v "$tool")" "$test_dir/tools/$tool"; done +for tool in bash grep install cat sed tail; do ln -s "$(command -v "$tool")" "$test_dir/tools/$tool"; done : >"$test_dir/rebuilds" printf '%s\n' "$old_cmdline" >"$test_dir/cmdline" PATH="$stub_bin:$ROOT/bin:$test_dir/tools" REBUILDS="$test_dir/rebuilds" \ From 452835da8b5e9d7de621191c78f1d9b045c30eae Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Sat, 10 Oct 2026 00:06:46 +0200 Subject: [PATCH 4/4] Read the boot loader's config only where the migration needs it The migration read ESP_PATH from /etc/default/limine at its top. On a machine without that file sed fails, and under bash -euo pipefail the migration stopped there with an error, before the check that lets a machine without Limine skip it. The read moves to after the rebuild, the one place the path is used; a path given for the tests is taken first, and a missing file means /boot. --- migrations/1791581004.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/migrations/1791581004.sh b/migrations/1791581004.sh index 2fda0f4f..233f3aeb 100644 --- a/migrations/1791581004.sh +++ b/migrations/1791581004.sh @@ -12,8 +12,6 @@ defaults_conf="${OMARCHY_LIMINE_DEFAULTS_CONF:-/etc/limine-entry-tool.d/omarchy- running_cmdline="${OMARCHY_RUNNING_CMDLINE:-/proc/cmdline}" rebuild_marker="${OMARCHY_LIMINE_REBUILD_MARKER:-/var/lib/omarchy/migrations/1791581004}" param="systemd.tty.term.console=dumb" -esp=$(sed -n 's/^ESP_PATH=["'\'']\?\([^"'\'']*\).*/\1/p' /etc/default/limine 2>/dev/null | tail -n 1) -limine_conf="${OMARCHY_LIMINE_CONF:-${esp:-/boot}/limine.conf}" omarchy-cmd-present limine-mkinitcpio || exit 0 [[ -f $defaults_conf && -r $running_cmdline ]] || exit 0 @@ -31,6 +29,17 @@ grep -Eq "^KERNEL_CMDLINE\[default\]\+=\".*${param//./\\.}.*\"" "$defaults_conf" echo "The booted kernel is missing $param; rebuilding the boot image" sudo limine-mkinitcpio +# The boot menu is on the ESP, which /etc/default/limine names; /boot when it +# does not, or is not there. +limine_conf="${OMARCHY_LIMINE_CONF:-}" +if [[ -z $limine_conf ]]; then + esp="" + if [[ -r /etc/default/limine ]]; then + esp=$(sed -n 's/^ESP_PATH=["'\'']\?\([^"'\'']*\).*/\1/p' /etc/default/limine | tail -n 1) + fi + limine_conf="${esp:-/boot}/limine.conf" +fi + # limine-mkinitcpio carries on past a kernel it could not build, a full boot # partition for one, and still exits 0. Done means a boot entry has the # parameter: without that the migration stays pending and the next update