From b475fecc5e15d739b6cb5c89150ca44b41b5eb3c Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Fri, 31 Jul 2026 23:35:19 -0500 Subject: [PATCH] =?UTF-8?q?feat(setup):=20appliance=20mode=20=E2=80=94=20b?= =?UTF-8?q?aked=20deps,=20/run=20units,=20GRUB=20skip=20(pithead#797=20R1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One opt-in env flag, RIGFORGE_APPLIANCE=1, for running setup on the Pithead appliance image: read-only root, volatile /etc overlay, and a boot leg that re-runs setup every boot instead of relying on persisted state. Under the flag: - dependencies are verified (command -v), never installed — a missing tool fails loudly naming the image-build gap; a prebuilt tree needs envsubst only (the R0 bench re-ran from cache with a half-broken compiler) - the GRUB leg takes the skip branch deliberately: the kernel cmdline, incl. any 1GB-hugepage reservation, is image-owned - units render into /run/systemd/system (SYSTEMD_DIR preset) and enable with systemctl enable --runtime - hugetlbfs mounts at runtime; no fstab or limits.conf appends (the unit already sets LimitMEMLOCK=infinity) - runtime tuning unchanged: modprobe msr, grow-only HugePages sysctl (#328), performance governor setup --dry-run previews the same decisions through the shared helpers, never a second copy. Every decision was proven on the pithead#797 R0 bench. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 16 ++++ docs/how-it-works.md | 26 ++++++ rigforge.sh | 146 ++++++++++++++++++++++++++----- tests/run.sh | 201 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 367 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1142337..21de146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ All notable changes to RigForge are documented here. The format is based on ## [Unreleased] +### Added + +- **Appliance mode: `RIGFORGE_APPLIANCE=1` (pithead#797 R1).** One opt-in env flag for running + `setup` on the Pithead appliance image, whose root filesystem is read-only and whose `/etc` is a + volatile overlay — every write there vanishes at reboot, and the image's boot path re-runs setup + each boot instead of relying on persisted state. Under the flag, setup installs no packages (the + toolchain is baked at image build; if a required tool is absent, setup fails naming it), skips the + GRUB leg deliberately (the kernel cmdline, including any 1 GB-hugepage reservation, is + image-owned), renders its systemd units into `/run/systemd/system` and enables them with + `--runtime`, mounts `hugetlbfs` at runtime instead of appending to `fstab`, and skips the + `limits.conf` memlock append (the unit already sets `LimitMEMLOCK=infinity`). Runtime tuning is + unchanged: `modprobe msr`, the grow-only HugePages sysctl (#328), and the performance governor all + work on a read-only root — each decision proven on the pithead#797 R0 bench. `setup --dry-run` + previews the same decisions. Everything else (a normal Linux install, macOS) behaves exactly as + before. + ### Fixed - **Runtime HugePages reservation is grow-only (#328).** `tune_kernel`'s runtime sysctl wrote the diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 130043e..8d380ab 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -209,6 +209,32 @@ XMRig accordingly (and there's no systemd service, so you run the miner yourself --- +## Appliance mode (`RIGFORGE_APPLIANCE=1`) + +The Pithead appliance image runs from a read-only root with a volatile `/etc` overlay: anything +setup writes there disappears at reboot, and the image's boot path re-runs setup every boot +instead. `RIGFORGE_APPLIANCE=1` adapts setup to that contract (pithead#797): + +- No package install. The toolchain is baked into the image at build; setup verifies the tools it + needs and fails naming any that are missing, rather than attempting an install that can't work + on a read-only root. A prebuilt worker needs no compiler at all — the every-boot re-run keeps + mining even without one. +- GRUB skipped deliberately. The kernel cmdline — including any 1 GB-hugepage reservation — is + owned by the image. Without that reservation XMRig falls back to 2 MB pages at 100%: working, + with a small known cost. +- Units in `/run`. The systemd units render into `/run/systemd/system` and are enabled with + `systemctl enable --runtime`, so the enablement lives and dies with the boot, matching the + re-run-every-boot model. +- Runtime mounts only. `hugetlbfs` is mounted directly instead of via `fstab`, and the + `limits.conf` memlock append is skipped — the unit already sets `LimitMEMLOCK=infinity`, and + interactive runs are not an appliance concern. + +Runtime tuning is untouched: `modprobe msr`, the grow-only HugePages sysctl, and the performance +governor all work on a read-only root. `setup --dry-run` previews the appliance decisions with the +same logic. On a normal install, leave the flag unset — nothing changes. + +--- + ## Safety & idempotency RigForge is built to be re-run: diff --git a/rigforge.sh b/rigforge.sh index 330c17f..dc7ace8 100755 --- a/rigforge.sh +++ b/rigforge.sh @@ -90,6 +90,33 @@ XMRIG_COMMIT="${XMRIG_COMMIT:-b2ca72480c58d197e18c885d9fc1a0c8d517e60a}" # recompile and the service restart — making re-runs idempotent (#4). XMRIG_REBUILD=true +# Appliance mode (pithead#797 R1): opt-in via RIGFORGE_APPLIANCE=1 for running setup on the Pithead +# appliance image, whose root filesystem is read-only and whose /etc is a volatile overlay — every +# write there vanishes at reboot, and a pithead boot leg re-runs setup each boot instead. An env +# flag, not a config.json key, because the caller is the image's boot path (not the operator) and +# the mode is a preset bundle over the env-overridable system paths below — the same seam the R0 +# bench drove by hand with GRUB_DEFAULT=/nonexistent. Under the flag, setup: +# - never installs packages (the toolchain is baked at image build; apt cannot run on the RO +# root) — it verifies the tools it needs and fails naming what's missing; +# - skips the GRUB leg deliberately (the kernel cmdline, incl. any 1GB-hugepage reservation, is +# image-owned; update-grub aborted the whole run on the RO /boot); +# - renders systemd units into /run/systemd/system and enables them with --runtime; +# - mounts hugetlbfs at runtime instead of appending to fstab, and skips the limits.conf memlock +# append (the unit already sets LimitMEMLOCK=infinity; interactive-run memlock is not an +# appliance concern). +# Runtime tuning is unchanged: modprobe msr, the grow-only HugePages sysctl (#328), and the +# cpupower governor via ExecStartPre all work on a read-only root. +RIGFORGE_APPLIANCE="${RIGFORGE_APPLIANCE:-0}" # every consumer tests `= 1`; anything else is off +if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + # Preset only — an explicit SYSTEMD_DIR in the environment (the test sandbox) still wins, and + # the non-appliance default below keeps this value because it is now set. + SYSTEMD_DIR="${SYSTEMD_DIR:-/run/systemd/system}" +fi +# Unit-enablement mode: appliance units live in /run, so their wants/ symlinks must too (a plain +# `enable` would write them to the volatile /etc overlay — working until reboot, then gone). +ENABLE_RUNTIME="" +if [ "$RIGFORGE_APPLIANCE" = 1 ]; then ENABLE_RUNTIME="--runtime"; fi + # System paths the script writes to. Overridable so the test suite can redirect them at a sandbox # (the defaults are the real locations, so production behaviour is unchanged). LOGROTATE_DIR="${LOGROTATE_DIR:-/etc/logrotate.d}" @@ -738,7 +765,36 @@ _missing_deps() { printf '%s' "$missing" } +# Appliance-mode tool check (pithead#797 R1), shared with the setup --dry-run plan (#146 — never a +# second copy to drift). Echoes the tools setup would actually invoke that are absent. `command -v`, +# not the package manager: on the appliance, dpkg/alternatives state rides the volatile /etc overlay +# and can be stale while the tools in /usr are fine (observed on the R0 bench after a reboot). +# envsubst renders the unit on every run; the compiler chain only matters when a build is pending — +# the R0 bench restored mining from a cached build with a half-broken toolchain, and the every-boot +# re-run must keep doing that. +_missing_appliance_tools() { + local t missing="" tools="envsubst" + [ "$XMRIG_REBUILD" = true ] && tools="git cmake make cc envsubst" + for t in $tools; do + command -v "$t" >/dev/null 2>&1 || missing="$missing $t" + done + printf '%s' "$missing" +} + install_dependencies() { + # Appliance mode: the toolchain is baked into the image at build — apt cannot run on the + # read-only root, and even a rw-remounted install leaves its /etc state on the volatile overlay + # (pithead#797 R0 item 1). So never install here: verify and fail naming the missing tools — + # that's an image build bug, not something a rig can fix at runtime. + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + local _missing + _missing="$(_missing_appliance_tools)" + if [ -n "$_missing" ]; then + error "Appliance mode: required tool(s) missing from the image:$_missing. The toolchain must be baked at image build — package install cannot run on the read-only root." + fi + log "Appliance mode: dependencies are baked into the image — skipping package install." + return 0 + fi if [ "$OS_TYPE" == "Darwin" ]; then log "Installing macOS dependencies..." if command -v brew &>/dev/null; then @@ -1079,8 +1135,8 @@ install_service() { # Reload systemd daemon sudo systemctl daemon-reload - # Enable service to start on boot - sudo systemctl enable "$SERVICE_NAME.service" + # Enable service to start on boot (transiently in appliance mode — the boot leg re-enables) + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} "$SERVICE_NAME.service" if [ "$REBOOT_REQUIRED" = true ]; then # HugePages aren't reserved until the GRUB change takes effect on reboot — starting the miner @@ -1127,7 +1183,7 @@ install_autotune() { envsubst '$AUTOTUNE_ONCALENDAR' \ <"$SCRIPT_DIR/systemd/rigforge-autotune.timer.template" | sudo tee "$tmr" >/dev/null sudo systemctl daemon-reload - sudo systemctl enable --now rigforge-autotune.timer 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} --now rigforge-autotune.timer 2>/dev/null || true } # Install (or remove) the systemd timer that runs the miner watchdog periodically, based on the @@ -1154,7 +1210,7 @@ install_watchdog() { envsubst '$WATCHDOG_INTERVAL_MIN' \ <"$SCRIPT_DIR/systemd/rigforge-watchdog.timer.template" | sudo tee "$tmr" >/dev/null sudo systemctl daemon-reload - sudo systemctl enable --now rigforge-watchdog.timer 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} --now rigforge-watchdog.timer 2>/dev/null || true } # Sister API (#99/#164, xmrig-model): one tiny persistent python3-stdlib server ships pre-computed @@ -1228,8 +1284,8 @@ install_api() { RIGFORGE_OPERATOR="$REAL_USER" SCRIPT_DIR="$SCRIPT_DIR" envsubst '$RIGFORGE_OPERATOR $SCRIPT_DIR' <"$SCRIPT_DIR/systemd/rigforge-api-refresh.service.template" | sudo tee "$rsvc" >/dev/null sudo tee "$rtmr" <"$SCRIPT_DIR/systemd/rigforge-api-refresh.timer.template" >/dev/null sudo systemctl daemon-reload - sudo systemctl enable --now rigforge-api-refresh.timer 2>/dev/null || true - sudo systemctl enable rigforge-api.service 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} --now rigforge-api-refresh.timer 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} rigforge-api.service 2>/dev/null || true # restart, not just enable --now: a bind/port/token change must be re-read (restart also starts). sudo systemctl restart rigforge-api.service 2>/dev/null || true # Prime the state files so the first poll isn't a 503 for a whole timer period. @@ -1281,11 +1337,11 @@ install_control() { sudo rm -f "$usvc" "$upath" fi sudo systemctl daemon-reload - sudo systemctl enable --now rigforge-control-apply.path 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} --now rigforge-control-apply.path 2>/dev/null || true if [ "${CONTROL_UPGRADE:-disabled}" = "enabled" ]; then - sudo systemctl enable --now rigforge-control-upgrade.path 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} --now rigforge-control-upgrade.path 2>/dev/null || true fi - sudo systemctl enable rigforge-control.service 2>/dev/null || true + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} rigforge-control.service 2>/dev/null || true # restart, not just enable --now: a bind/port/token/upgrade-flag change must be re-read (restart also starts). sudo systemctl restart rigforge-control.service 2>/dev/null || true } @@ -1389,7 +1445,13 @@ tune_kernel() { if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "i686" ]]; then log "Enabling MSR module for hardware prefetcher tuning..." sudo modprobe msr 2>/dev/null || true - if [ -d "$MODULES_LOAD_DIR" ]; then + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + # The modprobe above is the whole job on the appliance: setup re-runs every boot (the + # pithead boot leg), so a modules-load drop-in is persistence machinery for a + # persistence layer the box doesn't have. Smaller than writing /run/modules-load.d + # for the same effect — that dir is only read at boot, before this run exists. + log "Appliance mode: msr loaded at runtime only (no modules-load drop-in — setup re-runs each boot)." + elif [ -d "$MODULES_LOAD_DIR" ]; then echo "msr" | sudo tee "$MODULES_LOAD_DIR/msr.conf" >/dev/null elif [ -f "$MODULES_FILE" ]; then append_once "$MODULES_FILE" "msr" @@ -1414,7 +1476,14 @@ tune_kernel() { fi log "Configuring bootloader (GRUB) for persistent HugePages..." - if [ -f "$SCRIPT_DIR/util/proposed-grub.sh" ] && [ -f "$GRUB_DEFAULT" ]; then + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + # A deliberate skip, not the utility-not-found fallback below: on the appliance /boot is + # read-only and the kernel cmdline (incl. any 1GB-hugepage reservation) is image-owned — + # an unguarded update-grub took the whole R0 run down after a completed compile. Without + # the 1G reservation XMRig falls back to 2MB pages at 100%: working, small known cost + # (pithead#797 R0 item 2). + log "Appliance mode: skipping GRUB updates — the kernel cmdline is image-owned." + elif [ -f "$SCRIPT_DIR/util/proposed-grub.sh" ] && [ -f "$GRUB_DEFAULT" ]; then # proposed-grub.sh prints a generic "quiet splash" prefix plus the HugePage/MSR params we # manage. Keep only the params we manage and MERGE them into the existing cmdline so we don't # clobber other kernel parameters the user/distro set (#19 — boot-safety). @@ -1445,6 +1514,22 @@ configure_limits() { return fi + # Appliance mode: mount hugetlbfs at RUNTIME only. The fstab lines would land on the volatile + # /etc overlay and vanish at reboot (proven on the R0 bench) — setup re-runs each boot and just + # mounts again. The limits.conf memlock append covers interactive runs only, which are not an + # appliance concern: the unit already sets LimitMEMLOCK=infinity. + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + log "Appliance mode: mounting hugetlbfs at runtime (no fstab or limits.conf writes)..." + sudo mkdir -p "$HUGEPAGES_1G_DIR" + mountpoint -q /dev/hugepages 2>/dev/null || + sudo mount -t hugetlbfs hugetlbfs /dev/hugepages || + warn "Could not mount /dev/hugepages. Check 'dmesg' for details." + mountpoint -q "$HUGEPAGES_1G_DIR" 2>/dev/null || + sudo mount -t hugetlbfs -o pagesize=1G hugetlbfs_1g "$HUGEPAGES_1G_DIR" || + warn "Could not mount $HUGEPAGES_1G_DIR (1G pages need an image-owned cmdline reservation)." + return 0 + fi + log "Configuring persistent HugePage mounts and memory limits..." sudo mkdir -p "$HUGEPAGES_1G_DIR" @@ -1583,7 +1668,16 @@ _setup_plan() { _p "checking the build" "skip the build — XMRig $XMRIG_VERSION already built at the pinned commit" fi _p "preparing workspace" "workspace at $WORKER_ROOT (an existing prior install would be archived first)" - if [ "$OS_TYPE" = "Darwin" ]; then + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + # Same check install_dependencies runs (shared _missing_appliance_tools — no second copy). + local _mt + _mt="$(_missing_appliance_tools)" + if [ -n "$_mt" ]; then + _p "installing dependencies" "appliance mode: FAIL — required tool(s) missing from the image:$_mt (the toolchain must be baked at image build)" + else + _p "installing dependencies" "appliance mode: baked into the image — no package install" + fi + elif [ "$OS_TYPE" = "Darwin" ]; then _p "installing dependencies" "install/verify via brew: cmake libuv openssl hwloc" elif _detect_pkg_manager; then local _md @@ -1610,10 +1704,16 @@ _setup_plan() { local _msr _pages="(proposed-grub.sh missing — fallback 3072)" _grubline="GRUB: will check at run time" _reboot="" _msr="write msr to $MODULES_LOAD_DIR/msr.conf (module autoload)" [ -e "$MODULES_LOAD_DIR/msr.conf" ] && _msr="msr module already configured" + # Appliance arms: the same flag checks tune_kernel makes, previewed (pithead#797 R1). The + # GRUB probe/diff below is also gated off — the appliance skip line must stand as-is. + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + _msr="modprobe msr at runtime only (appliance: no modules-load drop-in)" + _grubline="skipping GRUB updates (appliance: the kernel cmdline is image-owned)" + fi if [ -f "$SCRIPT_DIR/util/proposed-grub.sh" ]; then _rx_setup_threads _pages=$(RX_THREADS="$RX_SETUP_THREADS" RESERVE_EXTRA_MB="${HUGEPAGES_RESERVE_EXTRA_MB:-0}" THREADS_CAP="${THREADS_CAP:-}" "$SCRIPT_DIR/util/proposed-grub.sh" --runtime 2>/dev/null) || _pages="?" - if [ -f "$GRUB_DEFAULT" ]; then + if [ "$RIGFORGE_APPLIANCE" != 1 ] && [ -f "$GRUB_DEFAULT" ]; then _grub_proposed if [ "$CURRENT" = "$MERGED" ]; then _grubline="GRUB already configured (no reboot needed for it)" @@ -1629,15 +1729,19 @@ _setup_plan() { _hpline="HugePages pool already covers the miner ($_pages pages needed) — no change" fi _p "tuning the kernel" "$_msr; $_hpline; $_grubline$_reboot" - local _f1="hugetlbfs /dev/hugepages hugetlbfs defaults 0 0" _f2="hugetlbfs_1g $HUGEPAGES_1G_DIR hugetlbfs pagesize=1G 0 0" _add="" - grep -qxF "$_f1" "$FSTAB" 2>/dev/null || _add=" '$_f1'" - grep -qxF "$_f2" "$FSTAB" 2>/dev/null || _add="$_add '$_f2'" - if [ -n "$_add" ]; then - _p "configuring limits" "append to $FSTAB:$_add; memlock unlimited for $REAL_USER in $LIMITS_CONF" + if [ "$RIGFORGE_APPLIANCE" = 1 ]; then + _p "configuring limits" "appliance mode: mount hugetlbfs at runtime — no $FSTAB or $LIMITS_CONF writes (volatile /etc)" else - _p "configuring limits" "fstab already configured; memlock unlimited for $REAL_USER in $LIMITS_CONF" + local _f1="hugetlbfs /dev/hugepages hugetlbfs defaults 0 0" _f2="hugetlbfs_1g $HUGEPAGES_1G_DIR hugetlbfs pagesize=1G 0 0" _add="" + grep -qxF "$_f1" "$FSTAB" 2>/dev/null || _add=" '$_f1'" + grep -qxF "$_f2" "$FSTAB" 2>/dev/null || _add="$_add '$_f2'" + if [ -n "$_add" ]; then + _p "configuring limits" "append to $FSTAB:$_add; memlock unlimited for $REAL_USER in $LIMITS_CONF" + else + _p "configuring limits" "fstab already configured; memlock unlimited for $REAL_USER in $LIMITS_CONF" + fi fi - _p "installing the service" "render systemd/xmrig.service.template -> $SYSTEMD_DIR/$SERVICE_NAME.service (User=${MINER_USER:-root}), daemon-reload, enable --now" + _p "installing the service" "render systemd/xmrig.service.template -> $SYSTEMD_DIR/$SERVICE_NAME.service (User=${MINER_USER:-root}), daemon-reload, enable${ENABLE_RUNTIME:+ $ENABLE_RUNTIME} --now" fi case "$AUTOTUNE_MODE" in disabled) _p "configuring autotune" "no periodic timer (autotune disabled) — an installed one would be removed" ;; @@ -3615,7 +3719,7 @@ svc_enable() { mac_enable return } - sudo systemctl enable "$SERVICE_NAME" && log "Enabled $SERVICE_NAME (starts on boot)." + sudo systemctl enable ${ENABLE_RUNTIME:+"$ENABLE_RUNTIME"} "$SERVICE_NAME" && log "Enabled $SERVICE_NAME (starts on boot)." } svc_disable() { [ "$OS_TYPE" = "Linux" ] || { diff --git a/tests/run.sh b/tests/run.sh index 6d028eb..56ed6cc 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -156,8 +156,10 @@ sed -e "s|\$BUILD_DIR|${BUILD_DIR:-}|g" -e "s|\$CPUPOWER_PATH|${CPUPOWER_PATH:-} -e "s|\$CONTROL_BIND|${CONTROL_BIND:-}|g" -e "s|\$CONTROL_PORT|${CONTROL_PORT:-}|g" EOF # No-op recorders / package managers. dpkg/rpm/pacman exit 0 so "is this dep installed?" is always yes. + # cc: the appliance-mode tool check (pithead#797 R1) probes `command -v cc` — stub it so black-box + # runs don't depend on whether the host has a compiler. local cmd - for cmd in make cmake systemctl modprobe mount umount mountpoint update-grub apt-get apt-cache dpkg dnf rpm pacman brew cpupower journalctl python3 nft useradd; do + for cmd in make cmake cc systemctl modprobe mount umount mountpoint update-grub apt-get apt-cache dpkg dnf rpm pacman brew cpupower journalctl python3 nft useradd; do cat >"$bin/$cmd" <> "\${CALL_LOG:-/dev/null}" @@ -5021,6 +5023,203 @@ SC="$HP/calls6" out="$(run_tk328_nopg "$SC")" assert_contains "fallback (no proposed-grub.sh) is grow-only too (#328)" "$(cat "$SC")" "vm.nr_hugepages=3072" +# --------------------------------------------------------------------------- +# Appliance mode (pithead#797 R1): RIGFORGE_APPLIANCE=1 runs setup on the Pithead appliance image — +# read-only root, volatile /etc overlay, a boot leg re-runs setup every boot. Under the flag setup +# must: never install packages (fail naming missing tools instead), skip the GRUB leg, render units +# into /run and enable them --runtime, mount hugetlbfs at runtime with no fstab/limits.conf writes — +# while runtime tuning (modprobe msr, grow-only sysctl) stays byte-identical. +echo "== black-box: appliance mode (pithead#797 R1) ==" +AP="$(mktemp -d "$SANDBOX/appliance.XXXXXX")" + +# The flag presets SYSTEMD_DIR to /run and flips enablement to --runtime; an explicit override and +# the no-flag defaults are unchanged. +out="$( (unset SYSTEMD_DIR && RIGFORGE_APPLIANCE=1 && source "$SCRIPT" && printf '%s|%s' "$SYSTEMD_DIR" "$ENABLE_RUNTIME"))" +assert_eq "flag presets /run/systemd/system + --runtime (#797)" "$out" "/run/systemd/system|--runtime" +out="$( (unset SYSTEMD_DIR && source "$SCRIPT" && printf '%s|%s' "$SYSTEMD_DIR" "$ENABLE_RUNTIME"))" +assert_eq "no flag: /etc/systemd/system + persistent enable (#797)" "$out" "/etc/systemd/system|" +out="$( (SYSTEMD_DIR="$AP/custom-sd" && RIGFORGE_APPLIANCE=1 && source "$SCRIPT" && printf '%s' "$SYSTEMD_DIR"))" +assert_eq "explicit SYSTEMD_DIR still wins under the flag (#797)" "$out" "$AP/custom-sd" + +# Dependency handling: tools verified (command -v), never installed. The toolchain is only required +# while a build is pending; a prebuilt tree needs envsubst alone (the R0 bench re-ran with a broken +# compiler). PATH is restricted to purpose-built bins so the host's real toolchain can't leak in. +mkbin_ap() { # : a dir of exit-0 fakes + local d="$1" c + shift + mkdir -p "$d" + for c in "$@"; do + printf '#!/bin/sh\nexit 0\n' >"$d/$c" + chmod +x "$d/$c" + done +} +mkbin_ap "$AP/bin-all" git cmake make cc envsubst +mkbin_ap "$AP/bin-envsubst" envsubst +run_apdeps() { # ; echoes output, exits with install_dependencies' rc + ( + RIGFORGE_APPLIANCE=1 + source "$SCRIPT" + OS_TYPE=Linux + XMRIG_REBUILD="$2" + set +e + # PATH is ONLY the purpose-built bin dir — no $STUBS (it fakes the whole toolchain, which + # would mask the missing-tool case) and no real PATH (a host compiler would too). The + # appliance branch itself needs nothing but shell builtins. + PATH="$1" CALL_LOG="$AP/deps-calls.log" install_dependencies &1 + ) +} +: >"$AP/deps-calls.log" +out="$(run_apdeps "$AP/bin-all" true)" +assert_rc "all tools baked -> deps step passes (#797)" "$?" "0" +assert_contains "deps step says baked, skipping install (#797)" "$out" "skipping package install" +assert_absent "no package manager is ever invoked (#797)" "$(cat "$AP/deps-calls.log")" "[apt-get]" +out="$(run_apdeps "$AP/bin-envsubst" true)" +assert_rc "missing toolchain while a build pends -> hard fail (#797)" "$?" "1" +assert_contains "failure names the missing tools (#797)" "$out" "missing from the image: git cmake make cc" +assert_contains "failure points at image build, not runtime install (#797)" "$out" "baked at image build" +out="$(run_apdeps "$AP/bin-envsubst" false)" +assert_rc "prebuilt tree needs no compiler — envsubst alone passes (#797)" "$?" "0" + +# tune_kernel: GRUB file present and update-grub available, yet the appliance skip branch runs — +# no cmdline edit, no backup, no update-grub, no modules-load drop-in. Runtime tuning unchanged: +# modprobe msr still runs and the grow-only sysctl still writes the shortfall. +APK="$AP/kernel" +mkdir -p "$APK/util" "$APK/home/worker" "$APK/mld" "$APK/bin" +cat >"$APK/util/proposed-grub.sh" <<'EOF' +#!/usr/bin/env bash +case "${1:-}" in +--runtime) echo 200 ;; +-q) echo "quiet splash default_hugepagesz=2M hugepages=200 msr.allow_writes=on" ;; +esac +EOF +chmod +x "$APK/util/proposed-grub.sh" +cat >"$APK/bin/sysctl" <<'EOF' +#!/usr/bin/env bash +echo "$*" >>"$SYSCTL_CALLS" +EOF +chmod +x "$APK/bin/sysctl" +printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"\n' >"$APK/grub" +printf 'HugePages_Free: 0\n' >"$APK/meminfo" +printf '0\n' >"$APK/nr_hugepages" +out="$( + ( + RIGFORGE_APPLIANCE=1 + source "$SCRIPT" + OS_TYPE=Linux + SCRIPT_DIR="$APK" + WORKER_ROOT="$APK/home/worker" + MODULES_LOAD_DIR="$APK/mld" # exists — the non-appliance path WOULD drop msr.conf here + GRUB_DEFAULT="$APK/grub" # exists — the non-appliance path WOULD edit it + MEMINFO="$APK/meminfo" + NR_HUGEPAGES_FILE="$APK/nr_hugepages" + export SYSCTL_CALLS="$APK/sysctl-calls.log" + set +e + PATH="$APK/bin:$STUBS:$PATH" CALL_LOG="$APK/calls.log" tune_kernel 2>&1 + ) +)" +assert_contains "GRUB leg skipped with the image-owned message (#797)" "$out" "skipping GRUB updates — the kernel cmdline is image-owned" +assert_eq "GRUB file untouched (#797)" "$(cat "$APK/grub")" 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"' +assert_eq "no GRUB backup written (#797)" "$([ -e "$APK/grub.bak" ] && echo present || echo absent)" "absent" +assert_absent "update-grub never runs (#797)" "$(cat "$APK/calls.log")" "[update-grub]" +assert_eq "no modules-load drop-in (#797)" "$([ -e "$APK/mld/msr.conf" ] && echo present || echo absent)" "absent" +assert_contains "modprobe msr still runs — runtime tuning unchanged (#797)" "$(cat "$APK/calls.log")" "[modprobe] msr" +assert_contains "grow-only HugePages sysctl still writes (#797/#328)" "$(cat "$APK/sysctl-calls.log")" "vm.nr_hugepages=200" + +# configure_limits: hugetlbfs mounted at runtime (both page sizes), fstab and limits.conf never +# touched. mountpoint is faked not-mounted so the mount calls are observable; the second run fakes +# already-mounted and must mount nothing (idempotent re-run, the every-boot path). +APL="$AP/limits" +mkdir -p "$APL/bin" +printf '#!/bin/sh\nexit 1\n' >"$APL/bin/mountpoint" +chmod +x "$APL/bin/mountpoint" +printf 'seeded\n' >"$APL/fstab" +printf 'seeded\n' >"$APL/limits.conf" +run_aplimits() { # + ( + RIGFORGE_APPLIANCE=1 + source "$SCRIPT" + OS_TYPE=Linux + FSTAB="$APL/fstab" + LIMITS_CONF="$APL/limits.conf" + HUGEPAGES_1G_DIR="$APL/hp1g" + set +e + PATH="$1:$STUBS:$PATH" CALL_LOG="$2" configure_limits 2>&1 + ) +} +out="$(run_aplimits "$APL/bin" "$APL/calls.log")" +assert_contains "2MB hugetlbfs mounted at runtime (#797)" "$(cat "$APL/calls.log")" "[mount] -t hugetlbfs hugetlbfs /dev/hugepages" +assert_contains "1G hugetlbfs mounted at runtime (#797)" "$(cat "$APL/calls.log")" "[mount] -t hugetlbfs -o pagesize=1G hugetlbfs_1g $APL/hp1g" +assert_eq "fstab untouched (#797)" "$(cat "$APL/fstab")" "seeded" +assert_eq "limits.conf untouched (#797)" "$(cat "$APL/limits.conf")" "seeded" +out="$(run_aplimits "$STUBS" "$APL/calls2.log")" # stub mountpoint exits 0 = already mounted +assert_absent "already mounted -> no mount calls (#797)" "$(cat "$APL/calls2.log")" "[mount]" + +# install_service: unit rendered into the (appliance-preset) systemd dir, enabled with --runtime. +APS="$AP/svc" +mkdir -p "$APS/run-systemd" "$APS/xmrig/build" +( + cd "$APS" || exit 1 + RIGFORGE_APPLIANCE=1 + source "$SCRIPT" + OS_TYPE=Linux + SCRIPT_DIR="$ROOT" # real systemd/xmrig.service.template + WORKER_ROOT="$APS" + SYSTEMD_DIR="$APS/run-systemd" + REBOOT_REQUIRED=false + XMRIG_REBUILD=true + set +e + PATH="$STUBS:$PATH" CALL_LOG="$APS/calls.log" install_service >/dev/null 2>&1 +) +assert_eq "unit rendered into the runtime systemd dir (#797)" "$([ -f "$APS/run-systemd/xmrig.service" ] && echo yes || echo no)" "yes" +assert_contains "unit enabled with --runtime (#797)" "$(cat "$APS/calls.log")" "[systemctl] enable --runtime xmrig.service" + +# setup --dry-run previews the SAME appliance decisions (shared logic, #146): baked deps, GRUB skip, +# runtime-only msr and mounts, --runtime enablement — and still covers every main() step. +APDR="$AP/dryrun" +mkdir -p "$APDR/etc" "$APDR/util" +cp "$APK/util/proposed-grub.sh" "$APDR/util/proposed-grub.sh" +printf 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"\n' >"$APDR/etc/grub" +printf 'HugePages_Free: 0\n' >"$APDR/etc/meminfo" +cat >"$APDR/config.json" <&1)" +assert_rc "appliance dry-run exits 0 (#797/#146)" "$?" "0" +assert_contains "plan: baked-deps arm (#797)" "$apdr_out" "appliance mode: baked into the image — no package install" +assert_contains "plan: GRUB skip arm (#797)" "$apdr_out" "skipping GRUB updates (appliance: the kernel cmdline is image-owned)" +assert_contains "plan: runtime-only msr arm (#797)" "$apdr_out" "modprobe msr at runtime only (appliance: no modules-load drop-in)" +assert_contains "plan: runtime mounts, no fstab/limits writes (#797)" "$apdr_out" "appliance mode: mount hugetlbfs at runtime" +assert_contains "plan: unit goes to /run with --runtime (#797)" "$apdr_out" "/run/systemd/system/xmrig.service" +assert_contains "plan: enable --runtime wording (#797)" "$apdr_out" "enable --runtime --now" +assert_contains "plan: grow-only preview still renders (#797/#328)" "$apdr_out" "grow the pool so 200 2MB HugePages are available" +for mut in apt-get modprobe tee mount sysctl update-grub; do + assert_absent "appliance dry-run never invokes $mut (#797/#146)" "$(cat "$APDR/calls.log" 2>/dev/null)" "[$mut]" +done +while IFS= read -r step; do + assert_contains "appliance plan covers main() step '$step' (#797/#146)" "$apdr_out" "$step" +done <<<"$main_steps" + +# Full black-box setup with the flag, host-native OS path: proves the flag survives main() wiring +# end to end. Portable asserts here; the Linux-only /etc assertions run on Linux hosts and in the +# Linux CI job (the macOS path skips kernel/limits/service by OS, not by flag). +APW="$(e2e_setup)" +RIGFORGE_APPLIANCE=1 e2e_run "$APW" "$HOST_OS" +rc=$? +assert_rc "appliance full run exits 0 (#797)" "$rc" "0" +assert_absent "appliance full run: no apt-get (#797)" "$(cat "$APW/calls.log")" "[apt-get]" +assert_absent "appliance full run: no brew install (#797)" "$(cat "$APW/calls.log")" "[brew] install" +assert_contains "appliance full run: says deps are baked (#797)" "$E2E_OUT" "dependencies are baked into the image" +if [ "$HOST_OS" = Linux ]; then + assert_contains "appliance full run: GRUB skip taken (#797)" "$E2E_OUT" "the kernel cmdline is image-owned" + assert_contains "appliance full run: GRUB file untouched (#797)" "$(cat "$APW/etc/default/grub")" 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash memmap=4G&2M"' + assert_absent "appliance full run: no fstab hugetlbfs lines (#797)" "$(cat "$APW/etc/fstab")" "hugetlbfs" + assert_absent "appliance full run: no memlock append (#797)" "$(cat "$APW/etc/security/limits.conf")" "memlock" + assert_eq "appliance full run: no msr.conf drop-in (#797)" "$([ -e "$APW/etc/modules-load.d/msr.conf" ] && echo present || echo absent)" "absent" + assert_contains "appliance full run: unit enabled --runtime (#797)" "$(cat "$APW/calls.log")" "[systemctl] enable --runtime xmrig.service" +fi + # tune with no built worker fails clearly. TN2="$(mktemp -d "$SANDBOX/tune2.XXXXXX")" cp "$ROOT/VERSION" "$TN2/"