From ad1efde7d13dfb6868815bba69e0dd10f383812d Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:06:22 +0200 Subject: [PATCH 1/2] Keep catch_errors' exit handler when setup_uv runs setup_uv cleaned its temp dir with trap ... EXIT, which replaced on_exit for the rest of every uv-based install and update. It now uses a RETURN trap, like the forge helpers. --- lib/runtime.func | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/runtime.func b/lib/runtime.func index 4de00cd..5f777e0 100644 --- a/lib/runtime.func +++ b/lib/runtime.func @@ -2720,8 +2720,8 @@ setup_uv() { local UVX_BIN="/usr/local/bin/uvx" local TMP_DIR=$(mktemp -d) - # trap for TMP Cleanup - trap "rm -rf '$TMP_DIR'" EXIT + # RETURN, not EXIT: an EXIT trap here replaced catch_errors' on_exit for the rest of the script + trap 'rm -rf "$TMP_DIR"; trap - RETURN' RETURN # Architecture Detection local ARCH=$(uname -m) From d4c0648d1aed0f2235d93447197300bb3097d363 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:06:23 +0200 Subject: [PATCH 2/2] Clean up after installs and updates again lxc/install.func overrode cleanup_lxc with pkg_clean alone, so installs never cleared the uv, pip, npm, Go or cargo caches. core.func's cleanup_lxc now covers the other package managers through pkg_clean, and the override is gone. Most ct scripts exit inside update_script, so _cs_run_update never reached its cleanup_lxc. on_exit now runs it after a successful update. --- .github/workflows/verify-split-files.yml | 5 ++ core/core.func | 7 ++- core/error_handler.func | 4 ++ lxc/install.func | 10 ---- tests/install-cleanup.sh | 71 ++++++++++++++++++++++++ ui/menu.func | 6 +- 6 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 tests/install-cleanup.sh diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index d5f18bb..c2f8727 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -35,6 +35,7 @@ on: - "tests/failed-command-output.sh" - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" + - "tests/install-cleanup.sh" - ".github/workflows/verify-split-files.yml" push: branches: [main] @@ -61,6 +62,7 @@ on: - "tests/failed-command-output.sh" - "tests/exit-code-hints.sh" - "tests/forge-release-json.sh" + - "tests/install-cleanup.sh" - ".github/workflows/verify-split-files.yml" workflow_dispatch: @@ -121,6 +123,9 @@ jobs: - name: Forge release lookup tests run: bash tests/forge-release-json.sh + - name: Install cleanup and uv trap tests + run: bash tests/install-cleanup.sh + - name: Prefetch lists cover every engine file run: | set -euo pipefail diff --git a/core/core.func b/core/core.func index 32eec6c..62752c4 100644 --- a/core/core.func +++ b/core/core.func @@ -2307,7 +2307,8 @@ cs_testing_state() { # cleanup_lxc() # # - Cleans package manager and language caches (safe for installs AND updates) -# - Supports Alpine (apk), Debian/Ubuntu (apt), Python, Node.js, Go, Rust, Ruby, PHP +# - Supports Alpine (apk), Debian/Ubuntu (apt), other distros through install.func's +# pkg_clean, and Python, Node.js, Go, Rust, Ruby, PHP caches # - Uses fallback error handling to prevent cleanup failures from breaking installs # ------------------------------------------------------------------------------ cleanup_lxc() { @@ -2321,10 +2322,12 @@ cleanup_lxc() { # problem worth reporting. $STD apk cache clean 2>/dev/null || true rm -rf /var/cache/apk/* - else + elif command -v apt-get &>/dev/null; then $STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)" $STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)" $STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)" + elif declare -f pkg_clean >/dev/null 2>&1; then + pkg_clean 2>/dev/null || msg_warn "Package cache cleanup failed (non-critical)" fi find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true diff --git a/core/error_handler.func b/core/error_handler.func index 62363e8..88a0e71 100644 --- a/core/error_handler.func +++ b/core/error_handler.func @@ -764,6 +764,10 @@ on_exit() { # Here, because most ct scripts exit inside update_script. declare -f apt_conffile_report >/dev/null 2>&1 && apt_conffile_report + if [[ $exit_code -eq 0 && "${_CS_UPDATE_CLEANUP:-}" == "1" ]] && declare -f cleanup_lxc >/dev/null 2>&1; then + _CS_UPDATE_CLEANUP=0 + cleanup_lxc || true + fi if _is_container_context; then if [[ $exit_code -ne 0 ]]; then diff --git a/lxc/install.func b/lxc/install.func index 3ab5395..7aecb5d 100644 --- a/lxc/install.func +++ b/lxc/install.func @@ -1883,13 +1883,3 @@ os_info() { echo "Pkg Manager: $PKG_MANAGER" echo "Init System: $INIT_SYSTEM" } - -# ------------------------------------------------------------------------------ -# cleanup_lxc() -# -# Overrides core.func version to support all OS types (not just Alpine/Debian) -# Cleans package caches and removes unnecessary packages after installation -# ------------------------------------------------------------------------------ -cleanup_lxc() { - pkg_clean -} diff --git a/tests/install-cleanup.sh b/tests/install-cleanup.sh new file mode 100644 index 0000000..c19d4dc --- /dev/null +++ b/tests/install-cleanup.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^setup_uv\(\) \{/,/^}/' "$CORE_DIR/lib/runtime.func")" +eval "$(awk '/^on_exit\(\) \{/,/^}/' "$CORE_DIR/core/error_handler.func")" +eval "$(awk '/^cleanup_lxc\(\) \{/,/^}/' "$CORE_DIR/core/core.func")" + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +passed=0 failed=0 + +check() { + if [[ "$3" == "$2" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + failed=$((failed + 1)) + fi +} + +# shellcheck disable=SC2034 # read by the eval'd helpers +STD="" +msg_info() { :; } +msg_ok() { :; } +msg_error() { :; } +msg_warn() { :; } +ensure_dependencies() { :; } +curl_with_retry() { return 1; } +_is_container_context() { return 0; } +_container_write_failure() { :; } + +# setup_uv returning early (here: a failed download) must leave the caller's EXIT trap alone +mktemp() { command mktemp "$@" | tee "$TEST_DIR/uv-tmp"; } +result=$( + trap ':' EXIT + UV_VERSION=0.0.1 setup_uv >/dev/null 2>&1 || true + trap -p EXIT +) +unset -f mktemp +check "setup_uv keeps the caller's EXIT trap" "trap -- ':' EXIT" "$result" +check "setup_uv removes its temp dir" "gone" "$([[ -d "$(cat "$TEST_DIR/uv-tmp")" ]] && echo left || echo gone)" + +run_exit() { + rm -f "$TEST_DIR/cleaned" + ( + cleanup_lxc() { touch "$TEST_DIR/cleaned"; } + _CS_UPDATE_CLEANUP="$1" + trap on_exit EXIT + exit "$2" + ) || true + [[ -e "$TEST_DIR/cleaned" ]] && echo ran || echo skipped +} +check "update exits inside update_script: cleanup runs" "ran" "$(run_exit 1 0)" +check "failed update: no cleanup" "skipped" "$(run_exit 1 1)" +check "not an update: no cleanup" "skipped" "$(run_exit "" 0)" + +# An empty PATH stands in for a distro without apt-get, and keeps cleanup_lxc's find away from /tmp +mkdir "$TEST_DIR/bin" +result=$( + PATH="$TEST_DIR/bin" + is_alpine() { return 1; } + pkg_clean() { echo pkg_clean; } + cleanup_lxc 2>/dev/null +) +check "no apt-get: cleanup_lxc cleans through pkg_clean" "pkg_clean" "$result" +check "install.func does not shadow core.func's cleanup_lxc" "" "$(grep -n '^cleanup_lxc()' "$CORE_DIR/lxc/install.func" || true)" + +printf '%s passed, %s failed\n' "$passed" "$failed" +((failed == 0)) diff --git a/ui/menu.func b/ui/menu.func index fd9c170..b2eb5c3 100644 --- a/ui/menu.func +++ b/ui/menu.func @@ -1115,8 +1115,8 @@ check_breaking_change_guard() { # The update path, shared by the three branches of start() that reach it. # exit, not return: returning continues into the ct script's host-only tail. -# Most ct scripts exit inside update_script, so the three calls after it are -# unreachable for them. +# Most ct scripts exit inside update_script, so the two calls after it are +# unreachable for them; the cleanup therefore runs from on_exit. _cs_run_update() { ensure_profile_loaded ensure_recorded_toolchains @@ -1126,10 +1126,10 @@ _cs_run_update() { check_container_os_guard || exit 0 check_breaking_change_guard || exit 0 apt_conffile_guard + _CS_UPDATE_CLEANUP=1 update_script run_addon_updates update_motd_ip - cleanup_lxc exit 0 }