Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions core/core.func
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,26 @@ stop_spinner() {
stty sane 2>/dev/null || true
}

# 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() {
((${#_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()
#
Expand Down Expand Up @@ -1161,6 +1181,7 @@ msg_info() {

stop_spinner
SPINNER_MSG="$msg"
_MSG_BLOCKS+=("$msg")

if is_verbose_mode || is_alpine; then
local HOURGLASS="${TAB}⏳${TAB}"
Expand Down Expand Up @@ -1200,12 +1221,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
}

# ------------------------------------------------------------------------------
Expand All @@ -1218,6 +1241,7 @@ msg_ok() {
# ------------------------------------------------------------------------------
msg_error() {
stop_spinner
_MSG_BLOCKS=()
local msg="$1"
echo -e "${BFR:-}${CROSS:-✖️}${RD}${msg}${CL}" >&2
log_msg "[ERROR] $msg"
Expand All @@ -1233,6 +1257,7 @@ msg_error() {
# ------------------------------------------------------------------------------
msg_warn() {
stop_spinner
_MSG_BLOCKS=()
local msg="$1"
echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2
log_msg "[WARN] $msg"
Expand Down Expand Up @@ -2043,6 +2068,16 @@ prompt_select() {
# # Require user input (no default)
# db_pass=$(prompt_password "Database password:" "" 60 12)
# ------------------------------------------------------------------------------
# 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
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:-}"
Expand Down
1 change: 1 addition & 0 deletions lib/API.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 35 additions & 7 deletions lib/forge.func
Original file line number Diff line number Diff line change
Expand Up @@ -470,21 +470,45 @@ _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%/}"

# 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.
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"
}

# ------------------------------------------------------------------------------
Expand All @@ -498,6 +522,8 @@ _cs_clean_target_dir() {
# - Honors CLEAN_INSTALL=1 (wipes <target> 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
# <target> that the wipe leaves alone (e.g. "geodata .env data/uploads").
# - Does NOT own <workdir>: 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.
Expand Down Expand Up @@ -543,6 +569,8 @@ _deploy_source_tarball() {
# - Honors CLEAN_INSTALL=1 (wipes <target> 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
# <target> that the wipe leaves alone (e.g. "geodata .env data/uploads").
# - Does NOT own <workdir>: the caller creates it and cleans it up.
#
# Returns: 0 on success, 65 on unsupported format, 251 on extraction failure,
Expand Down
14 changes: 14 additions & 0 deletions lib/runtime.func
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -107,6 +108,7 @@ setup_composer() {
fi

chmod +x "$COMPOSER_BIN"
"$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"
}
Expand Down Expand Up @@ -2262,6 +2264,14 @@ EOF
chmod +x /etc/profile.d/99-cargo.sh
}

# 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
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() {
Expand Down Expand Up @@ -2359,6 +2369,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"
Expand Down Expand Up @@ -2493,6 +2505,8 @@ _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, 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"
local UVX_BIN="/usr/local/bin/uvx"
Expand Down
2 changes: 2 additions & 0 deletions ui/API.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -158,6 +159,7 @@ prompt_input_required
prompt_password
prompt_select
pve_check
random_password
reclaim_tty
require_debian_like
require_pve_host
Expand Down
Loading