diff --git a/scripts/install.sh b/scripts/install.sh index 3bcc0478..a81f4d46 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -137,6 +137,9 @@ check_existing_installation() { abort_unverified() { echo -e "${RED}✗ Refusing to install an unverified binary.${NC}" + if [[ -x "$INSTALL_DIR/strix" || -x "$INSTALL_DIR/strix.exe" ]]; then + echo -e "${MUTED}Existing Strix installation left unchanged.${NC}" + fi echo -e "${RED}Re-run with STRIX_INSTALL_SKIP_VERIFY=1 to override (at your own risk).${NC}" exit 1 } @@ -271,7 +274,20 @@ download_and_install() { print_message info "\n${CYAN}🦉 Installing Strix${NC} ${MUTED}version: ${NC}$specific_version" print_message info "${MUTED}Platform: ${NC}$target\n" - local tmp_dir=$(mktemp -d) + local tmp_dir + tmp_dir=$(mktemp -d) + + # Never leave a half-written binary in INSTALL_DIR. Stage to *.new and only + # rename into place after a verified archive has been extracted. On any + # abort (including verification failure), remove the staging file and the + # download temp dir; the current install stays untouched. + cleanup_install_temps() { + cd / >/dev/null 2>&1 || true + rm -rf "$tmp_dir" + rm -f "$INSTALL_DIR/strix.new" "$INSTALL_DIR/strix.exe.new" + } + trap cleanup_install_temps EXIT + cd "$tmp_dir" echo -e "${MUTED}Downloading...${NC}" @@ -303,15 +319,17 @@ download_and_install() { echo -e "${MUTED}Extracting...${NC}" if [ "$os" = "windows" ]; then unzip -q "$filename" - mv "strix-${specific_version}-${target}.exe" "$INSTALL_DIR/strix.exe" + mv "strix-${specific_version}-${target}.exe" "$INSTALL_DIR/strix.exe.new" + mv -f "$INSTALL_DIR/strix.exe.new" "$INSTALL_DIR/strix.exe" else tar -xzf "$filename" - mv "strix-${specific_version}-${target}" "$INSTALL_DIR/strix" - chmod 755 "$INSTALL_DIR/strix" + mv "strix-${specific_version}-${target}" "$INSTALL_DIR/strix.new" + chmod 755 "$INSTALL_DIR/strix.new" + mv -f "$INSTALL_DIR/strix.new" "$INSTALL_DIR/strix" fi - cd - > /dev/null - rm -rf "$tmp_dir" + trap - EXIT + cleanup_install_temps echo -e "${GREEN}✓ Strix installed to $INSTALL_DIR${NC}" } diff --git a/tests/test_install_script.py b/tests/test_install_script.py index 2bde6820..eec83ba9 100644 --- a/tests/test_install_script.py +++ b/tests/test_install_script.py @@ -77,9 +77,13 @@ while [ "$#" -gt 0 ]; do done case "$url" in */SHA256SUMS) - hash=$(sha256sum "$STRIX_TEST_ARCHIVE" | awk '{print $1}') name=$(basename "$STRIX_TEST_ARCHIVE") - printf '%s %s\\n' "$hash" "$name" > "$output" + if [ -n "${STRIX_TEST_BAD_CHECKSUM:-}" ]; then + printf '%s %s\\n' "0" "$name" > "$output" + else + hash=$(sha256sum "$STRIX_TEST_ARCHIVE" | awk '{print $1}') + printf '%s %s\\n' "$hash" "$name" > "$output" + fi ;; *.intoto.jsonl) printf '{"test":true}\\n' > "$output" @@ -178,3 +182,36 @@ def test_installer_rejects_unsupported_architecture(tmp_path: Path) -> None: assert "Unsupported OS/Arch: linux/riscv64" in result.stdout assert not curl_log_path.exists() assert not (home_path / ".strix").exists() + + +def test_installer_leaves_existing_install_on_checksum_failure(tmp_path: Path) -> None: + repository_root = Path(__file__).resolve().parents[1] + archive_path = _create_release_archive(tmp_path) + mock_bin = _create_mock_commands(tmp_path, machine="aarch64") + environment, home_path, _curl_log_path = _create_installer_environment( + tmp_path, + archive_path, + mock_bin, + ) + environment["STRIX_TEST_BAD_CHECKSUM"] = "1" + + install_dir = home_path / ".strix" / "bin" + install_dir.mkdir(parents=True) + existing = install_dir / "strix" + _write_executable(existing, "#!/bin/sh\nprintf 'strix 1.0.0\\n'\n") + before = existing.read_bytes() + + result = _run_installer(repository_root, environment) + + assert result.returncode != 0 + assert "Checksum mismatch" in result.stdout + assert "Existing Strix installation left unchanged" in result.stdout + assert existing.read_bytes() == before + assert not (install_dir / "strix.new").exists() + installed_result = subprocess.run( # noqa: S603 + [str(existing), "--version"], + capture_output=True, + text=True, + check=True, + ) + assert installed_result.stdout.strip() == "strix 1.0.0"