diff --git a/bin/omarchy-hw-qualcomm-soc b/bin/omarchy-hw-qualcomm-soc new file mode 100755 index 0000000000..7bbe6c8b24 --- /dev/null +++ b/bin/omarchy-hw-qualcomm-soc @@ -0,0 +1,6 @@ +#!/bin/bash + +# omarchy:summary=Detect a Qualcomm SoC from the boot device tree. + +[[ -r /sys/firmware/devicetree/base/compatible ]] || exit 1 +tr '\0' '\n' /dev/null diff --git a/bin/omarchy-refresh-pacman b/bin/omarchy-refresh-pacman index 299d6c20d2..1f5921c7c6 100755 --- a/bin/omarchy-refresh-pacman +++ b/bin/omarchy-refresh-pacman @@ -1,23 +1,34 @@ #!/bin/bash # omarchy:summary=Overwrite the package configuration for /etc/pacman with the Omarchy default of using its dedicated mirrors and repositories, then update all packages. +# omarchy:args=[stable|rc|edge] # omarchy:requires-sudo=true -sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak -sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak +set -e channel="${1:-stable}" +pacman_config=${OMARCHY_PACMAN_CONFIG:-/etc/pacman.conf} +mirrorlist=${OMARCHY_MIRRORLIST:-/etc/pacman.d/mirrorlist} -if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then - echo "Error: Invalid channel '$channel'. Must be one of: stable, rc, edge" +# Do not replace a working ARM repository setup with an unpublished channel. +if [[ $(uname -m) == "aarch64" && ( $channel == "stable" || $channel == "rc" ) ]]; then + echo "Omarchy $channel packages are not published for aarch64 yet. No repository configuration changed." >&2 + echo "Use 'omarchy refresh pacman edge' only if you intend to use edge packages and upgrade the system." >&2 exit 1 fi +staged=$(mktemp -d) +trap 'rm -rf -- "$staged"' EXIT +source "$OMARCHY_PATH/install/helpers/pacman.sh" +pacman_write_repository_config "$channel" "$staged/pacman.conf" "$staged/mirrorlist" + echo "Setting channel to $channel" echo -sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf -sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist +sudo cp -f "$pacman_config" "$pacman_config.bak" +sudo cp -f "$mirrorlist" "$mirrorlist.bak" +sudo cp -f "$staged/pacman.conf" "$pacman_config" +sudo cp -f "$staged/mirrorlist" "$mirrorlist" # Allow user customization of /etc/pacman.conf before the upgrade runs omarchy-hook pre-refresh-pacman diff --git a/default/pacman/mirrorlist-aarch64 b/default/pacman/mirrorlist-aarch64 new file mode 100644 index 0000000000..c7185ed8f6 --- /dev/null +++ b/default/pacman/mirrorlist-aarch64 @@ -0,0 +1,2 @@ +# Arch Linux ARM publishes this endpoint with a $arch/$repo layout. +Server = http://mirror.archlinuxarm.org/$arch/$repo diff --git a/default/pacman/pacman-aarch64.conf b/default/pacman/pacman-aarch64.conf new file mode 100644 index 0000000000..b11e10a47c --- /dev/null +++ b/default/pacman/pacman-aarch64.conf @@ -0,0 +1,30 @@ +# See the pacman.conf(5) manpage for option and repository directives +# Arch Linux ARM provides the aarch64 repositories used below. + +[options] +Color +ILoveCandy +VerbosePkgLists +HoldPkg = pacman glibc +Architecture = auto +CheckSpace +ParallelDownloads = 5 +DownloadUser = alpm + +# By default, pacman accepts packages signed by keys that its local keyring +# trusts (see pacman-key and its man page), as well as unsigned packages. +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional + +# pacman searches repositories in the order defined here. +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +[alarm] +Include = /etc/pacman.d/mirrorlist + +[aur] +Include = /etc/pacman.d/mirrorlist diff --git a/etc/mkinitcpio.conf.d/thunderbolt_module.conf b/etc/mkinitcpio.conf.d/thunderbolt_module.conf index 9518f8b120..eb0565bef5 100644 --- a/etc/mkinitcpio.conf.d/thunderbolt_module.conf +++ b/etc/mkinitcpio.conf.d/thunderbolt_module.conf @@ -1 +1,4 @@ -MODULES+=(thunderbolt) +# linux-aarch64 does not provide the thunderbolt module. +if [[ $(uname -m) == x86_64 ]]; then + MODULES+=(thunderbolt) +fi diff --git a/install/hardware/all.sh b/install/hardware/all.sh index 7b806ce8ea..05c3d8a267 100644 --- a/install/hardware/all.sh +++ b/install/hardware/all.sh @@ -40,7 +40,12 @@ run_logged "$OMARCHY_INSTALL/hardware/apple/fix-suspend-nvme.sh" run_logged "$OMARCHY_INSTALL/hardware/apple/fix-t2.sh" run_logged "$OMARCHY_INSTALL/hardware/apple/fix-brcmfmac-supplicant.sh" +run_logged "$OMARCHY_INSTALL/hardware/qualcomm/dtb-uki.sh" +run_logged "$OMARCHY_INSTALL/hardware/qualcomm/kernel-params.sh" +run_logged "$OMARCHY_INSTALL/hardware/qualcomm/firmware.sh" + run_logged "$OMARCHY_INSTALL/hardware/lenovo/fix-yoga-pro7-bass-speakers.sh" +run_logged "$OMARCHY_INSTALL/hardware/lenovo/yoga-slim7x.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-bcm43xx.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-surface-keyboard.sh" diff --git a/install/hardware/lenovo/start-yoga-slim7x-remoteprocs.sh b/install/hardware/lenovo/start-yoga-slim7x-remoteprocs.sh new file mode 100644 index 0000000000..ec7a5c9419 --- /dev/null +++ b/install/hardware/lenovo/start-yoga-slim7x-remoteprocs.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -euo pipefail + +remoteproc_root=${OMARCHY_YOGA_REMOTEPROC_ROOT:-/sys/class/remoteproc} +attempts=${OMARCHY_YOGA_REMOTEPROC_ATTEMPTS:-30} +sleep_seconds=${OMARCHY_YOGA_REMOTEPROC_SLEEP:-1} + +for ((attempt = 1; attempt <= attempts; attempt++)); do + adsp_started=0 + shopt -s nullglob + + for state_file in "$remoteproc_root"/remoteproc*/state; do + remoteproc=${state_file%/state} + [[ -r $remoteproc/firmware && -r $state_file ]] || continue + + firmware=$(tr -d '\0\n' <"$remoteproc/firmware") + case "$firmware" in + *qcadsp*) kind=ADSP ;; + *qccdsp*) kind=CDSP ;; + *) continue ;; + esac + + if [[ $(<$state_file) == running ]]; then + echo "Yoga Slim 7x: $kind is already running" + [[ $kind == ADSP ]] && adsp_started=1 + elif [[ -w $state_file ]] && printf 'start\n' >"$state_file"; then + echo "Yoga Slim 7x: started $kind" + [[ $kind == ADSP ]] && adsp_started=1 + else + echo "Yoga Slim 7x: could not start $kind ($firmware)" >&2 + fi + done + + ((adsp_started)) && exit 0 + sleep "$sleep_seconds" +done + +echo "Yoga Slim 7x: audio DSP did not appear or could not be started" >&2 +exit 1 diff --git a/install/hardware/lenovo/yoga-slim7x.sh b/install/hardware/lenovo/yoga-slim7x.sh new file mode 100644 index 0000000000..2c9ed5c731 --- /dev/null +++ b/install/hardware/lenovo/yoga-slim7x.sh @@ -0,0 +1,68 @@ +# Lenovo Yoga Slim 7x (14Q8X9 / 83ED) board-specific setup. + +yoga_slim7x_compatible="" +compatible_path=${OMARCHY_YOGA_COMPATIBLE_PATH:-/sys/firmware/devicetree/base/compatible} +modules_load_dir=${OMARCHY_YOGA_MODULES_LOAD_DIR:-/etc/modules-load.d} +mkinitcpio_dir=${OMARCHY_YOGA_MKINITCPIO_DIR:-/etc/mkinitcpio.conf.d} +limine_config_dir=${OMARCHY_YOGA_LIMINE_CONFIG_DIR:-/etc/limine-entry-tool.d} +systemd_dir=${OMARCHY_YOGA_SYSTEMD_DIR:-/etc/systemd/system} + +if [[ -r $compatible_path ]]; then + yoga_slim7x_compatible=$(tr '\0' '\n' <"$compatible_path") +fi + +if omarchy-hw-qualcomm-soc && + { grep -qi '^lenovo,yoga-slim7x$' <<<"$yoga_slim7x_compatible" || + omarchy-hw-match 'Yoga Slim 7x' || omarchy-hw-match '83ED'; }; then + echo "Detected Lenovo Yoga Slim 7x, applying board-specific support..." + + # The Yoga exposes its CPU performance domains through SCMI. + mkdir -p "$modules_load_dir" + echo "scmi-cpufreq" >"$modules_load_dir/yoga-slim7x.conf" + + # Initialize the internal keyboard and display before disk unlock. + mkdir -p "$mkinitcpio_dir" + cat >"$mkinitcpio_dir/yoga-slim7x-initramfs.conf" <<'CONF' +MODULES+=(i2c-hid-of qrtr ps883x pmic_glink_altmode) + +# The board-signed zap shader is included by qcom-firmware-extract. +for firmware in \ + qcom/gen70500_sqe.fw \ + qcom/gen70500_gmu.bin; do + for suffix in '' .zst .xz; do + for directory in "${OMARCHY_YOGA_FIRMWARE_ROOT:-/usr/lib/firmware}/updates" \ + "${OMARCHY_YOGA_FIRMWARE_ROOT:-/usr/lib/firmware}"; do + if [[ -f $directory/$firmware$suffix ]]; then + FILES+=("$directory/$firmware$suffix") + break 2 + fi + done + done +done +unset firmware directory suffix +CONF + + mkdir -p "$limine_config_dir" + cat >"$limine_config_dir/yoga-slim7x.conf" <<'CONF' +KERNEL_CMDLINE[default]+=" initcall_blacklist=simpledrm_platform_driver_init" +# Keep Plymouth and the keyboard hooks on the laptop console, not the serial port. +KERNEL_CMDLINE[default]+=" console=tty0" +CONF + + # Start the board's DSP remote processors. + mkdir -p "$systemd_dir" + cat >"$systemd_dir/yoga-slim7x-remoteprocs.service" <<'UNIT' +[Unit] +Description=Start the Lenovo Yoga Slim 7x DSPs +ConditionPathExists=!/etc/modprobe.d/qualcomm-adsp-nofw.conf + +[Service] +Type=oneshot +ExecStart=/bin/bash /usr/share/omarchy/install/hardware/lenovo/start-yoga-slim7x-remoteprocs.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +UNIT + systemctl enable yoga-slim7x-remoteprocs.service +fi diff --git a/install/hardware/qualcomm/dtb-uki.sh b/install/hardware/qualcomm/dtb-uki.sh new file mode 100644 index 0000000000..a7728f8260 --- /dev/null +++ b/install/hardware/qualcomm/dtb-uki.sh @@ -0,0 +1,50 @@ +# Embed Qualcomm device trees so systemd-stub can select one from SMBIOS data. +# DeviceTreeAuto requires literal paths, so hardware setup refreshes the list. + +if omarchy-hw-qualcomm-soc; then + omarchy-pkg-add systemd-ukify + + dtb_dir=${OMARCHY_QUALCOMM_DTB_DIR:-/boot/dtbs/qcom} + uki_config=${OMARCHY_QUALCOMM_UKI_CONFIG:-/etc/kernel/uki.conf} + dtbs=() + for dtb in "$dtb_dir"/x1*.dtb "$dtb_dir"/hamoa*.dtb \ + "$dtb_dir"/glymur*.dtb "$dtb_dir"/sc8280xp*.dtb; do + if [[ -f $dtb && $dtb != *-el2.dtb ]]; then + dtbs+=("$dtb") + fi + done + + if ((${#dtbs[@]} == 0)); then + echo "No Windows-on-ARM device trees under $dtb_dir; the UKI gets no .dtbauto sections" >&2 + else + managed_begin="# BEGIN OMARCHY QUALCOMM DEVICE TREES" + managed_end="# END OMARCHY QUALCOMM DEVICE TREES" + + mkdir -p "$(dirname "$uki_config")" + uki_tmp=$(mktemp "${uki_config}.XXXXXX") + + if [[ -f $uki_config ]]; then + cp -p "$uki_config" "$uki_tmp" + awk -v begin="$managed_begin" -v end="$managed_end" ' + $0 == begin { managed = 1; next } + managed && $0 == end { managed = 0; next } + !managed { print } + ' "$uki_config" >"$uki_tmp" + else + chmod 0644 "$uki_tmp" + fi + + { + echo "$managed_begin" + echo "# Generated by Omarchy for Qualcomm Snapdragon devices." + echo "# systemd-stub selects a device tree from SMBIOS data." + echo "# Rerun 'omarchy apply hardware' after a kernel update adds new boards." + echo "[UKI]" + echo "DeviceTreeAuto=${dtbs[*]}" + echo "$managed_end" + } >>"$uki_tmp" + + mv -f "$uki_tmp" "$uki_config" + echo "Listed ${#dtbs[@]} device trees in $uki_config" + fi +fi diff --git a/install/hardware/qualcomm/firmware.sh b/install/hardware/qualcomm/firmware.sh new file mode 100644 index 0000000000..9c2b6cb624 --- /dev/null +++ b/install/hardware/qualcomm/firmware.sh @@ -0,0 +1,29 @@ +# Install packaged Qualcomm firmware and vendor-signed blobs extracted from Windows. +# Keep the DSP driver disabled when starting it could disconnect the root disk. + +if omarchy-hw-qualcomm-soc; then + omarchy-pkg-add linux-firmware-qcom qcom-firmware-extract + + qcom-firmware-extract --install --no-rebuild || true + + modprobe_dir=${OMARCHY_QUALCOMM_MODPROBE_DIR:-/etc/modprobe.d} + root_source=$(findmnt -no SOURCE --nofsroot / 2>/dev/null || true) + + mkdir -p "$modprobe_dir" + if [[ -n $root_source ]] && lsblk -sno TRAN "$root_source" 2>/dev/null | grep -q '^usb$'; then + # Starting the DSPs resets USB-C, so keep them off when root is on USB. + cat >"$modprobe_dir/qualcomm-adsp-nofw.conf" <<'CONF' +# Generated by Omarchy for Qualcomm Snapdragon devices. +# Keep the DSP driver off while the root disk is attached over USB. +blacklist qcom_q6v5_pas +CONF + elif ! missing_firmware=$(qcom-firmware-extract --list-missing) || grep -q 'adsp' <<<"$missing_firmware"; then + cat >"$modprobe_dir/qualcomm-adsp-nofw.conf" <<'CONF' +# Generated by Omarchy for Qualcomm Snapdragon devices. +# Keep the DSP driver off until its signed firmware can be verified. +blacklist qcom_q6v5_pas +CONF + else + rm -f "$modprobe_dir/qualcomm-adsp-nofw.conf" + fi +fi diff --git a/install/hardware/qualcomm/kernel-params.sh b/install/hardware/qualcomm/kernel-params.sh new file mode 100644 index 0000000000..4536395f55 --- /dev/null +++ b/install/hardware/qualcomm/kernel-params.sh @@ -0,0 +1,13 @@ +# Add firmware workarounds required by current Snapdragon laptops. + +DROP_IN="/etc/limine-entry-tool.d/qualcomm-snapdragon.conf" + +if omarchy-hw-qualcomm-soc; then + mkdir -p /etc/limine-entry-tool.d + cat >"$DROP_IN" <<'CONF' +# Generated by Omarchy for Qualcomm Snapdragon devices. +# Keep display and I/O resources enabled until their drivers load. +# Disable pointer authentication and waiting for an unavailable TPM. +KERNEL_CMDLINE[default]+=" clk_ignore_unused pd_ignore_unused arm64.nopauth systemd.tpm2_wait=0" +CONF +fi diff --git a/install/hardware/vulkan.sh b/install/hardware/vulkan.sh index a2447d46ff..e13de205b8 100644 --- a/install/hardware/vulkan.sh +++ b/install/hardware/vulkan.sh @@ -15,6 +15,11 @@ for vendor in "${!VULKAN_DRIVERS[@]}"; do fi done +# Adreno GPUs are platform devices, so lspci never sees them. +if omarchy-hw-qualcomm-soc; then + PACKAGES+=(vulkan-freedreno) +fi + if (( ${#PACKAGES[@]} > 0 )); then omarchy-pkg-add "${PACKAGES[@]}" fi diff --git a/install/helpers/pacman.sh b/install/helpers/pacman.sh new file mode 100644 index 0000000000..7198695a31 --- /dev/null +++ b/install/helpers/pacman.sh @@ -0,0 +1,25 @@ +# Write repository files for both offline-install finalization and refreshes. +# Callers choose the destination so refreshes can stage files before using sudo. +pacman_write_repository_config() { + local channel=$1 config=$2 mirrorlist=$3 + local defaults="$OMARCHY_PATH/default/pacman" + + if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then + echo "Error: Invalid channel '$channel'. Must be one of: stable, rc, edge" >&2 + return 1 + fi + + if [[ $(uname -m) == "aarch64" ]]; then + cp -f "$defaults/pacman-aarch64.conf" "$config" || return 1 + cp -f "$defaults/mirrorlist-aarch64" "$mirrorlist" || return 1 + + # Only edge publishes an aarch64 Omarchy repository. Offline stable/RC + # installs retain ALARM without silently opting into an experimental channel. + if [[ $channel == "edge" ]]; then + printf "\n[omarchy]\nServer = https://pkgs.omarchy.org/edge/\$arch\n" >>"$config" || return 1 + fi + else + cp -f "$defaults/pacman-$channel.conf" "$config" || return 1 + cp -f "$defaults/mirrorlist-$channel" "$mirrorlist" || return 1 + fi +} diff --git a/install/omarchy-other.packages b/install/omarchy-other.packages index e5d56d566f..1a7be9fec7 100644 --- a/install/omarchy-other.packages +++ b/install/omarchy-other.packages @@ -75,3 +75,6 @@ t2fanrd # Framework 16 qmk-hid + +# Qualcomm Snapdragon support packages +qcom-firmware-extract diff --git a/install/post-install/pacman.sh b/install/post-install/pacman.sh index da84975b3e..97a8a1d012 100644 --- a/install/post-install/pacman.sh +++ b/install/post-install/pacman.sh @@ -1,7 +1,20 @@ -# Configure pacman after package installation completes. Offline target package -# installs use the live ISO's offline pacman.conf until this final restore. -cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf -cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist +# Replace the installer's offline pacman configuration with online repositories. +pacman_config=${OMARCHY_PACMAN_CONFIG:-/etc/pacman.conf} +mirrorlist=${OMARCHY_MIRRORLIST:-/etc/pacman.d/mirrorlist} +source "$OMARCHY_PATH/install/helpers/pacman.sh" + +if [[ $(uname -m) == "aarch64" ]]; then + # Install the keyring before replacing the offline package source. + omarchy-pkg-add archlinuxarm-keyring +fi + +pacman_write_repository_config "${OMARCHY_MIRROR:-stable}" "$pacman_config" "$mirrorlist" || return 1 + +if [[ $(uname -m) == "aarch64" ]]; then + # Trust every installed keyring before the first signed sync. + pacman-key --init + pacman-key --populate +fi # Wait for CUPS to own the file, the way omarchy-settings does, so pacman does # not turn the override into a .pacnew during ISO package installation. diff --git a/install/user/mise-work.sh b/install/user/mise-work.sh index f77d5561e0..17ea1e25df 100644 --- a/install/user/mise-work.sh +++ b/install/user/mise-work.sh @@ -19,7 +19,12 @@ case ${OMARCHY_SETUP_CONTEXT:-runtime} in esac if [[ -n $NODE_PACKAGE_DIR ]]; then - NODE_TARBALL=$(find "$NODE_PACKAGE_DIR" -name "node-v*-linux-x64.tar.gz" -type f 2>/dev/null | head -n1) + # Node names its builds x64/arm64, not by uname + case "$(uname -m)" in + aarch64) _NODE_ARCH=arm64 ;; + *) _NODE_ARCH=x64 ;; + esac + NODE_TARBALL=$(find "$NODE_PACKAGE_DIR" -name "node-v*-linux-${_NODE_ARCH}.tar.gz" -type f 2>/dev/null | head -n1) if [[ -z $NODE_TARBALL ]]; then if [[ ${OMARCHY_SETUP_CONTEXT:-} == "provision-owner" ]]; then # A factory snapshot predating the bundled tarball may not have it staged. @@ -31,7 +36,7 @@ if [[ -n $NODE_PACKAGE_DIR ]]; then exit 1 fi else - NODE_VERSION=$(basename "$NODE_TARBALL" | sed 's/node-v\(.*\)-linux-x64.tar.gz/\1/') + NODE_VERSION=$(basename "$NODE_TARBALL" | sed "s/node-v\\(.*\\)-linux-${_NODE_ARCH}.tar.gz/\\1/") NODE_INSTALL_DIR="$HOME/.local/share/mise/installs/node/$NODE_VERSION" mkdir -p "$NODE_INSTALL_DIR" diff --git a/test/shell.d/pacman-aarch64-test.sh b/test/shell.d/pacman-aarch64-test.sh new file mode 100644 index 0000000000..c878051ad2 --- /dev/null +++ b/test/shell.d/pacman-aarch64-test.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +# Each invocation has an isolated environment; exported stubs run in the child bash. +# shellcheck disable=SC2030,SC2031,SC2329 + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT +mkdir -p "$scratch/source/default" "$scratch/source/install/helpers" "$scratch/install/hardware" +cp -r "$ROOT/default/pacman" "$scratch/source/default/" +cp "$ROOT/install/helpers/pacman.sh" "$scratch/source/install/helpers/" +printf ':\n' >"$scratch/install/hardware/pacman.sh" + +run_setup() ( + architecture=$1 + export OMARCHY_MIRROR=$2 + export OMARCHY_PATH="$scratch/source" OMARCHY_INSTALL="$scratch/install" + export OMARCHY_PACMAN_CONFIG="$scratch/pacman.conf" OMARCHY_MIRRORLIST="$scratch/mirrorlist" + uname() { printf '%s\n' "$architecture"; } + omarchy-pkg-add() { printf 'add %s\n' "$*" >>"$scratch/keys"; } + pacman-key() { printf 'key %s\n' "$*" >>"$scratch/keys"; } + source "$ROOT/install/post-install/pacman.sh" +) + +run_setup aarch64 edge +grep -Fxq "Server = https://pkgs.omarchy.org/edge/\$arch" "$scratch/pacman.conf" || + fail "ARM edge installations retain the published Omarchy repository" +grep -Fxq '[alarm]' "$scratch/pacman.conf" || fail "ARM installations retain ALARM repositories" +if grep -Fxq '[multilib]' "$scratch/pacman.conf"; then + fail "ARM installations do not inherit x86 multilib" +fi +grep -Fxq 'add archlinuxarm-keyring' "$scratch/keys" || fail "ALARM keyring is installed" +grep -Fxq 'key --populate' "$scratch/keys" || fail "installed keyrings are trusted" + +for mirror in stable rc; do + run_setup aarch64 "$mirror" + if grep -Fxq '[omarchy]' "$scratch/pacman.conf"; then + fail "ARM $mirror does not select an unpublished repository or switch to edge" + fi +done + +run_setup x86_64 stable +cmp "$ROOT/default/pacman/pacman-stable.conf" "$scratch/pacman.conf" || + fail "x86 repository configuration remains unchanged" + +pass "ARM edge retains Omarchy packages without switching other channels" + +run_refresh() ( + export OMARCHY_TEST_ARCH=$1 + shift + export OMARCHY_PATH="$scratch/source" + export OMARCHY_PACMAN_CONFIG="$scratch/pacman.conf" OMARCHY_MIRRORLIST="$scratch/mirrorlist" + export OMARCHY_TEST_LOG="$scratch/refresh.log" + : >"$OMARCHY_TEST_LOG" + + uname() { printf '%s\n' "$OMARCHY_TEST_ARCH"; } + sudo() { + printf '%s\n' "$*" >>"$OMARCHY_TEST_LOG" + if [[ $1 == "cp" ]]; then + [[ ${OMARCHY_TEST_COPY_FAIL:-0} != "1" ]] || return 1 + command "$@" + elif [[ $* == "env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm" ]]; then + # Deliberately never invoke pacman from this test. + grep -Fxq 'hook pre-refresh-pacman' "$OMARCHY_TEST_LOG" || return 1 + else + return 1 + fi + } + omarchy-hook() { printf 'hook %s\n' "$*" >>"$OMARCHY_TEST_LOG"; } + export -f uname sudo omarchy-hook + bash "$ROOT/bin/omarchy-refresh-pacman" "$@" +) + +run_setup aarch64 edge +cp "$scratch/pacman.conf" "$scratch/expected-arm.conf" +cp "$scratch/mirrorlist" "$scratch/expected-arm-mirrorlist" +printf 'original config\n' >"$scratch/pacman.conf" +printf 'original mirrors\n' >"$scratch/mirrorlist" +run_refresh aarch64 edge +cmp "$scratch/expected-arm.conf" "$scratch/pacman.conf" || fail "ARM refresh matches installer repositories" +cmp "$scratch/expected-arm-mirrorlist" "$scratch/mirrorlist" || fail "ARM refresh preserves ALARM mirror layout" +grep -Fxq 'original config' "$scratch/pacman.conf.bak" || fail "refresh backs up the previous configuration" +grep -Fxq 'original mirrors' "$scratch/mirrorlist.bak" || fail "refresh backs up the previous mirrors" +grep -Fxq 'env OMARCHY_UPDATE_PACMAN=1 pacman -Syyuu --noconfirm' "$scratch/refresh.log" || + fail "refresh retains the guarded upgrade after its customization hook" +run_refresh aarch64 edge +[[ $(grep -Fxc '[omarchy]' "$scratch/pacman.conf") == "1" ]] || fail "repeated refresh does not duplicate repositories" +pass "ARM refresh shares installer selection, backs up files and remains idempotent" + +for channel in stable rc invalid ''; do + args=() + [[ -z $channel ]] || args+=("$channel") + if run_refresh aarch64 "${args[@]}" >"$scratch/error.out" 2>&1; then + fail "ARM refresh rejects unpublished or invalid channels" + fi + [[ ! -s $scratch/refresh.log ]] || fail "rejected refresh does not invoke sudo, hooks or upgrades" + cmp "$scratch/expected-arm.conf" "$scratch/pacman.conf" || fail "rejected refresh keeps the current repository" + cmp "$scratch/expected-arm-mirrorlist" "$scratch/mirrorlist" || fail "rejected refresh keeps the current mirrors" + if [[ $channel != "invalid" ]]; then + grep -Fq 'No repository configuration changed' "$scratch/error.out" || fail "unpublished channel has an actionable error" + fi +done +pass "unpublished ARM channels never remove a working repository or silently select edge" + +for channel in stable rc edge; do + run_refresh x86_64 "$channel" + cmp "$ROOT/default/pacman/pacman-$channel.conf" "$scratch/pacman.conf" || fail "x86 $channel configuration is unchanged" + cmp "$ROOT/default/pacman/mirrorlist-$channel" "$scratch/mirrorlist" || fail "x86 $channel mirrors are unchanged" +done +pass "all x86 channel templates are preserved" + +cp "$scratch/pacman.conf" "$scratch/before-error.conf" +cp "$scratch/mirrorlist" "$scratch/before-error-mirrorlist" +mv "$scratch/source/default/pacman/mirrorlist-aarch64" "$scratch/source/default/pacman/mirrorlist-aarch64.saved" +if run_refresh aarch64 edge >"$scratch/error.out" 2>&1; then + fail "refresh stops when a template cannot be staged" +fi +[[ ! -s $scratch/refresh.log ]] || fail "missing templates never reach privileged writes or an upgrade" +cmp "$scratch/before-error.conf" "$scratch/pacman.conf" || fail "staging failure preserves configuration" +cmp "$scratch/before-error-mirrorlist" "$scratch/mirrorlist" || fail "staging failure preserves mirrors" +mv "$scratch/source/default/pacman/mirrorlist-aarch64.saved" "$scratch/source/default/pacman/mirrorlist-aarch64" + +if OMARCHY_TEST_COPY_FAIL=1 run_refresh aarch64 edge >"$scratch/error.out" 2>&1; then + fail "refresh stops when a privileged copy fails" +fi +if grep -Eq 'hook|pacman -S' "$scratch/refresh.log"; then + fail "failed privileged copies never reach hooks or upgrades" +fi +pass "failed staging or privileged copies stop before a system upgrade" diff --git a/test/shell.d/snapdragon-hardware-test.sh b/test/shell.d/snapdragon-hardware-test.sh new file mode 100644 index 0000000000..7b294448a8 --- /dev/null +++ b/test/shell.d/snapdragon-hardware-test.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +firmware_setup="$ROOT/install/hardware/qualcomm/firmware.sh" +dtb_setup="$ROOT/install/hardware/qualcomm/dtb-uki.sh" +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT + +for script in "$firmware_setup" "$dtb_setup"; do + bash -n "$script" || fail "Snapdragon hardware scripts have valid syntax" +done + +( + omarchy-hw-qualcomm-soc() { return 0; } + omarchy-pkg-add() { :; } + qcom-firmware-extract() { + if [[ $1 == "--install" ]]; then + return 1 + else + return 0 + fi + } + findmnt() { + [[ $* == "-no SOURCE --nofsroot /" ]] || fail "Snapdragon firmware setup strips the Btrfs subvolume suffix" + printf '/dev/mapper/root\n' + } + lsblk() { + [[ ${!#} == "/dev/mapper/root" ]] || fail "Snapdragon firmware setup passes a resolvable root device to lsblk" + printf 'usb\n' + } + + OMARCHY_QUALCOMM_MODPROBE_DIR="$scratch/modprobe.d" + source "$firmware_setup" +) + +[[ -f $scratch/modprobe.d/qualcomm-adsp-nofw.conf ]] || + fail "Snapdragon firmware setup protects a USB-backed root disk" + +run_internal_firmware_setup() ( + omarchy-hw-qualcomm-soc() { return 0; } + omarchy-pkg-add() { :; } + findmnt() { printf '/dev/mapper/root\n'; } + lsblk() { printf 'nvme\n'; } + inspection_status=$1 + qcom-firmware-extract() { + [[ $1 == "--list-missing" ]] || return 0 + return "$inspection_status" + } + OMARCHY_QUALCOMM_MODPROBE_DIR="$scratch/modprobe.d" + source "$firmware_setup" +) + +run_internal_firmware_setup 1 +[[ -f $scratch/modprobe.d/qualcomm-adsp-nofw.conf ]] || + fail "Snapdragon setup keeps DSPs disabled when firmware inspection fails" +run_internal_firmware_setup 0 +[[ ! -f $scratch/modprobe.d/qualcomm-adsp-nofw.conf ]] || + fail "Snapdragon setup enables DSPs after firmware is verified on an internal root" + +mkdir -p "$scratch/dtbs" +: >"$scratch/dtbs/x1e80100-test.dtb" +: >"$scratch/dtbs/x1e80100-test-el2.dtb" +cat >"$scratch/uki.conf" <<'CONF' +[UKI] +SecureBootPrivateKey=/secure/db.key +PCRPrivateKey=/secure/pcr.key +CONF + +run_dtb_setup() ( + omarchy-hw-qualcomm-soc() { return 0; } + omarchy-pkg-add() { :; } + + OMARCHY_QUALCOMM_DTB_DIR="$scratch/dtbs" + OMARCHY_QUALCOMM_UKI_CONFIG="$scratch/uki.conf" + source "$dtb_setup" +) + +run_dtb_setup +first_uki_config=$(<"$scratch/uki.conf") +run_dtb_setup +[[ $(<"$scratch/uki.conf") == "$first_uki_config" ]] || + fail "Snapdragon DTB setup is idempotent" + +grep -Fq 'SecureBootPrivateKey=/secure/db.key' "$scratch/uki.conf" || + fail "Snapdragon DTB setup preserves Secure Boot settings" +grep -Fq 'PCRPrivateKey=/secure/pcr.key' "$scratch/uki.conf" || + fail "Snapdragon DTB setup preserves PCR settings" +[[ $(grep -Fc '# BEGIN OMARCHY QUALCOMM DEVICE TREES' "$scratch/uki.conf") == 1 ]] || + fail "Snapdragon DTB setup keeps one managed UKI block" +grep -Fq "DeviceTreeAuto=$scratch/dtbs/x1e80100-test.dtb" "$scratch/uki.conf" || + fail "Snapdragon DTB setup lists the matching device tree" +if grep -Fq 'x1e80100-test-el2.dtb' "$scratch/uki.conf"; then + fail "Snapdragon DTB setup excludes EL2-only device trees" +fi + +pass "Snapdragon setup tolerates missing firmware and preserves UKI settings" diff --git a/test/shell.d/yoga-slim7x-hardware-test.sh b/test/shell.d/yoga-slim7x-hardware-test.sh new file mode 100644 index 0000000000..125210b052 --- /dev/null +++ b/test/shell.d/yoga-slim7x-hardware-test.sh @@ -0,0 +1,161 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +setup="$ROOT/install/hardware/lenovo/yoga-slim7x.sh" +starter="$ROOT/install/hardware/lenovo/start-yoga-slim7x-remoteprocs.sh" +scratch=$(mktemp -d) +trap 'rm -rf "$scratch"' EXIT + +for script in "$setup" "$starter"; do + bash -n "$script" || fail "Yoga Slim 7x hardware scripts have valid syntax" +done + +matching="$scratch/matching" +mkdir -p "$matching" +printf 'qcom,x1e80100\0lenovo,yoga-slim7x\0' >"$matching/compatible" +( + omarchy-hw-qualcomm-soc() { return 0; } + omarchy-hw-match() { return 1; } + systemctl() { printf '%s\n' "$*" >>"$matching/systemctl.log"; } + + OMARCHY_YOGA_COMPATIBLE_PATH="$matching/compatible" \ + OMARCHY_YOGA_MODULES_LOAD_DIR="$matching/modules-load.d" \ + OMARCHY_YOGA_MKINITCPIO_DIR="$matching/mkinitcpio.conf.d" \ + OMARCHY_YOGA_LIMINE_CONFIG_DIR="$matching/limine-entry-tool.d" \ + OMARCHY_YOGA_SYSTEMD_DIR="$matching/systemd" \ + source "$setup" +) + +grep -Fxq 'scmi-cpufreq' "$matching/modules-load.d/yoga-slim7x.conf" || + fail "Yoga Slim 7x setup loads its SCMI CPU-frequency driver" +( + firmware_root="$scratch/firmware" + mkdir -p "$firmware_root/qcom" "$firmware_root/updates/qcom" + touch "$firmware_root/qcom/gen70500_sqe.fw.zst" \ + "$firmware_root/qcom/gen70500_gmu.bin.xz" + MODULES=() FILES=() + OMARCHY_YOGA_FIRMWARE_ROOT="$firmware_root" \ + source "$matching/mkinitcpio.conf.d/yoga-slim7x-initramfs.conf" + [[ ${MODULES[*]} == "i2c-hid-of qrtr ps883x pmic_glink_altmode" ]] || + fail "the generated config loads the keyboard and display modules" + (( ${#FILES[@]} == 2 )) || fail "GPU microcode is included without duplicating the board zap shader" + [[ ${FILES[0]} == "$firmware_root/qcom/gen70500_sqe.fw.zst" ]] || + fail "zstd display firmware is included" + [[ ${FILES[1]} == "$firmware_root/qcom/gen70500_gmu.bin.xz" ]] || + fail "xz display firmware is included" + touch "$firmware_root/updates/qcom/gen70500_sqe.fw" + FILES=() + OMARCHY_YOGA_FIRMWARE_ROOT="$firmware_root" \ + source "$matching/mkinitcpio.conf.d/yoga-slim7x-initramfs.conf" + [[ ${FILES[0]} == "$firmware_root/updates/qcom/gen70500_sqe.fw" ]] || + fail "plain extracted firmware takes precedence over compressed packaged firmware" +) +( + declare -A KERNEL_CMDLINE=([default]="root=/dev/mapper/root quiet splash") + source "$matching/limine-entry-tool.d/yoga-slim7x.conf" + [[ " ${KERNEL_CMDLINE[default]} " == *" console=tty0 "* ]] || + fail "Yoga Slim 7x uses the laptop console for graphical disk unlock" + [[ " ${KERNEL_CMDLINE[default]} " == *" initcall_blacklist=simpledrm_platform_driver_init "* ]] || + fail "Yoga Slim 7x defers display ownership to the native driver" + [[ ${KERNEL_CMDLINE[default]} == "root=/dev/mapper/root quiet splash "* ]] || + fail "Yoga Slim 7x preserves the existing boot parameters" +) +grep -Fq 'ConditionPathExists=!/etc/modprobe.d/qualcomm-adsp-nofw.conf' \ + "$matching/systemd/yoga-slim7x-remoteprocs.service" || + fail "Yoga Slim 7x skips DSP startup when the generic firmware leaf blacklists it" +if grep -Fq 'systemd-udev-settle.service' "$matching/systemd/yoga-slim7x-remoteprocs.service"; then + fail "Yoga Slim 7x DSP startup does not wait for all udev devices" +fi +grep -Fxq 'enable yoga-slim7x-remoteprocs.service' "$matching/systemctl.log" || + fail "Yoga Slim 7x enables its remote processor service" + +nonmatching="$scratch/nonmatching" +mkdir -p "$nonmatching" +printf 'qcom,x1e80100\0hp,elitebook-ultra-g1q\0' >"$nonmatching/compatible" +( + omarchy-hw-qualcomm-soc() { return 0; } + omarchy-hw-match() { return 1; } + systemctl() { fail "nonmatching Qualcomm hardware does not enable Yoga services"; } + + OMARCHY_YOGA_COMPATIBLE_PATH="$nonmatching/compatible" \ + OMARCHY_YOGA_MODULES_LOAD_DIR="$nonmatching/modules-load.d" \ + OMARCHY_YOGA_MKINITCPIO_DIR="$nonmatching/mkinitcpio.conf.d" \ + OMARCHY_YOGA_LIMINE_CONFIG_DIR="$nonmatching/limine-entry-tool.d" \ + OMARCHY_YOGA_SYSTEMD_DIR="$nonmatching/systemd" \ + source "$setup" +) + +[[ ! -e $nonmatching/modules-load.d/yoga-slim7x.conf ]] || + fail "nonmatching Qualcomm hardware does not get Yoga CPU setup" +[[ ! -e $nonmatching/mkinitcpio.conf.d/yoga-slim7x-initramfs.conf ]] || + fail "nonmatching Qualcomm hardware does not get Yoga initramfs setup" +[[ ! -e $nonmatching/limine-entry-tool.d/yoga-slim7x.conf ]] || + fail "nonmatching Qualcomm hardware does not get Yoga boot parameters" +[[ ! -e $nonmatching/systemd/yoga-slim7x-remoteprocs.service ]] || + fail "nonmatching Qualcomm hardware does not get Yoga services" + +( + omarchy-hw-qualcomm-soc() { return 0; } + omarchy-hw-match() { [[ $1 == "83ED" ]]; } + systemctl() { :; } + OMARCHY_YOGA_COMPATIBLE_PATH="$scratch/no-compatible" \ + OMARCHY_YOGA_MODULES_LOAD_DIR="$nonmatching/modules-load.d" \ + OMARCHY_YOGA_MKINITCPIO_DIR="$nonmatching/mkinitcpio.conf.d" \ + OMARCHY_YOGA_LIMINE_CONFIG_DIR="$nonmatching/limine-entry-tool.d" \ + OMARCHY_YOGA_SYSTEMD_DIR="$nonmatching/systemd" \ + source "$setup" +) +[[ -f $nonmatching/modules-load.d/yoga-slim7x.conf ]] || + fail "Lenovo DMI matching works without a device-tree compatible property" + +remoteprocs="$scratch/remoteproc" +mkdir -p "$remoteprocs/remoteproc0" "$remoteprocs/remoteproc1" "$remoteprocs/remoteproc2" +printf 'qcom/x1e80100/LENOVO/83ED/qcadsp8380.mbn\n' >"$remoteprocs/remoteproc0/firmware" +printf 'offline\n' >"$remoteprocs/remoteproc0/state" +printf 'qcom/x1e80100/LENOVO/83ED/qccdsp8380.mbn\n' >"$remoteprocs/remoteproc1/firmware" +printf 'offline\n' >"$remoteprocs/remoteproc1/state" +printf 'unrelated.mbn\n' >"$remoteprocs/remoteproc2/firmware" +printf 'offline\n' >"$remoteprocs/remoteproc2/state" + +OMARCHY_YOGA_REMOTEPROC_ROOT="$remoteprocs" \ + OMARCHY_YOGA_REMOTEPROC_ATTEMPTS=1 \ + OMARCHY_YOGA_REMOTEPROC_SLEEP=0 \ + bash "$starter" + +[[ $(<"$remoteprocs/remoteproc0/state") == start ]] || + fail "Yoga Slim 7x helper starts the audio DSP by firmware identity" +[[ $(<"$remoteprocs/remoteproc1/state") == start ]] || + fail "Yoga Slim 7x helper starts the compute DSP by firmware identity" +[[ $(<"$remoteprocs/remoteproc2/state") == offline ]] || + fail "Yoga Slim 7x helper leaves unrelated remote processors alone" + +run_starter() { + OMARCHY_YOGA_REMOTEPROC_ROOT="$remoteprocs" \ + OMARCHY_YOGA_REMOTEPROC_ATTEMPTS=1 \ + OMARCHY_YOGA_REMOTEPROC_SLEEP=0 \ + bash "$starter" +} +printf 'running\n' >"$remoteprocs/remoteproc0/state" +printf 'running\n' >"$remoteprocs/remoteproc1/state" +run_starter +[[ $(<"$remoteprocs/remoteproc0/state") == running ]] || fail "running ADSP is not restarted" +[[ $(<"$remoteprocs/remoteproc1/state") == running ]] || fail "running CDSP is not restarted" + +printf 'unrelated.mbn\n' >"$remoteprocs/remoteproc0/firmware" +if run_starter; then fail "missing ADSP must time out, even when CDSP is running"; fi +printf 'qcadsp8380.mbn\n' >"$remoteprocs/remoteproc0/firmware" +printf 'offline\n' >"$remoteprocs/remoteproc0/state" +( + printf() { + [[ $1 != "start\n" ]] || return 1 + # shellcheck disable=SC2059 # Forward the caller's format unchanged. + builtin printf "$@" + } + export -f printf + if run_starter; then fail "a failed ADSP start must be reported"; fi +) + +pass "Yoga Slim 7x adds only its board-specific keyboard, display, CPU and DSP setup"