Restructure repo (#251)

* refactor: restructure src/ into modules/, scripts/, and build/

- src/nag_screen/ and src/node_info/ -> src/modules/  (mods grouped explicitly)
- src/Scripts/ -> src/scripts/                        (lowercase, consistent)
- src/gen-rules.sh -> build/                          (build tool separated from source)

Update all references: debian/rules, .github/workflows/*, build/gen-rules.sh,
src/scripts/test/test-syntax.sh (SRC_DIR now points to src/modules/),
src/scripts/test/test-patches.sh, and files.list comments.

* refactor: move CI test scripts from src/scripts/test/ to build/test/

Test scripts are not installed — they are CI/dev tooling and belong
alongside build/gen-rules.sh. Update workflow references and fix the
SRC_DIR path depth in build/test/test-syntax.sh (../ -> ../..).

* d

---------

Co-authored-by: Meliox <na>
This commit is contained in:
MelioxandMeliox authored and GitHub committed 2026-08-08 23:46:46 +02:00
1 parent 3db6a010df
commit 6585849459
44 files changed
+342 -30

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# build/gen-rules.sh
#
# Generates the per-module portion of the Debian build configuration from each
# module's metadata, so adding or changing a module never requires touching
# debian/rules by hand.
#
# A module is any directory under src/modules/ that contains a files/ and/or
# patches/ subdirectory. The module's directory name is its canonical mod key
# (matches the [modules] keys in pve-mod.conf and the install path
# usr/lib/pve-mod/patches/<mod>/).
#
# Usage:
# gen-rules.sh Emit dpkg install lines (tab-indented, no header).
# Append to debian/rules' override_dh_install recipe:
# bash build/gen-rules.sh >> debian/rules
# Conffiles under etc/ are detected automatically by
# debhelper (compat 13+); no separate conffiles step needed.
#
# For each module it emits, in order:
# 1. files/files.list -> install each mapped file at its destination.
# Format: <source> <destination> [permission]
# (permission defaults to 644; source is relative to
# the module's files/ directory).
# 2. patches/* -> every file under patches/ recursively, including
# patches.list and hook scripts. Shell scripts (.sh)
# get mode 755, everything else 644. Installed under
# usr/lib/pve-mod/patches/<mod>/<relative-path>.
# 3. <mod>.conf -> if present in the module root, installed as a
# conffile at etc/pve-mod/conf.d/<mod>.conf (644) plus
# a reference copy at
# usr/share/pve-mod/conf.d/<mod>.conf.default (644).
set -euo pipefail
# Resolve repository root from this script's location so paths are stable
# regardless of the caller's working directory.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SRC_DIR="$REPO_ROOT/src/modules"
PKG_DIR="debian/pve-mod"
# Print the list of module directory names (basenames), sorted, that contain a
# files/ or patches/ subdirectory.
list_modules() {
local d name
for d in "$SRC_DIR"/*/; do
[[ -d "$d" ]] || continue
name="$(basename "$d")"
if [[ -d "$d/files" || -d "$d/patches" ]]; then
echo "$name"
fi
done | sort
}
# Emit a single tab-indented `install -D` line.
# Args: <mode> <relative-source> <relative-destination>
emit_install() {
printf '\tinstall -Dm%s %s %s/%s\n' "$1" "$2" "$PKG_DIR" "$3"
}
emit_install_rules() {
local mod="$1"
local mod_dir="$SRC_DIR/$mod"
local rel_mod="src/modules/$mod"
# 1. Mapped files from files/files.list.
local manifest="$mod_dir/files/files.list"
if [[ -f "$manifest" ]]; then
local src dest perm
while read -r src dest perm; do
# Tolerate CRLF line endings.
src="${src%$'\r'}"; dest="${dest%$'\r'}"; perm="${perm%$'\r'}"
# Skip comments and blank lines.
[[ -z "${src:-}" || "$src" == \#* ]] && continue
perm="${perm:-644}"
emit_install "$perm" "$rel_mod/files/$src" "$dest"
done < "$manifest"
fi
# 2. Everything under patches/ (recursive): .sh -> 755, else 644.
local patches_dir="$mod_dir/patches"
if [[ -d "$patches_dir" ]]; then
local f rel perm
while IFS= read -r f; do
rel="${f#"$patches_dir"/}"
if [[ "$rel" == *.sh ]]; then
perm=755
else
perm=644
fi
emit_install "$perm" "$rel_mod/patches/$rel" "usr/lib/pve-mod/patches/$mod/$rel"
done < <(find "$patches_dir" -type f | sort)
fi
# 3. Per-module config: conffile + reference default copy.
local conf="$mod_dir/$mod.conf"
if [[ -f "$conf" ]]; then
emit_install 644 "$rel_mod/$mod.conf" "etc/pve-mod/conf.d/$mod.conf"
emit_install 644 "$rel_mod/$mod.conf" "usr/share/pve-mod/conf.d/$mod.conf.default"
fi
# 4. Per-module configure script.
local configure_script="$mod_dir/$mod.configure.sh"
if [[ -f "$configure_script" ]]; then
emit_install 755 "$rel_mod/$mod.configure.sh" "usr/lib/pve-mod/configure.d/$mod.sh"
fi
}
for mod in $(list_modules); do
emit_install_rules "$mod"
done
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Downloads upstream Proxmox VE packages and extracts all their files to real
# system paths, so the patch engine can be exercised on a plain CI runner.
set -euo pipefail
SUITE="${PVE_SUITE:-trixie}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
echo "[fetch] Configuring Proxmox no-subscription repo ($SUITE)..."
sudo wget -q "https://enterprise.proxmox.com/debian/proxmox-archive-keyring-${SUITE}.gpg" \
-O /usr/share/keyrings/proxmox-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/proxmox-archive-keyring.gpg] http://download.proxmox.com/debian/pve $SUITE pve-no-subscription" \
| sudo tee /etc/apt/sources.list.d/pve-test.list >/dev/null
sudo apt-get update -qq
echo "[fetch] Downloading pve-manager, proxmox-widget-toolkit, pve-yew-mobile-gui, libjson-perl, libpve-common-perl, libclone-perl, liblinux-inotify2-perl, libcommon-sense-perl, libhttp-message-perl, libstring-shellquote-perl, libencode-locale-perl, libdevel-cycle-perl, liburi-perl, libnet-ip-perl, libtimedate-perl and libfilesys-df-perl..."
cd "$WORKDIR"
apt-get download pve-manager proxmox-widget-toolkit pve-yew-mobile-gui libjson-perl libpve-common-perl libclone-perl liblinux-inotify2-perl libcommon-sense-perl libhttp-message-perl libstring-shellquote-perl libencode-locale-perl libdevel-cycle-perl liburi-perl libnet-ip-perl libtimedate-perl libfilesys-df-perl
echo "[fetch] Extracting to system paths..."
for deb in *.deb; do
echo "[fetch] $deb"
sudo dpkg-deb -x "$deb" /
done
echo "[fetch] Done."
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
# build/test/test-patches.sh
#
# Exercises pve-mod's patch engine against the installed Proxmox files.
#
# Usage: test-mods.sh <mod> | all
#
# Mods are auto-discovered from the [modules] section of the main config, so
# adding a new mod (a new [modules] key plus a patches/<mod>/ directory) is
# picked up automatically - no edits to this script are required.
#
# For each target mod it:
# 1. enables only that mod in the main config
# 2. turns on every conditional flag referenced by the mod's patches.list,
# so all of the mod's patches are exercised (e.g. node_info's gpu_history)
# 3. runs apply-patches.sh and asserts it exits 0
# 4. runs revert-patches.sh and asserts it reports no unclean reversions
#
# Failure detection relies on the apply exit code: the patch engine performs an
# atomic preflight dry-run and exits non-zero if any mod cannot apply cleanly.
#
# Must run as root (it edits /etc/pve-mod and the patched system files).
set -u
MAIN_CONF="${PVE_MOD_MAIN_CONF:-/etc/pve-mod/pve-mod.conf}"
CONFD_DIR="${PVE_MOD_CONFD_DIR:-/etc/pve-mod/conf.d}"
PATCHES_DIR="${PVE_MOD_PATCHES_DIR:-/usr/lib/pve-mod/patches}"
APPLY="${PVE_MOD_APPLY:-/usr/lib/pve-mod/apply-patches.sh}"
REVERT="${PVE_MOD_REVERT:-/usr/lib/pve-mod/revert-patches.sh}"
info() { echo "[test] $*"; }
warn() { echo "[test] WARNING: $*" >&2; }
# List the keys of the [modules] section in the main config, one per line.
list_modules() {
[[ -f "$MAIN_CONF" ]] || return 0
awk -F= '
/^\[/ { in_sec = ($0 == "[modules]") }
in_sec && /^[^#=]+=/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
print $1
}
' "$MAIN_CONF"
}
# set_conf <file> <section> <key> <value>: set an existing key within a section.
set_conf() {
local file="$1" section="$2" key="$3" value="$4"
[[ -f "$file" ]] || { warn "config not found: $file"; return 1; }
awk -v sec="[$section]" -v k="$key" -v v="$value" '
/^\[/ { in_sec = ($0 == sec) }
{
if (in_sec && $0 ~ "^[[:space:]]*"k"[[:space:]]*=") {
print k"="v
} else {
print
}
}
' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
}
# Disable every discovered mod in the main config.
disable_all_modules() {
local m
for m in $(list_modules); do
set_conf "$MAIN_CONF" modules "$m" 0
done
}
# Turn on every conditional flag a mod's patches.list references, so all of its
# patches become active. Conditions look like: <patch-file> section.key=value
enable_conditions() {
local mod="$1"
local manifest="$PATCHES_DIR/$mod/patches.list"
local mod_conf="$CONFD_DIR/$mod.conf"
[[ -f "$manifest" ]] || return 0
local line cond local_key want sect ckey
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
[[ -z "$line" ]] && continue
# Skip lines without a condition (no whitespace after the patch name).
[[ "$line" == *[[:space:]]* ]] || continue
cond="$(echo "${line#*[[:space:]]}" | sed -E 's/^[[:space:]]+//')"
[[ "$cond" == *=* ]] || continue
local_key="${cond%%=*}"
want="${cond#*=}"
sect="${local_key%%.*}"
ckey="${local_key#*.}"
set_conf "$mod_conf" "$sect" "$ckey" "$want" \
&& info " enabled condition $local_key=$want in $(basename "$mod_conf")"
done < "$manifest"
}
# Test a single mod end to end. Returns 0 on success, 1 on failure.
test_one_mod() {
local mod="$1"
echo "::group::Testing mod: $mod"
local ok=0
if [[ ! -f "$PATCHES_DIR/$mod/patches.list" ]]; then
warn "no patch manifest for mod '$mod' ($PATCHES_DIR/$mod/patches.list)"
echo "::endgroup::"
return 1
fi
disable_all_modules
set_conf "$MAIN_CONF" modules "$mod" 1
enable_conditions "$mod"
info "Applying patches for '$mod'..."
local apply_log apply_rc
apply_log="$("$APPLY" 2>&1)"; apply_rc=$?
echo "$apply_log"
if [[ $apply_rc -ne 0 ]]; then
warn "apply-patches.sh failed for mod '$mod' (exit $apply_rc)"
ok=1
fi
if [[ $ok -eq 0 ]]; then
info "Reverting patches for '$mod'..."
local revert_log
revert_log="$("$REVERT" 2>&1)"
echo "$revert_log"
if echo "$revert_log" | grep -qE "could not be reverted cleanly|reported an error"; then
warn "revert-patches.sh reported an unclean revert for mod '$mod'"
ok=1
fi
fi
if [[ $ok -eq 0 ]]; then
info "PASS: $mod"
else
warn "FAIL: $mod"
fi
echo "::endgroup::"
return $ok
}
# ── main ──────────────────────────────────────────────────────────────────────
[[ $# -eq 1 ]] || { echo "Usage: $0 <mod>|all" >&2; exit 2; }
if ! command -v patch >/dev/null 2>&1; then
warn "'patch' command not found; install the 'patch' package."
exit 1
fi
targets=()
if [[ "$1" == "all" ]]; then
mapfile -t targets < <(list_modules)
[[ ${#targets[@]} -gt 0 ]] || { warn "no mods found in [modules] of $MAIN_CONF"; exit 1; }
else
targets=("$1")
fi
failed=()
for mod in "${targets[@]}"; do
test_one_mod "$mod" || failed+=("$mod")
done
echo ""
if [[ ${#failed[@]} -gt 0 ]]; then
warn "Failed mods: ${failed[*]}"
exit 1
fi
info "All tested mods passed: ${targets[*]}"
exit 0
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# build/test/test-syntax.sh
#
# Validates source syntax for all mod files without requiring a running system.
#
# Usage: test-syntax.sh <mod> | all
#
# Mods are auto-discovered by scanning for subdirectories of src/ that contain
# a files/ or patches/ directory. No installed package required.
#
# For each target mod it:
# 1. runs perl -c on every .pm file under <mod>/files/
# 2. runs node --check on every .js file under <mod>/files/
# 3. runs bash -n on post-apply.sh and post-revert.sh in <mod>/patches/
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_DIR="${PVE_MOD_SRC_DIR:-$(cd "$SCRIPT_DIR/../.." && pwd)/src/modules}"
info() { echo "[syntax] $*"; }
warn() { echo "[syntax] WARNING: $*" >&2; }
# List mod names by scanning SRC_DIR for subdirs that contain files/ or patches/.
list_modules() {
local d name
for d in "$SRC_DIR"/*/; do
[[ -d "$d" ]] || continue
name="$(basename "$d")"
if [[ -d "$d/files" || -d "$d/patches" ]]; then
echo "$name"
fi
done | sort
}
# check_perl_syntax <mod>: run perl -c on every .pm under <mod>/files/
check_perl_syntax() {
local mod="$1"
local files_dir="$SRC_DIR/$mod/files"
[[ -d "$files_dir" ]] || return 0
local ok=0 f output
while IFS= read -r f; do
output="$(perl -c "$f" 2>&1)"
if [[ $? -ne 0 ]]; then
warn "perl -c FAILED: $f"
echo "$output"
ok=1
fi
done < <(find "$files_dir" -name "*.pm" -type f | sort)
return $ok
}
# check_js_syntax <mod>: run node --check on every .js under <mod>/files/
check_js_syntax() {
local mod="$1"
local files_dir="$SRC_DIR/$mod/files"
[[ -d "$files_dir" ]] || return 0
local ok=0 f output
while IFS= read -r f; do
output="$(node --check "$f" 2>&1)"
if [[ $? -ne 0 ]]; then
warn "node --check FAILED: $f"
echo "$output"
ok=1
fi
done < <(find "$files_dir" -name "*.js" -type f | sort)
return $ok
}
# check_bash_syntax <mod>: run bash -n on post-apply.sh and post-revert.sh
check_bash_syntax() {
local mod="$1"
local patches_src="$SRC_DIR/$mod/patches"
[[ -d "$patches_src" ]] || return 0
local ok=0 script f output
for script in post-apply.sh post-revert.sh; do
f="$patches_src/$script"
[[ -f "$f" ]] || continue
output="$(bash -n "$f" 2>&1)"
if [[ $? -ne 0 ]]; then
warn "bash -n FAILED: $f"
echo "$output"
ok=1
fi
done
return $ok
}
# test_syntax_one_mod <mod>: run all syntax checks for one mod.
# Returns 0 on pass, 1 on any failure.
test_syntax_one_mod() {
local mod="$1"
echo "::group::Syntax check: $mod"
local ok=0
check_perl_syntax "$mod" || ok=1
check_js_syntax "$mod" || ok=1
check_bash_syntax "$mod" || ok=1
if [[ $ok -eq 0 ]]; then
info "PASS: $mod"
else
warn "FAIL: $mod"
fi
echo "::endgroup::"
return $ok
}
# ── main ──────────────────────────────────────────────────────────────────────
[[ $# -eq 1 ]] || { echo "Usage: $0 <mod>|all" >&2; exit 2; }
if ! command -v perl >/dev/null 2>&1; then
warn "'perl' command not found; install perl."
exit 1
fi
if ! command -v node >/dev/null 2>&1; then
warn "'node' command not found; install nodejs."
exit 1
fi
targets=()
if [[ "$1" == "all" ]]; then
mapfile -t targets < <(list_modules)
[[ ${#targets[@]} -gt 0 ]] || { warn "no mods found in $SRC_DIR"; exit 1; }
else
targets=("$1")
fi
failed=()
for mod in "${targets[@]}"; do
test_syntax_one_mod "$mod" || failed+=("$mod")
done
echo ""
if [[ ${#failed[@]} -gt 0 ]]; then
warn "Failed mods: ${failed[*]}"
exit 1
fi
info "All tested mods passed: ${targets[*]}"
exit 0