From 1b704ef272dfc3bf132db88cf44ba2ad3f7a69cd Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:26:45 +0200 Subject: [PATCH 1/6] Resume nested message blocks and add random_password --- core/core.func | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/core.func b/core/core.func index 8af1637..aecd042 100644 --- a/core/core.func +++ b/core/core.func @@ -1031,6 +1031,28 @@ stop_spinner() { stty sane 2>/dev/null || true } +# msg_info opens a block, msg_ok closes it. A helper that opens a block of its +# own in between (setup_uv, setup_nodejs, fetch_and_deploy_*) used to leave the +# outer one dead: the inner msg_info stopped its spinner and nothing brought it +# back, so the outer msg_ok reported on a block that had been invisible since. +declare -ga _MSG_BLOCKS=() + +_msg_block_resume() { + ((${#_MSG_BLOCKS[@]})) || return 0 + SPINNER_MSG="${_MSG_BLOCKS[-1]}" + + if is_verbose_mode || is_alpine; then + printf "\r\e[2K%s %b" "${TAB}⏳${TAB}" "${YW}${SPINNER_MSG}${CL}" >&2 + return 0 + fi + + color_spinner + spinner & + SPINNER_PID=$! + echo "$SPINNER_PID" >/tmp/.spinner.pid + disown "$SPINNER_PID" 2>/dev/null || true +} + # ------------------------------------------------------------------------------ # _cs_os_family() # @@ -1161,6 +1183,7 @@ msg_info() { stop_spinner SPINNER_MSG="$msg" + _MSG_BLOCKS+=("$msg") if is_verbose_mode || is_alpine; then local HOURGLASS="${TAB}⏳${TAB}" @@ -1200,12 +1223,14 @@ msg_ok() { local msg="$1" [[ -z "$msg" ]] && return stop_spinner + ((${#_MSG_BLOCKS[@]})) && unset '_MSG_BLOCKS[-1]' clear_line echo -e "$CM${GN}${msg}${CL}$(_dev_step_end "$msg")" log_msg "[OK] $msg" local sanitized_key sanitized_key=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g') unset 'MSG_INFO_SHOWN['"$sanitized_key"']' 2>/dev/null || true + _msg_block_resume } # ------------------------------------------------------------------------------ @@ -1218,6 +1243,7 @@ msg_ok() { # ------------------------------------------------------------------------------ msg_error() { stop_spinner + _MSG_BLOCKS=() local msg="$1" echo -e "${BFR:-}${CROSS:-✖️}${RD}${msg}${CL}" >&2 log_msg "[ERROR] $msg" @@ -1233,6 +1259,7 @@ msg_error() { # ------------------------------------------------------------------------------ msg_warn() { stop_spinner + _MSG_BLOCKS=() local msg="$1" echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2 log_msg "[WARN] $msg" @@ -2043,6 +2070,21 @@ prompt_select() { # # Require user input (no default) # db_pass=$(prompt_password "Database password:" "" 60 12) # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +# random_password [length] +# +# Alphanumeric on purpose: base64 output carries / and +, which break the DSN +# and URL strings these passwords are pasted into. Reads a fixed chunk instead +# of piping /dev/urandom into head, so nothing takes a SIGPIPE under pipefail. +# ------------------------------------------------------------------------------ +random_password() { + local len="${1:-24}" out="" + while ((${#out} < len)); do + out+="$(LC_ALL=C tr -dc 'A-Za-z0-9' < <(head -c 256 /dev/urandom))" + done + printf '%s' "${out:0:len}" +} + prompt_password() { local message="${1:-Enter password:}" local default="${2:-}" From b80a66185fb517f9f980bef809605d7d95fd153e Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:26:45 +0200 Subject: [PATCH 2/6] Add CLEAN_INSTALL_KEEP to spare paths from a clean install --- lib/forge.func | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/lib/forge.func b/lib/forge.func index 7e79eab..64b58d6 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -470,21 +470,47 @@ _download_source_tarball() { # directory" for entries deleted on purpose. A file rather than an array, # because node_modules reaches tens of thousands of paths. _cs_clean_target_dir() { - local dir="${1:?directory}" list + local dir="${1:?directory}" list entry stash="" [[ -d "$dir" ]] || return 0 + dir="${dir%/}" + + # CLEAN_INSTALL_KEEP holds paths relative to the target that survive the wipe. + # Moved aside rather than pruned from find: a kept path can be nested, and + # removing its parent would take it along. mv also keeps a symlink a symlink, + # which is the case that brought this about. + local -a keep=() + [[ -n "${CLEAN_INSTALL_KEEP:-}" ]] && read -ra keep <<<"$CLEAN_INSTALL_KEEP" + if ((${#keep[@]})); then + stash="$(mktemp -d -p "$(dirname "$dir")" .clean-keep.XXXXXX)" || return 1 + for entry in "${keep[@]}"; do + entry="${entry#/}" + [[ -n "$entry" && -e "$dir/$entry" ]] || continue + mkdir -p "$stash/$(dirname "$entry")" + mv "$dir/$entry" "$stash/$entry" || msg_warn "Could not preserve ${entry}" + done + fi # mountpoint lives in util-linux, which a minimal Alpine may not have. Without # it the -exec below would fail once per directory, and there is nothing to # protect anyway -- so drop the top-level entries and let rm recurse. if ! command -v mountpoint >/dev/null 2>&1; then find "${dir:?}" -mindepth 1 -maxdepth 1 -print0 | xargs -0 rm -rf -- - return 0 + else + list="$(mktemp)" || return 1 + find "${dir:?}" -mindepth 1 \( -type d -exec mountpoint -q {} \; -prune \) -o -print0 >"$list" + xargs -0 rm -rf -- <"$list" + rm -f "$list" + fi + + if [[ -n "$stash" ]]; then + for entry in "${keep[@]}"; do + entry="${entry#/}" + [[ -n "$entry" && -e "$stash/$entry" ]] || continue + mkdir -p "$dir/$(dirname "$entry")" + mv "$stash/$entry" "$dir/$entry" || msg_warn "Could not restore ${entry}" + done + rm -rf "$stash" fi - - list="$(mktemp)" || return 1 - find "${dir:?}" -mindepth 1 \( -type d -exec mountpoint -q {} \; -prune \) -o -print0 >"$list" - xargs -0 rm -rf -- <"$list" - rm -f "$list" } # ------------------------------------------------------------------------------ @@ -498,6 +524,8 @@ _cs_clean_target_dir() { # - Honors CLEAN_INSTALL=1 (wipes first, dotfiles included, mount # points preserved — back up config dotfiles like .env via create_backup # and call restore_backup BEFORE any build step that sources them). +# - Honors CLEAN_INSTALL_KEEP, a space-separated list of paths relative to +# that the wipe leaves alone (e.g. "geodata .env data/uploads"). # - Does NOT own : the caller creates it and is responsible for its # cleanup (typically via a RETURN trap on its tmpdir). # - cp failures are non-fatal here, matching the previous inline behavior. @@ -543,6 +571,8 @@ _deploy_source_tarball() { # - Honors CLEAN_INSTALL=1 (wipes first, dotfiles included, mount # points preserved — back up config dotfiles like .env via create_backup # and call restore_backup BEFORE any build step that sources them). +# - Honors CLEAN_INSTALL_KEEP, a space-separated list of paths relative to +# that the wipe leaves alone (e.g. "geodata .env data/uploads"). # - Does NOT own : the caller creates it and cleans it up. # # Returns: 0 on success, 65 on unsupported format, 251 on extraction failure, From e5626a46bfa6c0f9b010d9098db9d5d4a3ac6834 Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:26:45 +0200 Subject: [PATCH 3/6] Raise cargo, uv and composer network limits --- lib/runtime.func | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/runtime.func b/lib/runtime.func index 92b73ac..fb8258f 100644 --- a/lib/runtime.func +++ b/lib/runtime.func @@ -71,6 +71,7 @@ setup_composer() { $STD "$COMPOSER_BIN" self-update --no-interaction || { msg_warn "Composer self-update failed, continuing with current version" } + "$COMPOSER_BIN" config --global process-timeout 600 >/dev/null 2>&1 || true local UPDATED_VERSION UPDATED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}') cache_installed_version "composer" "$UPDATED_VERSION" @@ -107,6 +108,8 @@ setup_composer() { fi chmod +x "$COMPOSER_BIN" + # 300s is composer's default and not enough for a large vendor tree. + "$COMPOSER_BIN" config --global process-timeout 600 >/dev/null 2>&1 || true $STD "$COMPOSER_BIN" self-update --no-interaction || { msg_warn "Composer self-update failed after fresh install" } @@ -2262,6 +2265,15 @@ EOF chmod +x /etc/profile.d/99-cargo.sh } +# cargo gives up after one retry, which a container on a busy host loses to +# regularly. Appended rather than written, so an existing config survives. +_cs_write_cargo_net() { + local cfg="${CARGO_HOME:-${HOME:-/root}/.cargo}/config.toml" + grep -q '^\[net\]' "$cfg" 2>/dev/null && return 0 + mkdir -p "$(dirname "$cfg")" + printf '\n[net]\nretry = 5\n' >>"$cfg" +} + # A source tree with rust-toolchain.toml makes rustup install that exact # version on build and never remove it. Keep the default, drop the rest. rust_prune_toolchains() { @@ -2359,6 +2371,8 @@ setup_rust() { msg_ok "Update Rust $RUST_VERSION" fi + _cs_write_cargo_net + # Install global crates if [[ -n "$RUST_CRATES" ]]; then msg_info "Processing Rust crates: $RUST_CRATES" @@ -2493,6 +2507,9 @@ _uv_required_version() { # UV_PROJECT_DIR - optional project directory; its uv pin wins over the latest # release. Set as a prefix, like PYTHON_VERSION and UV_VERSION. setup_uv() { + # uv has no config key for this; its default of 30s is too little for a large + # wheel over a slow link. + export UV_HTTP_TIMEOUT="${UV_HTTP_TIMEOUT:-600}" local PROJECT_DIR="${UV_PROJECT_DIR:-}" local UV_BIN="/usr/local/bin/uv" local UVX_BIN="/usr/local/bin/uvx" From 2e4c1d1d31b06d66d206d7fe47492be4cdad8f4a Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:26:18 +0200 Subject: [PATCH 4/6] Trim comments --- core/core.func | 15 ++++----------- lib/forge.func | 4 +--- lib/runtime.func | 7 ++----- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/core/core.func b/core/core.func index aecd042..9f297e1 100644 --- a/core/core.func +++ b/core/core.func @@ -1031,10 +1031,8 @@ stop_spinner() { stty sane 2>/dev/null || true } -# msg_info opens a block, msg_ok closes it. A helper that opens a block of its -# own in between (setup_uv, setup_nodejs, fetch_and_deploy_*) used to leave the -# outer one dead: the inner msg_info stopped its spinner and nothing brought it -# back, so the outer msg_ok reported on a block that had been invisible since. +# A helper opening its own block (setup_uv, setup_nodejs) stopped the outer +# one's spinner and nothing brought it back. declare -ga _MSG_BLOCKS=() _msg_block_resume() { @@ -2070,13 +2068,8 @@ prompt_select() { # # Require user input (no default) # db_pass=$(prompt_password "Database password:" "" 60 12) # ------------------------------------------------------------------------------ -# ------------------------------------------------------------------------------ -# random_password [length] -# -# Alphanumeric on purpose: base64 output carries / and +, which break the DSN -# and URL strings these passwords are pasted into. Reads a fixed chunk instead -# of piping /dev/urandom into head, so nothing takes a SIGPIPE under pipefail. -# ------------------------------------------------------------------------------ +# random_password [length] - alphanumeric, because base64 carries / and +. +# Reads a fixed chunk rather than piping urandom into head, which SIGPIPEs. random_password() { local len="${1:-24}" out="" while ((${#out} < len)); do diff --git a/lib/forge.func b/lib/forge.func index 64b58d6..ea19b4f 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -474,10 +474,8 @@ _cs_clean_target_dir() { [[ -d "$dir" ]] || return 0 dir="${dir%/}" - # CLEAN_INSTALL_KEEP holds paths relative to the target that survive the wipe. # Moved aside rather than pruned from find: a kept path can be nested, and - # removing its parent would take it along. mv also keeps a symlink a symlink, - # which is the case that brought this about. + # removing its parent would take it along. mv also keeps a symlink a symlink. local -a keep=() [[ -n "${CLEAN_INSTALL_KEEP:-}" ]] && read -ra keep <<<"$CLEAN_INSTALL_KEEP" if ((${#keep[@]})); then diff --git a/lib/runtime.func b/lib/runtime.func index fb8258f..96e8bf6 100644 --- a/lib/runtime.func +++ b/lib/runtime.func @@ -108,7 +108,6 @@ setup_composer() { fi chmod +x "$COMPOSER_BIN" - # 300s is composer's default and not enough for a large vendor tree. "$COMPOSER_BIN" config --global process-timeout 600 >/dev/null 2>&1 || true $STD "$COMPOSER_BIN" self-update --no-interaction || { msg_warn "Composer self-update failed after fresh install" @@ -2265,8 +2264,7 @@ EOF chmod +x /etc/profile.d/99-cargo.sh } -# cargo gives up after one retry, which a container on a busy host loses to -# regularly. Appended rather than written, so an existing config survives. +# Appended, not written, so an existing config survives. _cs_write_cargo_net() { local cfg="${CARGO_HOME:-${HOME:-/root}/.cargo}/config.toml" grep -q '^\[net\]' "$cfg" 2>/dev/null && return 0 @@ -2507,8 +2505,7 @@ _uv_required_version() { # UV_PROJECT_DIR - optional project directory; its uv pin wins over the latest # release. Set as a prefix, like PYTHON_VERSION and UV_VERSION. setup_uv() { - # uv has no config key for this; its default of 30s is too little for a large - # wheel over a slow link. + # uv has no config key for this, only the env var. export UV_HTTP_TIMEOUT="${UV_HTTP_TIMEOUT:-600}" local PROJECT_DIR="${UV_PROJECT_DIR:-}" local UV_BIN="/usr/local/bin/uv" From 1021467eaae3806c50395eac37e64b82af25789c Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:27:27 +0200 Subject: [PATCH 5/6] Add _cs_write_cargo_net to API.txt --- lib/API.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/API.txt b/lib/API.txt index 7aa63f9..639b58c 100644 --- a/lib/API.txt +++ b/lib/API.txt @@ -5,6 +5,7 @@ _cs_apt_install_optional _cs_clean_target_dir _cs_drop_pm_shim _cs_repair_dpkg +_cs_write_cargo_net _cs_write_cargo_profile _deploy_source_tarball _deploy_unpacked_archive From ccafe16bc75d2930ba37eae259c5b0ad6c90f46b Mon Sep 17 00:00:00 2001 From: MickLesk <47820557+MickLesk@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:30:31 +0200 Subject: [PATCH 6/6] Add the two new core functions to ui/API.txt --- ui/API.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/API.txt b/ui/API.txt index e30d22f..8dd223c 100644 --- a/ui/API.txt +++ b/ui/API.txt @@ -37,6 +37,7 @@ _is_whitelisted_key _list_os_versions _list_templates _load_vars_file_to_map +_msg_block_resume _msg_fit _pve_version _pveam_available @@ -158,6 +159,7 @@ prompt_input_required prompt_password prompt_select pve_check +random_password reclaim_tty require_debian_like require_pve_host