diff --git a/scripts/install.sh b/scripts/install.sh index 83689cda0252..99d901d76a06 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -49,10 +49,12 @@ trap abort_install_int INT trap abort_install_term TERM mktempfile() { - local f + local output_var="${1:?output variable required}" f f="$(mktemp)" + # Assign into caller scope; command substitution would lose this cleanup + # registration in its subshell. TMPFILES+=("$f") - echo "$f" + printf -v "$output_var" '%s' "$f" } resolve_openclaw_effective_home() { @@ -117,7 +119,7 @@ download_file() { run_remote_bash() { local url="$1" local tmp - tmp="$(mktempfile)" + mktempfile tmp download_file "$url" "$tmp" /bin/bash "$tmp" } @@ -479,8 +481,8 @@ run_with_spinner() { if [[ -n "$GUM" ]] && gum_is_tty && ! is_shell_function "${1:-}"; then local gum_err gum_out - gum_err="$(mktempfile)" - gum_out="$(mktempfile)" + mktempfile gum_err + mktempfile gum_out if "$GUM" spin --spinner dot --title "$title" -- "$@" >"$gum_out" 2>"$gum_err"; then if is_gum_raw_mode_failure "$gum_out" || is_gum_raw_mode_failure "$gum_err"; then GUM="" @@ -523,7 +525,7 @@ run_quiet_step() { fi local log - log="$(mktempfile)" + mktempfile log local showed_progress=false local cmd_exit=0 @@ -1012,7 +1014,7 @@ print_npm_failure_diagnostics() { install_openclaw_npm() { local spec="$1" local log - log="$(mktempfile)" + mktempfile log if ! run_npm_global_install "$spec" "$log"; then local attempted_build_tool_fix=false if auto_install_build_tools_for_npm_failure "$log"; then @@ -1860,7 +1862,7 @@ install_node() { ui_info "Installing Node.js via NodeSource" if command -v apt-get &> /dev/null; then local tmp - tmp="$(mktempfile)" + mktempfile tmp run_required_step "Downloading NodeSource setup script" download_file "https://deb.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp" if is_root; then run_required_step "Configuring NodeSource repository" bash "$tmp" @@ -1871,7 +1873,7 @@ install_node() { fi elif command -v dnf &> /dev/null; then local tmp - tmp="$(mktempfile)" + mktempfile tmp run_required_step "Downloading NodeSource setup script" download_file "https://rpm.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp" if is_root; then run_required_step "Configuring NodeSource repository" bash "$tmp" @@ -1882,7 +1884,7 @@ install_node() { fi elif command -v yum &> /dev/null; then local tmp - tmp="$(mktempfile)" + mktempfile tmp run_required_step "Downloading NodeSource setup script" download_file "https://rpm.nodesource.com/setup_${NODE_DEFAULT_MAJOR}.x" "$tmp" if is_root; then run_required_step "Configuring NodeSource repository" bash "$tmp" diff --git a/test/scripts/install-sh.test.ts b/test/scripts/install-sh.test.ts index afe2d4f20479..e463b74448eb 100644 --- a/test/scripts/install-sh.test.ts +++ b/test/scripts/install-sh.test.ts @@ -1,6 +1,14 @@ // Install Sh tests cover install sh script behavior. import { spawnSync } from "node:child_process"; -import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + existsSync, + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; @@ -50,6 +58,30 @@ describe("install.sh", () => { } }); + it("removes a downloaded script temp file when remote execution fails", () => { + const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-remote-cleanup-")); + const tempFile = join(tmp, "remote-script.sh"); + + try { + const result = runInstallShell( + [ + "set -euo pipefail", + `source ${JSON.stringify(SCRIPT_PATH)}`, + 'mktemp() { : > "$PROBE_PATH"; printf \'%s\\n\' "$PROBE_PATH"; }', + "download_file() { printf '#!/bin/bash\\nexit 42\\n' > \"$2\"; }", + 'run_remote_bash "https://example.invalid/setup.sh"', + ].join("\n"), + { PROBE_PATH: tempFile }, + ); + + expect(result.status).toBe(42); + expect(existsSync(tempFile)).toBe(false); + expect(script).not.toMatch(/\$\(\s*mktempfile\s*\)/); + } finally { + rmSync(tmp, { force: true, recursive: true }); + } + }); + it("runs apt-get through noninteractive wrappers", () => { expect(script).toContain("apt_get()"); expect(script).toContain('DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"');