257 lines
9.5 KiB
Bash
Executable File
257 lines
9.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# test/lint.sh
|
|
#
|
|
# Fast, offline integrity checks for the dotfiles repo.
|
|
# Does NOT require Docker or any packages to be installed beyond:
|
|
# - shellcheck (silently skipped if unavailable)
|
|
# - bash (for `bash -n` syntax checks)
|
|
#
|
|
# Run with: make test-lint
|
|
|
|
set -uo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT" || exit 1
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
say_fail() { printf 'FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
|
|
say_pass() { printf 'ok: %s\n' "$*"; PASS=$((PASS+1)); }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Collect scripts to check.
|
|
# Only repo-owned scripts; skip vendored tpm/ plugin shell scripts.
|
|
# Preferred source is git ls-files (the repo checkout); fall back to a
|
|
# filesystem scan when .git is not present (e.g. inside the docker dev
|
|
# image, where the repo is COPYied without .git). Note: git ls-files can
|
|
# also silently return nothing when the mount has dubious ownership, so the
|
|
# fallback must stay robust by excluding vendored plugin trees ('*/plugins/').
|
|
# ---------------------------------------------------------------------------
|
|
# Exclusions: never lint tpm or general vendored plugin trees, whose third-party
|
|
# scripts may legitimately fail `bash -n` and are not written/maintained here.
|
|
EXCLUDE_VENDOR='(/tpm/|/plugins/|/plugins$)'
|
|
mapfile -t SCRIPTS < <(git ls-files '*.sh' 2>/dev/null | grep -vE "$EXCLUDE_VENDOR")
|
|
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
|
|
mapfile -t SCRIPTS < <(find . -name '*.sh' \
|
|
-not -path './.git/*' -not -path '*/.git/*' \
|
|
-not -path '*/tpm/*' -not -path '*/plugins/*' \
|
|
| sort)
|
|
fi
|
|
|
|
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
|
|
say_fail "no *.sh files found"
|
|
exit 1
|
|
fi
|
|
|
|
echo "== Checking $((${#SCRIPTS[@]})) tracked shell scripts =="
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Syntax check (bash -n) - always runs, no external deps.
|
|
# ---------------------------------------------------------------------------
|
|
for s in "${SCRIPTS[@]}"; do
|
|
if bash -n "$s" 2>/dev/null; then
|
|
:
|
|
else
|
|
say_fail "bash -n $s"
|
|
fi
|
|
done
|
|
[[ $FAIL -eq 0 ]] && say_pass "bash -n syntax check passed"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. shellcheck (optional dependency).
|
|
# ---------------------------------------------------------------------------
|
|
if command -v shellcheck >/dev/null 2>&1; then
|
|
echo "== Running shellcheck =="
|
|
if shellcheck --version >/dev/null 2>&1; then
|
|
if shellcheck -S warning "${SCRIPTS[@]}" ; then
|
|
say_pass "shellcheck (warning severity)"
|
|
else
|
|
say_fail "shellcheck reported issues (warning severity)"
|
|
fi
|
|
else
|
|
say_fail "shellcheck binary present but not functional"
|
|
fi
|
|
else
|
|
echo "== shellcheck not installed; skipping (install with: pacman -S shellcheck) =="
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. packages.txt parsing sanity (mirrors setup/lib/common.sh read_packages).
|
|
# ---------------------------------------------------------------------------
|
|
echo "== Checking packages.txt markers =="
|
|
for pkgdir in term basic utils hypr kreation; do
|
|
# Setup scripts + package lists live in setup/ (outside the stow
|
|
# packages), sharing one parser in setup/lib/common.sh.
|
|
PKG_FILE="setup/$pkgdir/packages.txt"
|
|
SETUP_FILE="setup/$pkgdir/setup.sh"
|
|
if ! [[ -f "$PKG_FILE" ]]; then
|
|
say_fail "$PKG_FILE missing"
|
|
continue
|
|
fi
|
|
if grep -q '^@pacman' "$PKG_FILE"; then
|
|
say_pass "$PKG_FILE declares @pacman section"
|
|
else
|
|
say_fail "$PKG_FILE missing @pacman marker"
|
|
fi
|
|
# every @aur/@npm marker must be matched by a real consumer: either the
|
|
# thin setup.sh itself or the shared setup/lib/common.sh it sources.
|
|
for marker in '@aur' '@npm'; do
|
|
if grep -q "^$marker" "$PKG_FILE"; then
|
|
if grep -q "$marker" "$SETUP_FILE" || grep -q "$marker" "setup/lib/common.sh"; then
|
|
say_pass "$PKG_FILE $marker handled by $SETUP_FILE (via common.sh)"
|
|
else
|
|
say_fail "$PKG_FILE uses $marker but neither $SETUP_FILE nor setup/lib/common.sh handles it"
|
|
fi
|
|
else
|
|
say_pass "$PKG_FILE has no $marker section"
|
|
fi
|
|
done
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Keep Caelestia on the upstream AUR Quickshell runtime.
|
|
# ---------------------------------------------------------------------------
|
|
echo "== Checking Quickshell package selection =="
|
|
PACKAGE_FILES=()
|
|
for pkgdir in term basic utils hypr kreation; do
|
|
PACKAGE_FILES+=("setup/$pkgdir/packages.txt")
|
|
done
|
|
|
|
has_package() {
|
|
local target="$1" file
|
|
shift
|
|
for file in "$@"; do
|
|
if awk -v target="$target" '
|
|
{
|
|
sub(/#.*/, "")
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
|
|
sub(/^[^/]+\//, "")
|
|
}
|
|
$0 == target { found = 1 }
|
|
END { exit(found ? 0 : 1) }
|
|
' "$file"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
has_aur_package() {
|
|
local target="$1" file
|
|
shift
|
|
for file in "$@"; do
|
|
if awk -v target="$target" '
|
|
{
|
|
sub(/#.*/, "")
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
|
|
}
|
|
$0 == "@aur" { in_aur = 1; next }
|
|
/^@/ { in_aur = 0; next }
|
|
in_aur && $0 == target { found = 1 }
|
|
END { exit(found ? 0 : 1) }
|
|
' "$file"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
if has_package quickshell "${PACKAGE_FILES[@]}" && has_package quickshell-git "${PACKAGE_FILES[@]}"; then
|
|
say_fail "package lists declare both quickshell and quickshell-git"
|
|
else
|
|
say_pass "no mutually exclusive quickshell packages declared"
|
|
fi
|
|
|
|
if has_package noctalia-qs "${PACKAGE_FILES[@]}"; then
|
|
say_fail "package lists must not install noctalia-qs for Caelestia"
|
|
else
|
|
say_pass "incompatible noctalia-qs provider is not declared"
|
|
fi
|
|
|
|
if has_aur_package aur/quickshell-git "${PACKAGE_FILES[@]}"; then
|
|
say_pass "quickshell-git is explicitly selected from the AUR"
|
|
else
|
|
say_fail "quickshell-git must use the explicit aur/quickshell-git target"
|
|
fi
|
|
|
|
if awk '
|
|
/^[[:space:]]*yay[[:space:]]+-S/ && /--aur/ && /--needed/ { found = 1 }
|
|
END { exit(found ? 0 : 1) }
|
|
' setup/lib/common.sh; then
|
|
say_pass "AUR package installation uses yay --aur mode"
|
|
else
|
|
say_fail "AUR package installation does not use yay --aur mode"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Makefile <-> setup script name consistency.
|
|
# Catches typos such as `setup/basic/setuo.sh`
|
|
# (missing '/') in Makefile targets.
|
|
# ---------------------------------------------------------------------------
|
|
echo "== Checking Makefile references setup scripts that exist =="
|
|
# Pull out every `setup/...sh` token mentioned in the Makefile.
|
|
while IFS= read -r tok; do
|
|
[[ -z "$tok" ]] && continue
|
|
if [[ -e "$REPO_ROOT/$tok" ]]; then
|
|
say_pass "Makefile references existing script: $tok"
|
|
else
|
|
say_fail "Makefile references missing script: $tok"
|
|
fi
|
|
done < <(grep -P '^\t' Makefile | grep -oE 'setup/[_A-Za-z0-9./-]+\.sh' | sort -u)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Makefile <-> stow package name consistency.
|
|
# Catches bugs such as `stow nvim` when the actual package dir is `term/`.
|
|
# ---------------------------------------------------------------------------
|
|
echo "== Checking Makefile stow targets reference real package dirs =="
|
|
# A 'package' is any tracked top-level directory (a .config/ subtree is stowable).
|
|
have_pkg() {
|
|
local name="$1"
|
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
git ls-files 2>/dev/null | awk -F/ 'NF>1 {print $1}' | sort -u | grep -qx "$name"
|
|
else
|
|
# git-less checkout (e.g. docker dev image): fall back to filesystem.
|
|
# Only verify the dir exists; "is it stowable" is validated by stow
|
|
# itself during the smoke test.
|
|
[[ -d "$name" ]]
|
|
fi
|
|
}
|
|
|
|
while IFS= read -r pkg; do
|
|
[[ -z "$pkg" ]] && continue
|
|
case "$pkg" in
|
|
\#*|"") continue ;;
|
|
esac
|
|
if have_pkg "$pkg" && [[ -d "$REPO_ROOT/$pkg" ]]; then
|
|
say_pass "Makefile stows existing package: $pkg"
|
|
else
|
|
say_fail "Makefile stows non-existent package: $pkg"
|
|
fi
|
|
done < <(grep -P '^\t' Makefile | grep -E '\bstow\b' | awk '{print $NF}' | sort -u)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Central theme drift guard: no hardcoded theme colors outside
|
|
# basic/.config/themes (plus declared mirrors). See
|
|
# basic/.config/themes/scripts/sync-theme.sh.
|
|
# ---------------------------------------------------------------------------
|
|
echo "== Checking central theme has no hardcoded copies =="
|
|
if [[ -f "basic/.config/themes/scripts/sync-theme.sh" ]]; then
|
|
if bash basic/.config/themes/scripts/sync-theme.sh; then
|
|
say_pass "central theme drift guard"
|
|
else
|
|
say_fail "central theme drift guard"
|
|
fi
|
|
else
|
|
say_fail "basic/.config/themes/scripts/sync-theme.sh missing"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "=============================================="
|
|
echo "lint.sh: $PASS passed, $FAIL failed"
|
|
echo "=============================================="
|
|
[[ $FAIL -eq 0 ]]
|