Compare commits

..
4 Commits
Author SHA1 Message Date
mathias2784 aaecbd8472 test suite with docker container and linter 2026-09-05 20:57:59 +02:00
mathias2784 d32a30f39e readme 2026-09-05 12:59:50 +02:00
mathias2784 4e31a23470 term: rework of setup script 2026-09-05 12:59:33 +02:00
mathias2784 7c8d0eff4b term: some cleaning up 2026-09-05 12:40:47 +02:00
14 changed files with 1107 additions and 215 deletions
+9
View File
@@ -0,0 +1,9 @@
# Keep image builds light: the runtime mounts the live repo via -v.
# NOTE: do NOT exclude test/ - the dev stage COPYs the repo and then runs
# test/lint.sh from it.
.git
**/node_modules
**/lazy-lock.json
**/fish_variables
kreation/.vst3
term/.config/tmux/plugins
+3 -3
View File
@@ -7,15 +7,15 @@ Only entries that would be missed without explicit help are included.
## 1. Project Scope ## 1. Project Scope
- **Dotfiles** for a personal workstation (hyprland + Neovim + VST plugin). - **Dotfiles** for a personal workstation (hyprland + Neovim + VST plugin).
- No tests, CI or build scripts the repo is purely configuration data. - No CI or build scripts the repo is purely configuration data; the only executable content is the `install_*` scripts and the `test/` harness.
- The only executable content is the `install_*` shell scripts that install system packages. - **Testing** lives in `test/`. Run `make test-lint` / `make test-stow` (offline, no Docker) or `make test-smoke` (Docker-based, see `test/Dockerfile` and `README.md → Testing`). The harness currently surfaces pre-existing Makefile bugs by design (missing `/` in `install_*` script paths, `stow nvim` with no `nvim/` package).
--- ---
## 2. System Setup ## 2. System Setup
| Task | Command | Why it matters | | Task | Command | Why it matters |
|------|---------|----------------| |------|---------|----------------|
| **Install required programs** | `bash .install_skripte/install_terminal_pcks.sh` <br>`bash .install_skripte/install_basic_pcks.sh` <br>`bash .install_skripte/install_hypr_pcks.sh` <br>`bash .install_skripte/install_kreation_pcks.sh` | These scripts install Neovim, hyprland, VST runtime, and other utilities. | | **Install required programs** | `bash term/setup.sh` <br>`bash _install_skripte/install_basic_pcks.sh` <br>`bash _install_skripte/install_hypr_pcks.sh` <br>`bash _install_skripte/install_kreation_pcks.sh` | These scripts install Neovim, hyprland, VST runtime, and other utilities. Edit `term/packages.txt` to add/remove terminal packages. |
| **Link configuration directories** | `ln -s ~/.dotfiles/hypr/.config/hypr ~/.config/hypr` <br>`ln -s ~/.dotfiles/term/.config/nvim ~/.config/nvim` | The repository expects the configs to live in `~/.config`. | | **Link configuration directories** | `ln -s ~/.dotfiles/hypr/.config/hypr ~/.config/hypr` <br>`ln -s ~/.dotfiles/term/.config/nvim ~/.config/nvim` | The repository expects the configs to live in `~/.config`. |
| **Add VST plugin** | Copy `kreation/.vst3/MT-PowerDrumKit.vst3` into your DAWs plugins folder (usually `~/.vst`). | The repo contains a prebuilt VST for use in audio software. | | **Add VST plugin** | Copy `kreation/.vst3/MT-PowerDrumKit.vst3` into your DAWs plugins folder (usually `~/.vst`). | The repo contains a prebuilt VST for use in audio software. |
+43 -2
View File
@@ -1,8 +1,8 @@
# setup ist ein Kommando-Target (keine Datei) und soll immer ausgeführt werden. # setup ist ein Kommando-Target (keine Datei) und soll immer ausgeführt werden.
.PHONY: basic .PHONY: basic
termn: term:
bash _install_skripte_install_terminal_pcks.sh bash term/setup.sh
stow term stow term
basic: term basic: term
bash _install_skripte_install_basic_pcks.sh bash _install_skripte_install_basic_pcks.sh
@@ -19,3 +19,44 @@ kreation: basic
stow kreation stow kreation
bunters-monster: basic hypr kreation bunters-monster: basic hypr kreation
_install_skripte/buntes-monster-links.sh _install_skripte/buntes-monster-links.sh
# ---------------------------------------------------------------------------
# Testing (see README.md "Testing" and test/ for details)
# ---------------------------------------------------------------------------
.PHONY: test test-lint test-smoke test-stow test-shellcheck test-lint-docker
# Full test suite: fast offline lint first, then the optional Docker smoke.
test: test-lint test-smoke
# Fast, offline integrity checks. No Docker required.
# shellcheck is used if installed, otherwise only syntax checks run.
test-lint:
bash test/lint.sh
# Check stow package resolution without creating symlinks.
test-stow:
bash test/probe_stow.sh
# Run shellcheck over all tracked repo scripts (needs shellcheck installed).
test-shellcheck:
shellcheck -S warning $$(git ls-files '*.sh' | grep -v '/tpm/')
# Test shellcheck: build the /dev stage with shellcheck and run lint.sh.
# docker build context is the repo root; .dockerignore excludes heavy dirs.
test-lint-docker:
docker build --target dev -t dotfiles-lint -f test/Dockerfile .
docker run --rm dotfiles-lint
# Docker smoke test: builds the Arch container and runs the dry probes.
# RUN_SMOKE=1 also installs the real pacman packages inside the container.
# RUN_SMOKE=1 RUN_FULL=1 additionally attempts AUR/npm + hypr/kreation.
# SKIP_NET=1 skips anything that contacts the network.
# Example: make test-smoke RUN_SMOKE=1
test-smoke:
docker build --target test -t dotfiles-test -f test/Dockerfile .
docker run --rm \
-e RUN_SMOKE="$${RUN_SMOKE:-0}" \
-e RUN_FULL="$${RUN_FULL:-0}" \
-e SKIP_NET=$${SKIP_NET:-0} \
-v "$$PWD":/dotfiles \
dotfiles-test
+182
View File
@@ -0,0 +1,182 @@
# Dotfiles
Personal configuration files for an Arch Linux workstation (Hyprland + Neovim + VST plugin), managed with [GNU Stow](https://www.gnu.org/software/stow/).
## Layout
| Directory | Contents |
|-----------|----------|
| `term/` | Fish + kitty + starship + nvim + tmux + yazi + fastfetch + opencode terminal environment |
| `hypr/` | Hyprland compositor config, kitty theme, wpaperd wallpapers |
| `kreation/`| VST plugin (`MT-PowerDrumKit.vst3`) |
| `local_ai/`| Local AI / MCP tooling config |
| `_install_skripte/` | Miscellaneous install helper scripts |
| `test/` | Docker test harness (lint + smoke tests) |
Each top-level directory is a **stow package**. `stow` symlinks the files from the repository into your home directory (e.g. `term/.config/...``~/.config/...`).
## Prerequisites
- Arch Linux (or a distro with `pacman`)
- [`stow`](https://www.gnu.org/software/stow/)
- Relies on `git`, `sudo`, `curl`
## Quick start
```bash
# 1. Clone the repo
git clone https://github.com/<user>/dotfiles.git ~/.dotfiles
cd ~/.dotfiles
# 2. Install a target and symlink its files
make term # terminal + fish + kitty + nvim
make hypr # Hyprland (also pulls in basic + term)
make kreation # VST plugin (also pulls in basic)
```
Run `make <target>` from the repository root. Each target first runs the relevant install script and then `stow`s the package.
> The install scripts require `sudo` and may prompt for your password.
## Targets
| Target | What it does |
|--------|--------------|
| `make term` | Runs `term/setup.sh` (installs terminal packages, sets fish as default shell, clones tmuxifier), then `stow term` |
| `make basic` | Runs `install_basic_pcks.sh`, stows `nvim`, runs `nextcloud.sh` |
| `make hypr` | `make basic`, installs Hyprland packages, stows `hypr`, runs the Hyprland setup scripts (`ensure-env.sh`, `portal-host-override.sh`, `fix_dolphin_file_associations.sh`) |
| `make kreation` | `make basic`, installs VST runtime, stows `kreation` |
| `make buntes-monster` | Everything (`basic` + `hypr` + `kreation`), then wires up symlinks |
### Managing terminal packages
The terminal package list lives in a single editable file:
```
term/packages.txt
```
Add/remove packages on separate lines. Lines starting with `#` are ignored, and you can switch between package sources with section markers:
```
@pacman # install via pacman (default)
@aur # install via yay (AUR)
@npm # install via npm -g
```
The default section is `@pacman`. Any AUR dependency (`yay`) is bootstrapped automatically by `term/setup.sh`.
## What the install scripts do
- **`term/setup.sh`** enables multilib, updates the system, installs `yay` if missing, reads `term/packages.txt` and installs all packages with `--needed` (skips already-installed ones), sets `fish` as the default shell (`chsh -s /usr/bin/fish`), clones `tmuxifier`, adds the `git` yazi plugin, and runs `sudo updatedb`. It is idempotent and safe to re-run.
## Stow: resolving conflicts with existing paths
`stow` refuses to create a symlink where a real file or directory already exists on the target (your home directory). When you first set up a machine that already has configs in `~/.config`, you typically see an error like:
```
WARNING: stowing term would cause conflicts:
* existing target is neither a link nor a directory: .config/fish/config.fish
```
The **recommended** workflow is to back up the affected paths and remove them from the target location so stow can take over, then restore anything you want to keep as an overlay on top of the symlink. There are a few common approaches:
### 1. Back up and remove (recommended for first-time setup)
Point stow elsewhere so it only *reports* conflicts without touching anything, then handle the listed paths yourself:
```bash
# Dry-run to see exactly which paths conflict
cd ~/.dotfiles
stow --verbose --simulate term
# Inspect the conflicts, then back them up on the target side
mkdir -p ~/.config-backup
mv ~/.config/fish ~/.config-backup/fish # example — repeat for each conflicting path
# Now stow can create the links
stow term
```
Afterwards you can restore your old configs **on top of** the symlinks if you want a custom overlay:
```bash
cp -r ~/.config-backup/fish/* ~/.config/fish/
```
> The backup dir lives outside the stow packages, so stow never touches it.
### 2. Use `--adopt` with care
```bash
git stash # only if you have uncommitted repo changes you want to keep
cd ~/.dotfiles
stow --adopt term
git checkout . # discard any files that stow pulled into the repo
```
`stow --adopt` moves conflicting files *from your home dir into the repository* (overwriting what's stored there), then creates the symlink. This is convenient but **destructive to the repo copy** of those files — always review with a simulate run first and reset via git afterwards if you didn't intend to change the tracked config. Prefer approach #1 unless you deliberately want to recapture your live config into the repo.
### 3. Skip conflicts you don't manage in the repo
If a path simply shouldn't be managed by stow, use `--ignore`:
```bash
stow --ignore='\.config/(local|misc)' term
```
---
## Testing
The `test/` directory contains a Docker-based test harness that verifies the
install scripts, the `Makefile`, and the stow packages.
There are **two tiers**:
| Target | What it does | Docker required? |
|--------|--------------|------------------|
| `make test-lint` | Fast, offline integrity checks: `bash -n` syntax pass over every script, `shellcheck` (if installed), `packages.txt` marker validation, and Makefile ↔ script/stow-package name consistency. Catches typos like `_install_skripte_install_basic_pcks.sh` (missing `/`). | No |
| `make test-stow` | `stow --simulate` for every stow-able package; fails if a package the Makefile stows doesn't exist (e.g. `stow nvim` — nvim lives under `term/`). | No |
| `make test-shellcheck` | Runs shellcheck over all tracked scripts (shellcheck must be installed). | No |
| `make test-lint-docker` | Builds the `/dev` stage (`archlinux:latest` + shellcheck) and runs `test/lint.sh` inside it. | Yes |
| `make test-smoke` | Builds the `/test` stage and runs the container's dry probes: `make -n` over every target, `stow --simulate` over every Makefile-stowed package, plus `test/lint.sh`. A network-enabled `RUN_SMOKE=1` variant installs the real pacman packages. | Yes |
| `make test` | `test-lint` + `test-smoke`. | Partially |
### Docker smoke details
```bash
make test-smoke # dry probes only (fast, no installs)
make test-smoke RUN_SMOKE=1 # + real pacman installs of the term packages
make test-smoke RUN_SMOKE=1 RUN_FULL=1 # + AUR/npm + hypr/kreation/buntes-monster
make test-smoke SKIP_NET=1 # skip anything that contacts the network
```
The container:
- uses `archlinux:latest` (the scripts assume Arch + pacman),
- creates a non-root user (`mathias`, uid 1000) with passwordless sudo so the
scripts don't hang on a password prompt,
- mounts the live repo at `/dotfiles` (`-v "$PWD":/dotfiles`), so the tests
always run against your current checkout,
- stubs host daemon commands (`systemctl`, `updatedb`, `kbuildsycoca6`,
`dbus-update-activation-environment`) as no-ops so nothing talks to a real
systemd/dbus inside the container.
In the `RUN_SMOKE=1` install path, the harness additionally **verifies the
installed packages** (`test/verify_packages.sh`):
- every `@pacman` entry is confirmed via `pacman -Q <pkg>` (covers fonts,
libs and python modules that ship no binary),
- a curated mapping probes each package's binary with a real
`--version`-style call (e.g. `fish --version`, `nvim --version`,
`ssh -V`), failing if the binary is missing or does not report a version.
> **Note for the current repo state:** `make test-lint` and `make test-smoke`
> currently **fail** because the harness surfaces pre-existing Makefile bugs
> (missing `/` in the `install_*` script paths and `stow nvim` for a
> non-existent `nvim/` package). That is the intended behaviour — fix the
> Makefile and the tests should go green.
---
**When in doubt:** run `stow --verbose --simulate <target>` first — it prints what would happen (linking) without changing anything. Never `rm -rf` the `~/.config` of something you still want to keep; move it to a backup directory instead.
-73
View File
@@ -1,73 +0,0 @@
#!/bin/bash
# multilib aktivieren in /etc/pacman.conf
sudo sed -i.bak '/^#\[multilib\]/,+1 s/^#//' /etc/pacman.conf
# Für Noobs wie mich
# - erstellt Sicherheitskopie pacman.conf.bak vor der Änderung
# - sucht nach "[multilib]"
# - bezieht die nächste Zeile mit ein
# - Ersetzt ^# mit nüscht -> Kommentar entfernt :-)
sudo pacman -Syu
# Install yay
if pacman -Q yay &>/dev/null; then
echo "yay bereits installiert; fahre fort."
elif pacman -Si yay &>/dev/null; then
echo "Installiere yay mit pacman ..."
sudo pacman -S --needed yay
else
sudo pacman -S --needed base-devel git
echo "installiere yay via git ..."
git clone https://aur.archlinux.org/yay.git
(
cd yay || {
echo "Fehler: CD in yay-Verzeichnis fehlgeschlagen";
exit 1;
}
makepkg -si
)
rm -rf yay
fi
sudo pacman -S --needed \
fish \
vim \
nvim \
openssh \
rclone \
rsync \
unison \
stow \
nodejs \
npm \
starship \
fastfetch \
tree-sitter-cli \
python \
python-pip \
extra/ttf-jetbrains-mono-nerd \
unzip \
ruff \
python-ruff \
python-debugpy \
chromium \
selene \
opencode \
tmux \
yazi \
zoxide \
fzf \
locate \
ttf-jetbrains-mono-nerd \
ttf-font-awesome
# fish as standard terminal
chsh -s /usr/bin/fish
git clone https://github.com/jimeh/tmuxifier.git ~/.tmuxifier
npm install -g vscode-js-debug
npm install -g eslint_d
ya pkg add yazi-rs/plugins:git
sudo updatedb
-1
View File
@@ -37,4 +37,3 @@ fi
export DATAFLEX_BACKEND_REPO="/c/_Master/202602 DataFleX/repos/flexibility_platform_backend/" export DATAFLEX_BACKEND_REPO="/c/_Master/202602 DataFleX/repos/flexibility_platform_backend/"
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
. "$HOME/.cargo/env"
@@ -1,136 +0,0 @@
function fish_prompt
# This prompt shows:
# - green lines if the last return command is OK, red otherwise
# - your user name, in red if root or yellow otherwise
# - your hostname, in cyan if ssh or blue otherwise
# - the current path (with prompt_pwd)
# - date +%X
# - the current virtual environment, if any
# - the current git status, if any, with fish_git_prompt
# - the current battery state, if any, and if your power cable is unplugged, and if you have "acpi"
# - current background jobs, if any
# It goes from:
# ┬─[nim@Hattori:~]─[11:39:00]
# ╰─>$ echo here
# To:
# ┬─[nim@Hattori:~/w/dashboard]─[11:37:14]─[V:django20]─[G:master↑1|●1✚1…1]─[B:85%, 05:41:42 remaining]
# │ 2 15054 0% arrêtée sleep 100000
# │ 1 15048 0% arrêtée sleep 100000
# ╰─>$ echo there
set -l retc red
test $status = 0; and set retc green
set -q __fish_git_prompt_showupstream
or set -g __fish_git_prompt_showupstream auto
function _nim_prompt_wrapper
set retc $argv[1]
set -l field_name $argv[2]
set -l field_value $argv[3]
set_color normal
set_color $retc
echo -n '─'
set_color -o green
echo -n '['
set_color normal
test -n $field_name
and echo -n $field_name:
set_color $retc
echo -n $field_value
set_color -o green
echo -n ']'
end
set_color $retc
echo -n '┬─'
set_color -o green
echo -n [
if functions -q fish_is_root_user; and fish_is_root_user
set_color -o red
else
set_color -o yellow
end
echo -n $USER
set_color -o white
echo -n @
if test -z "$SSH_CLIENT"
set_color -o blue
else
set_color -o cyan
end
echo -n (prompt_hostname)
set_color -o white
echo -n :(prompt_pwd)
set_color -o green
echo -n ']'
# Date
_nim_prompt_wrapper $retc '' (date +%X)
# Vi-mode
if test "$fish_key_bindings" = fish_vi_key_bindings
or test "$fish_key_bindings" = fish_hybrid_key_bindings
set -l mode
switch $fish_bind_mode
case default
set mode (set_color --bold red)N
case operator f F t T
set mode (set_color --bold cyan)N
case insert
set mode (set_color --bold green)I
case replace_one
set mode (set_color --bold green)R
case replace
set mode (set_color --bold cyan)R
case visual
set mode (set_color --bold magenta)V
end
set mode $mode(set_color normal)
_nim_prompt_wrapper $retc '' $mode
end
# Virtual Environment
set -q VIRTUAL_ENV_DISABLE_PROMPT
or set -g VIRTUAL_ENV_DISABLE_PROMPT true
set -q VIRTUAL_ENV
and _nim_prompt_wrapper $retc V (path basename "$VIRTUAL_ENV")
# git
set -l prompt_git (fish_git_prompt '%s')
test -n "$prompt_git"
and _nim_prompt_wrapper $retc G $prompt_git
# Battery status
type -q acpi
and acpi -a 2>/dev/null | string match -rq off
and _nim_prompt_wrapper $retc B (acpi -b | cut -d' ' -f 4-)
# New line
echo
# Background jobs
set_color normal
for job in (jobs)
set_color $retc
echo -n '│ '
set_color brown
echo $job
end
set_color normal
set_color $retc
echo -n '╰─>'
set_color -o red
echo -n '$ '
set_color normal
end
+46
View File
@@ -0,0 +1,46 @@
# ---------------------------------------------------------------------------
# Terminal package list for term/setup.sh
#
# Maintenance:
# - Add / remove packages on separate lines.
# - Lines starting with '#' and blank lines are ignored.
# - Use "@pacman", "@aur" or "@npm" markers to switch section.
# - The default section is @pacman.
# ---------------------------------------------------------------------------
@pacman
fish
extra/kitty
neovim
openssh
rclone
rsync
unison
stow
nodejs
npm
starship
fastfetch
tree-sitter-cli
python
python-pip
unzip
ruff
python-ruff
python-debugpy
chromium
selene
tmux
yazi
zoxide
fzf
locate
ttf-jetbrains-mono-nerd
ttf-font-awesome
@aur
opencode
@npm
eslint_d
vscode-js-debug
Executable
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGES_FILE="$SCRIPT_DIR/packages.txt"
# ---------------------------------------------------------------------------
# Enable multilib in /etc/pacman.conf (with a safety backup)
# ---------------------------------------------------------------------------
enable_multilib() {
sudo sed -i.bak '/^#\[multilib\]/,+1 s/^#//' /etc/pacman.conf
}
# ---------------------------------------------------------------------------
# Ensure yay (AUR helper) is installed.
# ---------------------------------------------------------------------------
install_yay() {
if pacman -Q yay &>/dev/null; then
echo "yay bereits installiert; fahre fort."
return
fi
if pacman -Si yay &>/dev/null; then
echo "Installiere yay mit pacman ..."
sudo pacman -S --needed yay
return
fi
echo "installiere yay via git ..."
sudo pacman -S --needed base-devel git
local tmp
tmp="$(mktemp -d)"
git clone https://aur.archlinux.org/yay.git "$tmp/yay"
(
cd "$tmp/yay"
makepkg -si
)
rm -rf "$tmp"
}
# ---------------------------------------------------------------------------
# Read packages.txt, split into pacman / aur / npm sections.
# ---------------------------------------------------------------------------
read_packages() {
local section="pacman"
local current
current=""
while IFS= read -r line || [[ -n "$line" ]]; do
current="${line%%#*}"
current="${current##*( )}"
current="${current%%*( )}"
[[ -z "$current" ]] && continue
case "$current" in
@pacman) section="pacman" ;;
@aur) section="aur" ;;
@npm) section="npm" ;;
*)
case "$section" in
pacman) pacman_pkgs+=("$current") ;;
aur) aur_pkgs+=("$current") ;;
npm) npm_pkgs+=("$current") ;;
esac
;;
esac
done < "$PACKAGES_FILE"
}
declare -a pacman_pkgs=() aur_pkgs=() npm_pkgs=()
# ---------------------------------------------------------------------------
# Install sections.
# ---------------------------------------------------------------------------
install_pacman() {
if ((${#pacman_pkgs[@]})); then
echo "==> Installiere Pacman-Pakete: ${pacman_pkgs[*]}"
yay -S --needed "${pacman_pkgs[@]}"
fi
}
install_aur() {
if ((${#aur_pkgs[@]})); then
echo "==> Installiere AUR-Pakete: ${aur_pkgs[*]}"
yay -S --needed "${aur_pkgs[@]}"
fi
}
install_npm() {
if ((${#npm_pkgs[@]})); then
echo "==> Installiere globale npm-Pakete: ${npm_pkgs[*]}"
npm install -g "${npm_pkgs[@]}"
fi
}
# ---------------------------------------------------------------------------
# Post-install configuration.
# ---------------------------------------------------------------------------
set_fish_default_shell() {
if command -v fish >/dev/null 2>&1; then
if [[ "$SHELL" != "/usr/bin/fish" ]]; then
echo "Setze fish als Standard-Shell ..."
chsh -s /usr/bin/fish
else
echo "fish ist bereits die Standard-Shell."
fi
else
echo "Warnung: fish wurde nicht installiert; ueberspringe chsh."
fi
}
install_tmuxifier() {
if [[ -d "$HOME/.tmuxifier" ]]; then
echo "tmuxifier bereits installiert; fahre fort."
else
echo "Klone tmuxifier ..."
git clone https://github.com/jimeh/tmuxifier.git "$HOME/.tmuxifier"
fi
}
add_yazi_git_plugin() {
if command -v yazi >/dev/null 2>&1 && command -v ya >/dev/null 2>&1; then
echo "Fuege yazi git-Plugin hinzu ..."
ya pkg add yazi-rs/plugins:git
fi
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
echo "==> Aktiviere multilib ..."
enable_multilib
echo "==> System aktualisieren ..."
sudo pacman -Syu
echo "==> Sicherstelle yay ..."
install_yay
echo "==> Lese Paketliste: $PACKAGES_FILE"
read_packages
install_pacman
install_aur
install_npm
set_fish_default_shell
install_tmuxifier
add_yazi_git_plugin
echo "==> Aktualisiere locate-Datenbank ..."
sudo updatedb
echo "Fertig. Starte ein neues Terminal, damit fish und die Umgebung greifen."
}
main
+73
View File
@@ -0,0 +1,73 @@
# test/Dockerfile
#
# Docker test harness for the dotfiles repo.
#
# Two stages:
# --target dev : fast shellcheck/lint environment (no heavy installs)
# --target test : smoke-test environment that can run the install scripts
#
# Build with (repo root as context; .dockerignore trims heavy/binary dirs):
# docker build --target test -t dotfiles-test -f test/Dockerfile .
# Run with:
# docker run --rm -v "$PWD":/dotfiles dotfiles-test
#
# Or simply use the Makefile targets:
# make test-lint-docker # build dev stage, run shellcheck+lint
# make test-smoke # dry probes; add RUN_SMOKE=1 for real installs
#
# See README.md ("Testing") and test/entrypoint-test.sh for details.
FROM archlinux:latest AS base
# Non-root test user that mirrors the workstation account.
ARG UID_ARG=1000
ARG USERNAME=mathias
RUN pacman -Syu --noconfirm \
base-devel \
git \
stow \
curl \
which \
sudo \
&& pacman -Scc --noconfirm
RUN useradd -m -u "$UID_ARG" -s /bin/bash -G wheel "$USERNAME"
# Passwordless sudo so scripts that call `sudo pacman`, `sudo systemctl`, etc.
# do not hang on a password prompt inside the container.
RUN printf '%%wheel ALL=(ALL) NOPASSWD: ALL\n' > /etc/sudoers.d/wheel-nopasswd \
&& chmod 440 /etc/sudoers.d/wheel-nopasswd
USER "$USERNAME"
ENV HOME="/home/$USERNAME"
WORKDIR /dotfiles
# ---------------------------------------------------------------------------
# dev stage: shellcheck/lint only. Fast, no install heavy-lifting.
# ---------------------------------------------------------------------------
FROM base AS dev
USER root
RUN pacman -S --noconfirm --needed shellcheck
USER "$USERNAME"
WORKDIR /dotfiles
COPY --chown="$USERNAME" . /dotfiles
CMD ["bash", "test/lint.sh"]
# ---------------------------------------------------------------------------
# test stage: prepared to actually run the install scripts & probe stow.
# Uses the mounted repo (see -v $PWD:/dotfiles in the Makefile) so scripts
# see the live workspace checkout.
# ---------------------------------------------------------------------------
FROM base AS test
USER "$USERNAME"
WORKDIR /dotfiles
# The entrypoint drives the smoke tests. The repo is mounted at /dotfiles
# via the Makefile's `docker run -v`.
ENTRYPOINT ["bash", "/dotfiles/test/entrypoint-test.sh"]
+252
View File
@@ -0,0 +1,252 @@
#!/bin/bash
# test/entrypoint-test.sh
#
# Runs inside the docker `test` stage. The repo is mounted read-write at
# /dotfiles (see the Makefile's `docker run -v "$PWD":/dotfiles`).
#
# Two tiers:
# 1. DRY PROBES (always, fast, offline)
# - `make -n` dry-runs to catch Makefile typos / missing scripts
# - `stow --simulate` to prove every stow target resolves
# 2. REAL SMOKE (guarded by RUN_SMOKE=1)
# - exercises term/setup.sh package parsing and (optionally) installs
# - verifies stow actually creates symlinks into $HOME
# Full AUR/npm + kreation/buntes-monster only run with RUN_FULL=1.
#
# Env flags (set via `docker run -e`):
# RUN_SMOKE=1 run the real pacman-install smoke
# RUN_FULL=1 also attempt AUR/npm + hypr/kreation/buntes-monster
# SKIP_NET=1 pretend no network; skip anything that needs the mirror
set -uo pipefail
# Resolve the repo root: prefer the container's /dotfiles mount, otherwise
# fall back to the directory above this script (a local checkout).
if [[ -d /dotfiles ]]; then
cd /dotfiles || { echo "FATAL: repo not mounted at /dotfiles"; exit 1; }
else
cd "$(dirname "${BASH_SOURCE[0]}")/.." || { echo "FATAL: cannot locate repo root"; exit 1; }
fi
REPO="$(pwd)"
# Guard: both resolution paths must find a real checkout.
if [[ ! -f "$REPO/Makefile" || ! -d "$REPO/test" ]]; then
echo "FATAL: '$REPO' does not look like the dotfiles repo root." >&2
echo " In docker, run with -v \"\$PWD\":/dotfiles ." >&2
exit 1
fi
PASS=0
FAIL=0
say_ok() { printf ' ok: %s\n' "$*"; PASS=$((PASS+1)); }
say_fail(){ printf ' FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
header() { printf '\n==== %s ====\n' "$*"; }
if command -v git >/dev/null 2>&1 && [[ ! -d .git ]]; then
# when mounted from a worktree, .git may be a file; fine.
:
fi
echo "entrypoint-test.sh: running as $(id -un) in $(pwd)"
# ---------------------------------------------------------------------------
# 1. DRY PROBES - always run, cheap, offline.
# ---------------------------------------------------------------------------
header "Dry probes"
# 1a. Makefile dry runs for every target.
# Discover targets dynamically so we never drift from the Makefile.
targets=$(grep -oE '^[A-Za-z0-9_-]+:' Makefile | tr -d ':' | grep -v '^test' | sort -u)
for target in $targets; do
# `make -n` must not error out from missing scripts / dirs.
if ( cd "$REPO" && make -n "$target" >/dev/null 2>err.txt ); then
say_ok "make -n $target parses"
else
say_fail "make -n $target (typo? missing script?)"
sed 's/^/ /' err.txt 2>/dev/null
fi
rm -f err.txt
done
# 1b. stow --simulate for every package the Makefile stows.
while IFS= read -r pkg; do
[[ -z "$pkg" ]] && continue
if stow --simulate --dir="$REPO" --target="$REPO" "$pkg" 2>err.txt; then
say_ok "stow --simulate $pkg resolves"
else
say_fail "stow --simulate $pkg (non-existent package or conflict)"
sed 's/^/ /' err.txt 2>/dev/null
fi
rm -f err.txt
done < <(grep -P '^\t' Makefile | grep -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)
# 1c. run the repo's own lint + probe scripts (they're mounted here).
if [[ -x test/lint.sh ]] || [[ -f test/lint.sh ]]; then
if bash test/lint.sh >/dev/null 2>&1; then
say_ok "test/lint.sh"
else
say_fail "test/lint.sh (see logs above / rerun outside container)"
fi
else
say_fail "test/lint.sh not present in mount"
fi
header "Smoke gates"
echo " RUN_SMOKE=${RUN_SMOKE:-0} RUN_FULL=${RUN_FULL:-0} SKIP_NET=${SKIP_NET:-0}"
# ---------------------------------------------------------------------------
# 2. REAL SMOKE (RUN_SMOKE=1)
# ---------------------------------------------------------------------------
if [[ "${RUN_SMOKE:-0}" == "1" ]]; then
header "Real smoke (RUN_SMOKE=1)"
# 2a. Allow scripts to use sudo without a password (already NOPASSWD via
# the Dockerfile for the wheel group).
#
# 2b. Override host-daemon commands with no-ops so nothing tries to talk
# to systemd/dbus/dbus in the container.
mkdir -p "$HOME/bin"
cat > "$HOME/bin/systemctl" <<'NOOP'
#!/bin/bash
echo "[stub] systemctl $*"
exit 0
NOOP
cat > "$HOME/bin/dbus-update-activation-environment" <<'NOOP'
#!/bin/bash
echo "[stub] dbus-update-activation-environment $*"
exit 0
NOOP
cat > "$HOME/bin/kbuildsycoca6" <<'NOOP'
#!/bin/bash
echo "[stub] kbuildsycoca6 $*"
exit 0
NOOP
cat > "$HOME/bin/updatedb" <<'NOOP'
#!/bin/bash
echo "[stub] updatedb $*"
exit 0
NOOP
chmod +x "$HOME/bin/"*
export PATH="$HOME/bin:$PATH"
# 2c. term/setup.sh package parsing sanity (mirrors read_packages).
if ( cd "$REPO" && bash -n term/setup.sh ); then
say_ok "term/setup.sh syntax OK"
else
say_fail "term/setup.sh syntax error"
fi
pacman_count=$(awk '
/^@pacman/{s="pacman"}
/^@aur/{s="aur"}
/^@npm/{s="npm"}
/^[[:space:]]*#/{next}
/^[[:space:]]*$/{next}
s=="pacman"{pc++} s=="aur"{ac++} s=="npm"{nc++}
END{printf "%d %d %d", pc,ac,nc}
' $REPO/term/packages.txt)
read -r PC AC NC <<< "$pacman_count"
say_ok "packages.txt -> ${PC} pacman, ${AC} aur, ${NC} npm entries"
# 2d. If network allowed and not skipped, actually install the pacman
# section of term/setup.sh (mirrors `make term` without AUR/npm).
if [[ "${SKIP_NET:-0}" != "1" ]]; then
header "make term (pacman packages, AUR/npm skipped)"
# Patch packages.txt to drop @aur/@npm so we don't hit AUR/npm.
cp "$REPO/term/packages.txt" "$HOME/packages.txt.full"
awk '
/^@aur$/{s="skip"}; /^@npm$/{s="skip"}; /^@pacman$/{s="pacman"}
{ if (s!="skip" && NF>0 && $1 !~ /^#/) print }
' "$REPO/term/packages.txt" > "$HOME/packages.txt.term-only"
# set a scratch HOME for setup.sh so it doesn't touch the real one.
mkdir -p "$HOME/term"
cp "$REPO/term/setup.sh" "$HOME/term/setup.sh"
cp "$HOME/packages.txt.term-only" "$HOME/term/packages.txt"
# chsh is stubbed via $HOME/bin so even `chsh` works without a pty.
setup_ok=0
if ( cd "$HOME/term" && bash setup.sh ) ; then
say_ok "term/setup.sh (term-only) completed"
setup_ok=1
else
say_fail "term/setup.sh (term-only) failed (exit $?)"
fi
# 2d2. Verify that every package is really installed and its binary
# runs. Only meaningful if the install itself succeeded.
if [[ "$setup_ok" == "1" ]]; then
if [[ -x test/verify_packages.sh ]]; then
header "verify installed packages"
if bash test/verify_packages.sh "$HOME/packages.txt.term-only"; then
say_ok "verify_packages.sh: all packages verified"
else
say_fail "verify_packages.sh: one or more packages failed verification (see above)"
fi
else
say_fail "test/verify_packages.sh missing"
fi
else
echo " skipping package verification (install did not complete)"
fi
else
echo " SKIP_NET=1 -> skipping real package install and verification"
fi
# 2e. stow --actual into $HOME to prove real symlinks are created.
header "stow real symlinks into \$HOME"
for pkg in term hypr kreation local_ai; do
if [[ -d "$REPO/$pkg" ]]; then
if stow --dir="$REPO" --target="$HOME" "$pkg" 2>err.txt; then
say_ok "stow $pkg into \$HOME"
else
say_fail "stow $pkg into \$HOME"
sed 's/^/ /' err.txt 2>/dev/null
fi
rm -f err.txt
else
say_fail "package dir missing: $pkg"
fi
done
# sanity: representative symlinks must now exist in $HOME.
missing=""
for link in .config/fish .config/hypr .vst3/MT-PowerDrumKit.vst3; do
[[ -e "$HOME/$link" ]] || missing="$missing $link"
done
if [[ -z "$missing" ]]; then
say_ok "stow created representative \$HOME symlinks"
else
say_fail "missing expected stow symlinks:$missing"
fi
# 2f. kreation package: verify the VST path is stow-able and present.
header "kreation VST"
if [[ -f "$REPO/kreation/.vst3/MT-PowerDrumKit.vst3/Contents/x86_64-linux/MT-PowerDrumKit.so" ]]; then
say_ok "MT-PowerDrumKit.vst3 binary present"
else
say_fail "MT-PowerDrumKit.vst3 binary missing"
fi
else
echo " RUN_SMOKE not set -> skipping real installs & symlink creation."
fi
# ---------------------------------------------------------------------------
# 3. FULL (RUN_FULL=1) - opt-in heavy path (AUR/npm/hypr/kreation)
# ---------------------------------------------------------------------------
if [[ "${RUN_FULL:-0}" == "1" ]] && [[ "${SKIP_NET:-0}" != "1" ]]; then
header "Full smoke (RUN_FULL=1)"
if ( cd "$REPO" && make buntes-monster ); then
say_ok "make buntes-monster"
else
say_fail "make buntes-monster (exit $?)"
fi
else
echo " RUN_FULL not set (or SKIP_NET) -> skipping full/hypr/kreation install."
fi
# ---------------------------------------------------------------------------
# Summary & exit code
# ---------------------------------------------------------------------------
echo
echo "=============================================="
echo "entrypoint-test.sh: $PASS passed, $FAIL failed"
echo "=============================================="
# Exit non-zero if anything failed.
[[ $FAIL -eq 0 ]]
Executable
+145
View File
@@ -0,0 +1,145 @@
#!/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="$(pwd)"
cd "$REPO_ROOT"
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).
# ---------------------------------------------------------------------------
mapfile -t SCRIPTS < <(git ls-files '*.sh' 2>/dev/null | grep -v '/tpm/' | grep -vE '/plugins/[^/]+/scripts/')
if [[ ${#SCRIPTS[@]} -eq 0 ]]; then
mapfile -t SCRIPTS < <(find . -name '*.sh' -not -path './.git/*' -not -path '*/tpm/*' -not -path '*/.git/*' | 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 term/setup.sh read_packages).
# ---------------------------------------------------------------------------
echo "== Checking term/packages.txt markers =="
if grep -q '^@pacman' term/packages.txt; then
say_pass "packages.txt declares @pacman section"
else
say_fail "packages.txt missing @pacman marker"
fi
# every @aur/@npm marker must be matched by a real consumer in setup.sh
for marker in '@aur' '@npm'; do
if grep -q "^$marker" term/packages.txt; then
grep -q "$marker" term/setup.sh \
&& say_pass "packages.txt $marker handled by term/setup.sh" \
|| say_fail "packages.txt uses $marker but term/setup.sh does not handle it"
else
say_pass "packages.txt has no $marker section"
fi
done
# ---------------------------------------------------------------------------
# 5. Makefile <-> install script name consistency.
# Catches typos such as `_install_skripte_install_basic_pcks.sh`
# (missing '/') in Makefile targets.
# ---------------------------------------------------------------------------
echo "== Checking Makefile references install scripts that exist =="
# Pull out every `*_install_*_pcks.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 '[_A-Za-z0-9/-]*install_[A-Za-z0-9_-]*\.sh' | sort -u)
# ---------------------------------------------------------------------------
# 6. 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 -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
echo
echo "=============================================="
echo "lint.sh: $PASS passed, $FAIL failed"
echo "=============================================="
[[ $FAIL -eq 0 ]]
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# test/probe_stow.sh
#
# Verifies that each package the Makefile stows actually resolves and that
# stow can create all symlinks into the target without conflicts.
#
# This is a DRY RUN: it uses `stow --simulate` and creates no real symlinks.
# It catches bugs like `stow nvim` for a non-existent `nvim/` package.
#
# Usage:
# bash test/probe_stow.sh # probe every stow-able package
# bash test/probe_stow.sh <pkg>... # probe specific packages
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
# A stow-able package is any tracked top-level directory that contains a
# dot-prefixed config subtree (or a dot-prefixed top-level file/dir).
tracked_basenames() {
git ls-files | awk -F/ 'NF>1 {print $1}' | sort -u
}
is_stowable() {
local pkg="$1"
[[ -d "$REPO_ROOT/$pkg" ]] || return 1
# stow needs at least one dot-prefixed entry at the package root
git ls-files "$pkg/" | grep -qE "^$pkg/\." || return 1
}
# Determine which packages to probe.
PACKAGES=()
if [[ $# -eq 0 ]]; then
while IFS= read -r b; do
[[ -z "$b" ]] && continue
is_stowable "$b" && PACKAGES+=("$b")
done < <(tracked_basenames)
else
PACKAGES=("$@")
fi
if [[ ${#PACKAGES[@]} -eq 0 ]]; then
echo "probe_stow.sh: no stow-able packages found" >&2
exit 1
fi
echo "== Probing stow packages: ${PACKAGES[*]} =="
FAIL=0
for pkg in "${PACKAGES[@]}"; do
echo "--- stow --simulate: $pkg ---"
if stow --simulate --verbose=1 --dir="$REPO_ROOT" --target="$REPO_ROOT" "$pkg" >/dev/null 2>probe_err.txt; then
echo "ok: stow --simulate '$pkg' resolved without conflicts"
else
echo "FAIL: stow --simulate '$pkg'"
sed 's/^/ /' probe_err.txt
FAIL=$((FAIL+1))
fi
rm -f probe_err.txt
done
# Cross-check: does the Makefile stow anything that is NOT in our package set?
echo "== Checking Makefile stow targets against stow-able packages =="
make_pkgs="$(grep -P '^\t' Makefile | grep -oE 'stow [A-Za-z0-9_-]+' | awk '{print $2}' | sort -u)"
for mp in $make_pkgs; do
if is_stowable "$mp"; then
echo "ok: Makefile 'stow $mp' matches a stow-able package"
else
echo "FAIL: Makefile 'stow $mp' does not match a stow-able package"
FAIL=$((FAIL+1))
fi
done
echo
echo "=============================================="
echo "probe_stow.sh: $(( ${#PACKAGES[@]} + $(printf '%s\n' $make_pkgs | grep -c . || true) )) checks, $FAIL failed"
echo "=============================================="
[[ $FAIL -eq 0 ]]
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
# test/verify_packages.sh
#
# Post-install verification of the packages the smoke test installs.
# Given a package-list file (one package per line, `@section` markers
# allowed - only the section content is verified), it checks that every
# listed package is:
# 1. installed in the pacman DB -> `pacman -Q <pkg>`
# 2. its expected binary exists and reports a version (curated mapping)
#
# Layer 2 runs a real `--version`-style probe for packages that install a
# binary, so a broken/renamed executable is caught even if pacman reports
# the package as installed. Fonts/libs/modules without a binary are verified
# by Layer 1 only.
#
# Usage: bash test/verify_packages.sh <package-list-file>
set -uo pipefail
PKGS_FILE="${1:?usage: verify_packages.sh <package-list-file>}"
[[ -f "$PKGS_FILE" ]] || { echo "verify_packages.sh: not a file: $PKGS_FILE" >&2; exit 1; }
PASS=0
FAIL=0
say_ok() { printf ' ok: %s\n' "$*"; PASS=$((PASS+1)); }
say_fail() { printf ' FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); }
# ---------------------------------------------------------------------------
# Curated mapping: package -> "binary version_flag"
# - packages not listed here are checked with pacman -Q only
# - flag missing (empty) => existence check only
# ---------------------------------------------------------------------------
declare -A BINS=(
[fish]="fish --version"
[kitty]="kitty --version"
[neovim]="nvim --version"
[openssh]="ssh -V"
[rclone]="rclone --version"
[rsync]="rsync --version"
[unison]="unison -version"
[stow]="stow --version"
[nodejs]="node --version"
[npm]="npm --version"
[starship]="starship --version"
[fastfetch]="fastfetch --version"
[tree-sitter-cli]="tree-sitter --version"
[python]="python --version"
[python-pip]="pip3 --version"
[unzip]="unzip --version"
[selene]="selene --version"
[tmux]="tmux -V"
[yazi]="yazi --version"
[zoxide]="zoxide --version"
[fzf]="fzf --version"
[chromium]="chromium --version"
[locate]="locate --version"
)
# python-pip may expose `pip` instead of `pip3` on some setups; treat them
# as interchangeable so the probe does not spuriously fail.
probe_pip() {
for b in pip3 pip; do
if command -v "$b" >/dev/null 2>&1; then
out=$("$b" --version 2>&1) && [[ -n "$out" ]] && { say_ok "$b --version ($out)"; return 0; }
fi
done
say_fail "python-pip: neither pip3 nor pip found/probe failed"
return 1
}
echo "== Verifying installed packages from: $PKGS_FILE =="
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}" # strip trailing comments
line="${line// /}" # drop inline whitespace
[[ -z "$line" ]] && continue
[[ "$line" == @* ]] && continue # skip @pacman/@aur/@npm section markers
pkg="${line#*/}" # strip repo prefix (extra/kitty -> kitty)
if [[ -z "$pkg" ]]; then
say_fail "empty package name in '$line'"
continue
fi
# Layer 1: installed in the pacman DB?
if pacman -Q "$pkg" >/dev/null 2>&1; then
say_ok "pacman -Q $pkg (installed)"
else
say_fail "pacman -Q $pkg (not installed)"
continue
fi
# Layer 2: runtime binary probe (curated mapping)?
spec="${BINS[$pkg]:-}"
[[ -z "$spec" ]] && continue # no binary (font/lib/module) - Layer 1 only
# python-pip has special multi-candidate handling
[[ "$pkg" == "python-pip" ]] && { probe_pip; continue; }
read -r bin flag <<< "$spec"
if command -v "$bin" >/dev/null 2>&1; then
if [[ -z "$flag" ]]; then
say_ok "command -v $bin (present)"
else
out=$("$bin" $flag 2>&1) && [[ -n "$out" ]]
if [[ $? -eq 0 ]]; then
say_ok "$bin $flag ($out)"
else
say_fail "$bin $flag (binary present but probe failed)"
fi
fi
else
say_fail "command -v $bin (expected from package '$pkg')"
fi
done < "$PKGS_FILE"
echo
echo "----------------------------------------------"
echo "verify_packages.sh: $PASS passed, $FAIL failed"
echo "----------------------------------------------"
[[ $FAIL -eq 0 ]]