diff --git a/bin/omarchy-first-run b/bin/omarchy-first-run index 992a3f4d..91230f3f 100755 --- a/bin/omarchy-first-run +++ b/bin/omarchy-first-run @@ -6,6 +6,17 @@ set -e omarchy-setup-user || true +wait_for_notifications() { + command -v omarchy-shell >/dev/null || return 0 + + for _ in {1..50}; do + omarchy-shell notifications ping >/dev/null 2>&1 && return 0 + sleep 0.1 + done + + return 0 +} + state_dir=~/.local/state/omarchy mkdir -p "$state_dir" @@ -16,6 +27,8 @@ if [[ ! -f $USER_MARKER ]]; then bash "$OMARCHY_PATH/install/user/hardware/recover-internal-monitor.sh" bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh" bash "$OMARCHY_PATH/install/user/first-run/gtk-primary-paste.sh" + + wait_for_notifications bash "$OMARCHY_PATH/install/user/first-run/welcome.sh" bash "$OMARCHY_PATH/install/user/first-run/wifi.sh" diff --git a/bin/omarchy-setup-hardware b/bin/omarchy-setup-hardware index 4c66395b..d1255754 100755 --- a/bin/omarchy-setup-hardware +++ b/bin/omarchy-setup-hardware @@ -58,5 +58,4 @@ export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-in export PATH="$OMARCHY_PATH/bin:$PATH" source "$OMARCHY_INSTALL/helpers/logging.sh" -source "$OMARCHY_INSTALL/helpers/chroot.sh" source "$OMARCHY_INSTALL/hardware/all.sh" diff --git a/bin/omarchy-setup-system b/bin/omarchy-setup-system index eb2c1c87..0d4e7df7 100755 --- a/bin/omarchy-setup-system +++ b/bin/omarchy-setup-system @@ -71,7 +71,6 @@ export OMARCHY_INSTALL_LOG_FILE="${OMARCHY_INSTALL_LOG_FILE:-/var/log/omarchy-in export PATH="$OMARCHY_PATH/bin:$PATH" source "$OMARCHY_INSTALL/helpers/logging.sh" -source "$OMARCHY_INSTALL/helpers/chroot.sh" start_install_log run_logged "$OMARCHY_INSTALL/config/theme-system.sh" @@ -86,7 +85,6 @@ omarchy-setup-hardware --install-user "$install_user" run_logged "$OMARCHY_INSTALL/login/sddm.sh" -run_logged "$OMARCHY_INSTALL/post-install/dns-resolver.sh" run_logged "$OMARCHY_INSTALL/post-install/pacman.sh" run_logged "$OMARCHY_INSTALL/post-install/udev.sh" run_logged "$OMARCHY_INSTALL/post-install/localdb.sh" diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index 01f86041..d78fbefc 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -56,21 +56,18 @@ snapshot_current_background() { snapshot_background_path "$current_background" "previous" } -set_theme_background() { +choose_theme_background() { local backgrounds=() - local current_background index new_background next_index new_background_snapshot + local current_background index next_index i + CHOSEN_THEME_BACKGROUND="" mapfile -d '' -t backgrounds < <( find -L "$HOME/.config/omarchy/backgrounds/$THEME_NAME/" "$CURRENT_THEME_PATH/backgrounds/" -maxdepth 1 -type f \ \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.webp' \) \ -print0 2>/dev/null | sort -z ) - if (( ${#backgrounds[@]} == 0 )); then - omarchy-notification-send "No background was found for theme" -t 2000 - shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true - return - fi + (( ${#backgrounds[@]} > 0 )) || return 1 current_background=$(readlink "$CURRENT_BACKGROUND_LINK" 2>/dev/null || true) index=-1 @@ -82,12 +79,28 @@ set_theme_background() { done if (( index == -1 )); then - new_background="${backgrounds[0]}" + CHOSEN_THEME_BACKGROUND="${backgrounds[0]}" else next_index=$(((index + 1) % ${#backgrounds[@]})) - new_background="${backgrounds[$next_index]}" + CHOSEN_THEME_BACKGROUND="${backgrounds[$next_index]}" + fi +} + +set_theme_background_link() { + choose_theme_background || return 1 + ln -nsf "$CHOSEN_THEME_BACKGROUND" "$CURRENT_BACKGROUND_LINK" +} + +set_theme_background() { + local new_background new_background_snapshot + + if ! choose_theme_background; then + omarchy-notification-send "No background was found for theme" -t 2000 + shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true + return fi + new_background="$CHOSEN_THEME_BACKGROUND" new_background_snapshot=$(snapshot_background_path "$new_background" "next") if [[ -f $OLD_BACKGROUND_SNAPSHOT && -f $new_background_snapshot ]]; then @@ -159,7 +172,10 @@ echo "$THEME_NAME" >"$HOME/.config/omarchy/current/theme.name" colors_payload=$([[ -f $CURRENT_THEME_PATH/colors.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/colors.toml") shell_payload=$([[ -f $CURRENT_THEME_PATH/shell.toml ]] && base64 -w 0 "$CURRENT_THEME_PATH/shell.toml") if [[ $THEME_HEADLESS == "1" ]]; then - : # No shell/session bus exists during ISO chroot finalization. + # No shell/session bus exists during ISO chroot finalization, but the first + # real login still needs a current background symlink for omarchy-shell to + # render. + [[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]] || set_theme_background_link || true elif [[ $OMARCHY_THEME_SKIP_BACKGROUND == "1" ]]; then shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true else diff --git a/bin/omarchy-upload-log b/bin/omarchy-upload-log index 3673720b..d5437d4a 100755 --- a/bin/omarchy-upload-log +++ b/bin/omarchy-upload-log @@ -68,7 +68,10 @@ install) TARGET_OMARCHY_LOG="/mnt/var/log/omarchy-install.log" cat "$SYSTEM_INFO" >"$TEMP_LOG" - [[ -s $ARCHINSTALL_LOG ]] && cat "$ARCHINSTALL_LOG" >>"$TEMP_LOG" + if [[ -s $ARCHINSTALL_LOG ]]; then + printf '\n========================================\nARCHINSTALL SUBSYSTEM LOG (%s)\n========================================\n\n' "$ARCHINSTALL_LOG" >>"$TEMP_LOG" + cat "$ARCHINSTALL_LOG" >>"$TEMP_LOG" + fi if [[ -s $LIVE_OMARCHY_LOG ]]; then printf '\n========================================\nOMARCHY INSTALL LOG (%s)\n========================================\n\n' "$LIVE_OMARCHY_LOG" >>"$TEMP_LOG" diff --git a/config/systemd/user/bt-agent.service b/config/systemd/user/bt-agent.service index 57fb661f..cad87cef 100644 --- a/config/systemd/user/bt-agent.service +++ b/config/systemd/user/bt-agent.service @@ -1,6 +1,7 @@ [Unit] Description=Bluetooth pairing agent (auto-accept) Documentation=man:bt-agent(1) +ConditionPathIsDirectory=/sys/class/bluetooth # bluez must be reachable on the system bus before we can register. After=dbus.socket Requires=dbus.socket diff --git a/etc/systemd/resolved.conf.d/10-disable-multicast.conf b/etc/systemd/resolved.conf.d/10-disable-multicast.conf index 523ed7c9..14798f6e 100644 --- a/etc/systemd/resolved.conf.d/10-disable-multicast.conf +++ b/etc/systemd/resolved.conf.d/10-disable-multicast.conf @@ -1,2 +1,3 @@ [Resolve] +LLMNR=no MulticastDNS=no diff --git a/install/config/enable-services.sh b/install/config/enable-services.sh index d130545b..8df4f171 100644 --- a/install/config/enable-services.sh +++ b/install/config/enable-services.sh @@ -5,6 +5,7 @@ systemctl enable cups-browsed.service systemctl enable avahi-daemon.service systemctl enable linux-modules-cleanup.service systemctl enable docker.socket +systemctl enable systemd-resolved.service systemctl enable NetworkManager.service systemctl enable power-profiles-daemon.service systemctl enable sddm.service diff --git a/install/config/firewall.sh b/install/config/firewall.sh index 5dd48fc3..de389431 100644 --- a/install/config/firewall.sh +++ b/install/config/firewall.sh @@ -10,8 +10,43 @@ ufw allow 53317/tcp ufw allow in proto udp from 172.16.0.0/12 to 172.17.0.1 port 53 comment 'allow-docker-dns' ufw allow in proto udp from 192.168.0.0/16 to 172.17.0.1 port 53 comment 'allow-docker-dns' -# Turn on Docker protections. -ufw-docker install +# Turn on Docker protections. ufw-docker refuses to install its after.rules +# block unless UFW is already active, but during ISO finalization the target +# chroot shares the live installer's kernel firewall. Keep the live firewall +# untouched: for this config-file-only install action, satisfy ufw-docker's +# status preflight without activating UFW. +install_ufw_docker_rules() { + local shim_dir status ufw_docker_bin + + ufw_docker_bin=$(command -v ufw-docker) + shim_dir=$(mktemp -d) + cat >"$shim_dir/ufw" <<'EOF' +#!/bin/bash +if [[ ${1:-} == "status" ]]; then + echo "Status: active" + exit 0 +fi + +exec /usr/bin/ufw "$@" +EOF + + # The packaged ufw-docker pins PATH internally, so run a temporary copy whose + # PATH can see the status shim above. + sed "0,/^PATH=/s#^PATH=.*#PATH=\"$shim_dir:/bin:/usr/bin:/sbin:/usr/sbin:/snap/bin/\"#" \ + "$ufw_docker_bin" >"$shim_dir/ufw-docker" + chmod 755 "$shim_dir/ufw" "$shim_dir/ufw-docker" + + if "$shim_dir/ufw-docker" install; then + status=0 + else + status=$? + fi + + rm -rf "$shim_dir" + return "$status" +} + +install_ufw_docker_rules # Installs are followed by reboot, so configure UFW to start on the installed # system instead of mutating the live install session's firewall. diff --git a/install/helpers/chroot.sh b/install/helpers/chroot.sh deleted file mode 100644 index acefb81b..00000000 --- a/install/helpers/chroot.sh +++ /dev/null @@ -1,12 +0,0 @@ -# Installs are followed by reboot, so enable units without starting/reloading -# running services during the install. -chrootable_systemctl_enable() { - sudo systemctl enable $1 -} - -chrootable_systemctl_enable_only() { - sudo systemctl enable $1 -} - -export -f chrootable_systemctl_enable -export -f chrootable_systemctl_enable_only diff --git a/install/helpers/logging.sh b/install/helpers/logging.sh index e046ec56..e325d83e 100644 --- a/install/helpers/logging.sh +++ b/install/helpers/logging.sh @@ -1,53 +1,79 @@ +omarchy_log_to_stdout() { + [[ ${OMARCHY_LOG_TO_STDOUT:-} == "1" || -z ${OMARCHY_INSTALL_LOG_FILE:-} ]] +} + +omarchy_log_line() { + if omarchy_log_to_stdout; then + echo "$1" + else + echo "$1" >>"$OMARCHY_INSTALL_LOG_FILE" + fi +} + start_install_log() { - mkdir -p "$(dirname "$OMARCHY_INSTALL_LOG_FILE")" - touch "$OMARCHY_INSTALL_LOG_FILE" - chmod 666 "$OMARCHY_INSTALL_LOG_FILE" 2>/dev/null || true + if ! omarchy_log_to_stdout; then + mkdir -p "$(dirname "$OMARCHY_INSTALL_LOG_FILE")" + touch "$OMARCHY_INSTALL_LOG_FILE" + chmod 666 "$OMARCHY_INSTALL_LOG_FILE" 2>/dev/null || true + fi export OMARCHY_START_TIME="${OMARCHY_START_TIME:-$(date '+%Y-%m-%d %H:%M:%S')}" export OMARCHY_START_EPOCH="${OMARCHY_START_EPOCH:-$(date +%s)}" - echo "=== Omarchy Setup Started: $OMARCHY_START_TIME ===" >>"$OMARCHY_INSTALL_LOG_FILE" + omarchy_log_line "=== Omarchy Setup Started: $OMARCHY_START_TIME ===" } stop_install_log() { - [[ -n ${OMARCHY_INSTALL_LOG_FILE:-} ]] || return 0 - local end_time end_epoch duration mins secs end_time=$(date '+%Y-%m-%d %H:%M:%S') end_epoch=$(date +%s) - echo "=== Omarchy Setup Completed: $end_time ===" >>"$OMARCHY_INSTALL_LOG_FILE" + omarchy_log_line "=== Omarchy Setup Completed: $end_time ===" if [[ -n ${OMARCHY_START_EPOCH:-} ]]; then duration=$((end_epoch - OMARCHY_START_EPOCH)) mins=$((duration / 60)) secs=$((duration % 60)) - echo "Omarchy setup: ${mins}m ${secs}s" >>"$OMARCHY_INSTALL_LOG_FILE" + omarchy_log_line "Omarchy setup: ${mins}m ${secs}s" fi } run_logged() { local script="$1" + local exit_code errexit_was_set=0 - if [[ -z ${OMARCHY_INSTALL_LOG_FILE:-} ]]; then - bash -eE -c 'source "$1"' bash "$script" - return - fi + omarchy_log_line "[$(date '+%Y-%m-%d %H:%M:%S')] Starting: $script" - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting: $script" >>"$OMARCHY_INSTALL_LOG_FILE" + case $- in + *e*) + errexit_was_set=1 + set +e + ;; + esac - if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then - PS4='+ ${BASH_SOURCE[0]##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' \ - bash -x -eE -c 'source "$1"' bash "$script" >"$OMARCHY_INSTALL_LOG_FILE" 2>&1 + if omarchy_log_to_stdout; then + if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then + PS4='+ ${BASH_SOURCE[0]##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' \ + bash -x -eE -c 'source "$1"' bash "$script" &1 + else + bash -eE -c 'source "$1"' bash "$script" &1 + fi else - bash -eE -c 'source "$1"' bash "$script" >"$OMARCHY_INSTALL_LOG_FILE" 2>&1 + if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then + PS4='+ ${BASH_SOURCE[0]##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' \ + bash -x -eE -c 'source "$1"' bash "$script" >"$OMARCHY_INSTALL_LOG_FILE" 2>&1 + else + bash -eE -c 'source "$1"' bash "$script" >"$OMARCHY_INSTALL_LOG_FILE" 2>&1 + fi fi - local exit_code=$? + exit_code=$? + (( errexit_was_set )) && set -e + if (( exit_code == 0 )); then - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Completed: $script" >>"$OMARCHY_INSTALL_LOG_FILE" + omarchy_log_line "[$(date '+%Y-%m-%d %H:%M:%S')] Completed: $script" else - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Failed: $script (exit code: $exit_code)" >>"$OMARCHY_INSTALL_LOG_FILE" + omarchy_log_line "[$(date '+%Y-%m-%d %H:%M:%S')] Failed: $script (exit code: $exit_code)" fi return $exit_code diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index d3375573..e3666b63 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -57,6 +57,7 @@ hyprsunset imagemagick imv inetutils +inotify-tools inxi networkmanager jq diff --git a/install/post-install/dns-resolver.sh b/install/post-install/dns-resolver.sh deleted file mode 100644 index c4d62b24..00000000 --- a/install/post-install/dns-resolver.sh +++ /dev/null @@ -1,5 +0,0 @@ -# https://wiki.archlinux.org/title/Systemd-resolved -# The target does not need the stub file to exist yet; systemd-resolved creates -# it at boot. -echo "Symlinking resolved stub-resolv to /etc/resolv.conf" -ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf diff --git a/install/post-install/system.sh b/install/post-install/system.sh index ad588b5e..a143c60a 100644 --- a/install/post-install/system.sh +++ b/install/post-install/system.sh @@ -1,4 +1,3 @@ -run_logged "$OMARCHY_INSTALL/post-install/dns-resolver.sh" run_logged "$OMARCHY_INSTALL/post-install/pacman.sh" run_logged "$OMARCHY_INSTALL/post-install/udev.sh" run_logged "$OMARCHY_INSTALL/post-install/localdb.sh" diff --git a/test/cli b/test/cli index b085866b..7213e2b3 100755 --- a/test/cli +++ b/test/cli @@ -6,6 +6,7 @@ ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) CLI="$ROOT/bin/omarchy" TMPDIR="" PI_TMPDIR="" +THEME_TMPDIR="" export PATH="$ROOT/bin:$PATH" @@ -35,6 +36,7 @@ assert_output_contains() { cleanup() { [[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR" [[ -n $PI_TMPDIR && -d $PI_TMPDIR ]] && rm -rf "$PI_TMPDIR" + [[ -n $THEME_TMPDIR && -d $THEME_TMPDIR ]] && rm -rf "$THEME_TMPDIR" } trap cleanup EXIT @@ -399,6 +401,14 @@ pass "vscode generated theme clears obsolete extension marker" jq -e '.[] | select(.identifier.id == "local.omarchy-theme" and .relativeLocation == "omarchy-theme")' "$PI_TMPDIR/.vscode/extensions/extensions.json" >/dev/null || fail "vscode generated theme registers local extension" pass "vscode generated theme registers local extension" +THEME_TMPDIR=$(mktemp -d) +OMARCHY_PATH="$ROOT" HOME="$THEME_TMPDIR" OMARCHY_THEME_HEADLESS=1 "$ROOT/bin/omarchy-theme-set" "Tokyo Night" +background_path=$(readlink -f "$THEME_TMPDIR/.config/omarchy/current/background" 2>/dev/null || true) +expected_background="$THEME_TMPDIR/.config/omarchy/current/theme/backgrounds/0-swirl-buck.jpg" +[[ $background_path == "$expected_background" ]] || fail "headless theme set creates current background symlink" +[[ -f $background_path ]] || fail "headless theme background target exists" +pass "headless theme set creates current background symlink" + for binary in \ omarchy-update \ omarchy-theme-set \ diff --git a/test/shell.d/bluetooth-test.sh b/test/shell.d/bluetooth-test.sh index aa8c5f10..a0a6d5c8 100644 --- a/test/shell.d/bluetooth-test.sh +++ b/test/shell.d/bluetooth-test.sh @@ -4,6 +4,10 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" +grep -q '^ConditionPathIsDirectory=/sys/class/bluetooth$' "$ROOT/config/systemd/user/bt-agent.service" || \ + fail "bt-agent is skipped on machines without Bluetooth hardware" +pass "bt-agent is skipped on machines without Bluetooth hardware" + run_node_test <<'JS' const bluetooth = requireFromRoot('shell/plugins/panels/bluetooth/Model.js') diff --git a/test/shell.d/firewall-config-test.sh b/test/shell.d/firewall-config-test.sh new file mode 100644 index 00000000..e402bfb1 --- /dev/null +++ b/test/shell.d/firewall-config-test.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname -- "${BASH_SOURCE[0]}")/base-test.sh" + +stub_dir=$(mktemp -d) +trap 'rm -rf "$stub_dir"' EXIT + +cat >"$stub_dir/ufw" <<'STUB' +#!/bin/bash +printf 'ufw %s\n' "$*" >>"$TEST_LOG" +if [[ ${1:-} == status ]]; then + echo 'Status: inactive' +fi +STUB + +cat >"$stub_dir/ufw-docker" <<'STUB' +#!/bin/bash +set -euo pipefail +PATH="/bin:/usr/bin:/sbin:/usr/sbin:/snap/bin/" +printf 'ufw-docker %s\n' "$*" >>"$TEST_LOG" +if ! ufw status 2>/dev/null | grep -Fq 'Status: active'; then + echo 'inactive' >&2 + exit 1 +fi +STUB + +cat >"$stub_dir/sed" <<'STUB' +#!/bin/bash +printf 'sed %s\n' "$*" >>"$TEST_LOG" +if [[ ${1:-} == 0,/^PATH=* ]]; then + exec /usr/bin/sed "$@" +fi +exit 0 +STUB + +cat >"$stub_dir/systemctl" <<'STUB' +#!/bin/bash +printf 'systemctl %s\n' "$*" >>"$TEST_LOG" +STUB + +chmod +x "$stub_dir"/* + +export TEST_LOG="$stub_dir/firewall.log" +PATH="$stub_dir:$PATH" bash -eE -c 'source "$1"' bash "$ROOT/install/config/firewall.sh" + +grep -q '^ufw-docker install$' "$TEST_LOG" || fail "ufw-docker rules are installed" +grep -q '^systemctl enable ufw$' "$TEST_LOG" || fail "ufw is enabled for next boot" + +pass "firewall config installs ufw-docker rules without activating live UFW" diff --git a/test/shell.d/logging-test.sh b/test/shell.d/logging-test.sh new file mode 100644 index 00000000..58e9a6c4 --- /dev/null +++ b/test/shell.d/logging-test.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname -- "${BASH_SOURCE[0]}")/base-test.sh" + +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT + +failing_script="$work_dir/fail.sh" +log_file="$work_dir/install.log" +cat >"$failing_script" <<'SCRIPT' +echo "about to fail" +false +SCRIPT + +set +e +( + set -euo pipefail + export OMARCHY_INSTALL_LOG_FILE="$log_file" + source "$ROOT/install/helpers/logging.sh" + run_logged "$failing_script" + echo "unreachable" +) +status=$? +set -e + +(( status != 0 )) || fail "run_logged returns failing script status" +grep -q "Starting: $failing_script" "$log_file" || fail "run_logged logs script start" +grep -q "about to fail" "$log_file" || fail "run_logged captures script output" +grep -q "Failed: $failing_script (exit code: 1)" "$log_file" || fail "run_logged logs failed script before errexit exits" + +stdout_log="$work_dir/stdout.log" +set +e +( + set -euo pipefail + export OMARCHY_INSTALL_LOG_FILE="$work_dir/iso-owned.log" + export OMARCHY_LOG_TO_STDOUT=1 + source "$ROOT/install/helpers/logging.sh" + run_logged "$failing_script" +) >"$stdout_log" 2>&1 +stdout_status=$? +set -e + +(( stdout_status != 0 )) || fail "stdout run_logged returns failing script status" +[[ ! -e $work_dir/iso-owned.log ]] || fail "stdout logging mode does not write directly to install log" +grep -q "Starting: $failing_script" "$stdout_log" || fail "stdout logging mode emits script start" +grep -q "about to fail" "$stdout_log" || fail "stdout logging mode emits script output" +grep -q "Failed: $failing_script (exit code: 1)" "$stdout_log" || fail "stdout logging mode emits failure marker" + +pass "run_logged records failures under errexit"