From 09f97c1585683b06a3012c25cde460b090dc1bab Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 12:31:53 +0100 Subject: [PATCH 01/30] ci: enable ARM builds --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cb6149..11707ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,18 +30,18 @@ jobs: target: x86_64-unknown-linux-gnu platform: linux arch: amd64 - # - os: depot-ubuntu-22.04-arm - # target: aarch64-unknown-linux-gnu - # platform: linux - # arch: arm64 + - os: depot-ubuntu-22.04-arm + target: aarch64-unknown-linux-gnu + platform: linux + arch: arm64 - os: depot-ubuntu-22.04 target: x86_64-unknown-linux-musl platform: alpine arch: amd64 - # - os: depot-ubuntu-22.04-arm - # target: aarch64-unknown-linux-musl - # platform: alpine - # arch: arm64 + - os: depot-ubuntu-22.04-arm + target: aarch64-unknown-linux-musl + platform: alpine + arch: arm64 - os: macos-14 target: x86_64-apple-darwin platform: darwin From bc096e470d5bef34b3d4b900575e45a31bd5d557 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 11:23:44 +0100 Subject: [PATCH 02/30] feat: expand test matrix to match release matrix (#23) - Add all release targets to CI test matrix (musl, arm64) - Add platform and arch labels to matrix - Install musl-tools and cross-compilation tools for musl targets - Use BASE_DIR/FOUNDRY_DIR pattern matching foundry for XDG support - Add --yes alias for -y flag - Fix test to unset XDG_CONFIG_HOME --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 555cec3..5580e6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,18 +30,18 @@ jobs: target: x86_64-unknown-linux-gnu platform: linux arch: amd64 - # - os: depot-ubuntu-22.04-arm - # target: aarch64-unknown-linux-gnu - # platform: linux - # arch: arm64 + - os: depot-ubuntu-22.04-arm + target: aarch64-unknown-linux-gnu + platform: linux + arch: arm64 - os: depot-ubuntu-22.04 target: x86_64-unknown-linux-musl platform: alpine arch: amd64 - # - os: depot-ubuntu-22.04-arm - # target: aarch64-unknown-linux-musl - # platform: alpine - # arch: arm64 + - os: depot-ubuntu-22.04-arm + target: aarch64-unknown-linux-musl + platform: alpine + arch: arm64 - os: macos-14 target: x86_64-apple-darwin platform: darwin From 35c93339d860eabaca0655fda254b7bb84b7bf3e Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 10:43:58 +0100 Subject: [PATCH 03/30] feat: add build attestations and verification - Update release workflow to generate attestations using actions/attest-build-provenance@v3 - Record attestation URLs to .attestation.txt files included in releases - Add attestation verification to foundryup-init.sh installer script - Support --force/-f flag and FOUNDRYUP_SKIP_VERIFY env var to skip verification --- .github/workflows/release.yml | 31 ++++++++++++- foundryup-init.sh | 83 +++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20a9bad..9a58897 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,7 @@ name: Release +permissions: {} + on: push: tags: @@ -10,6 +12,10 @@ env: jobs: build: + permissions: + id-token: write + contents: read + attestations: write runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: @@ -74,21 +80,42 @@ jobs: run: cargo build --profile dist --target ${{ matrix.target }} - name: Prepare artifact + id: artifacts shell: bash run: | if [[ "${{ matrix.platform }}" == "win32" ]]; then exe=".exe" fi - cp target/${{ matrix.target }}/dist/foundryup${exe} foundryup_${{ matrix.platform }}_${{ matrix.arch }} + bin_name="foundryup_${{ matrix.platform }}_${{ matrix.arch }}${exe}" + cp target/${{ matrix.target }}/dist/foundryup${exe} "$bin_name" + printf 'bin_name=%s\n' "$bin_name" >> "$GITHUB_OUTPUT" + printf 'attestation_file=%s\n' "foundryup_${{ matrix.platform }}_${{ matrix.arch }}.attestation.txt" >> "$GITHUB_OUTPUT" + + - name: Generate attestation + id: attestation + uses: actions/attest-build-provenance@v3 + with: + subject-path: ${{ steps.artifacts.outputs.bin_name }} + + - name: Record attestation URL + env: + ATTESTATION_URL: ${{ steps.attestation.outputs.attestation-url }} + ATTESTATION_FILE: ${{ steps.artifacts.outputs.attestation_file }} + shell: bash + run: printf '%s\n' "$ATTESTATION_URL" > "$ATTESTATION_FILE" - name: Upload artifact uses: actions/upload-artifact@v6 with: name: foundryup_${{ matrix.platform }}_${{ matrix.arch }} - path: foundryup_${{ matrix.platform }}_${{ matrix.arch }} + path: | + ${{ steps.artifacts.outputs.bin_name }} + ${{ steps.artifacts.outputs.attestation_file }} if-no-files-found: error release: + permissions: + contents: write runs-on: ubuntu-latest needs: build timeout-minutes: 30 diff --git a/foundryup-init.sh b/foundryup-init.sh index 610d823..6b8a355 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -23,6 +23,7 @@ FOUNDRYUP_REPO="foundry-rs/foundryup" BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" FOUNDRY_DIR="${FOUNDRY_DIR:-$BASE_DIR/.foundry}" FOUNDRYUP_BIN_DIR="$FOUNDRY_DIR/bin" +FOUNDRYUP_SKIP_VERIFY="${FOUNDRYUP_SKIP_VERIFY:-false}" usage() { cat </dev/null; then + local _attestation_artifact_link + _attestation_artifact_link="$(head -n1 "$_attestation_file" | tr -d '\r')" + + if [ -n "$_attestation_artifact_link" ] && ! grep -q 'Not Found' "$_attestation_file"; then + say "verifying attestation..." + local _sigstore_file="${_dir}/attestation.sigstore.json" + + if downloader "${_attestation_artifact_link}/download" "$_sigstore_file" "$_arch" 2>/dev/null; then + # Extract the payload from the sigstore JSON and decode it + local _payload_b64 + local _payload_json + _payload_b64=$(awk '/"payload":/ {gsub(/[",]/, "", $2); print $2; exit}' "$_sigstore_file") + _payload_json=$(printf '%s' "$_payload_b64" | base64 -d 2>/dev/null || printf '%s' "$_payload_b64" | base64 -D 2>/dev/null || true) + + if [ -n "$_payload_json" ]; then + # Extract SHA256 hash from the payload + _expected_hash=$(printf '%s' "$_payload_json" | grep -oE '"sha256"[[:space:]]*:[[:space:]]*"[a-fA-F0-9]{64}"' | head -1 | grep -oE '[a-fA-F0-9]{64}') + fi + + rm -f "$_sigstore_file" + fi + fi + + rm -f "$_attestation_file" + fi + + if [ -z "$_expected_hash" ]; then + warn "no attestation found for this release, skipping verification" + fi + fi say "downloading foundryup..." ensure mkdir -p "$_dir" ensure downloader "$_url" "$_file" "$_arch" + + # Verify the downloaded binary against the attestation hash + if [ -n "$_expected_hash" ]; then + say "verifying binary integrity..." + local _actual_hash + _actual_hash=$(compute_sha256 "$_file") + + if [ "$_actual_hash" != "$_expected_hash" ]; then + err "hash verification failed: + expected: $_expected_hash + actual: $_actual_hash +Use --force to skip verification (INSECURE)" + fi + say "binary verified ✓" + fi + ensure chmod u+x "$_file" if [ ! -x "$_file" ]; then @@ -251,6 +318,16 @@ assert_nz() { fi } +compute_sha256() { + if check_cmd sha256sum; then + sha256sum "$1" | cut -d' ' -f1 | sed 's/^\\//' + elif check_cmd shasum; then + shasum -a 256 "$1" | cut -d' ' -f1 + else + err "need 'sha256sum' or 'shasum' for verification" + fi +} + ensure() { if ! "$@"; then err "command failed: $*" From 49a1058ddfec2bd74c54be654c3b39a75007cbb3 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 10:50:32 +0100 Subject: [PATCH 04/30] test: skip attestation verification in installer test Attestations are not yet available for current releases, so skip verification in the test to avoid failures when downloading. --- tests/it/installer_script.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 3a8b0f5..2550803 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -308,6 +308,7 @@ fn script_downloads_foundryup() { .args(["foundryup-init.sh", "-y"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("FOUNDRY_DIR", &temp_dir) + .env("FOUNDRYUP_SKIP_VERIFY", "true") .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() From 0e9aa7035c4b916be107413bdacfffd4e6ace87e Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 10:56:22 +0100 Subject: [PATCH 05/30] fix: use try_download for attestations to avoid exit on 404 The downloader function exits on 404, which caused the script to fail when attestations are not available. Use a new try_download function that silently returns non-zero on failure instead. --- foundryup-init.sh | 16 ++++++++++++++-- tests/it/installer_script.rs | 1 - 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index 6b8a355..bee549f 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -131,7 +131,8 @@ main() { say "skipping attestation verification (--force or FOUNDRYUP_SKIP_VERIFY set)" else say "downloading attestation..." - if downloader "$_attestation_url" "$_attestation_file" "$_arch" 2>/dev/null; then + # Use curl/wget directly to avoid the downloader's exit-on-404 behavior + if try_download "$_attestation_url" "$_attestation_file"; then local _attestation_artifact_link _attestation_artifact_link="$(head -n1 "$_attestation_file" | tr -d '\r')" @@ -139,7 +140,7 @@ main() { say "verifying attestation..." local _sigstore_file="${_dir}/attestation.sigstore.json" - if downloader "${_attestation_artifact_link}/download" "$_sigstore_file" "$_arch" 2>/dev/null; then + if try_download "${_attestation_artifact_link}/download" "$_sigstore_file"; then # Extract the payload from the sigstore JSON and decode it local _payload_b64 local _payload_json @@ -328,6 +329,17 @@ compute_sha256() { fi } +# Download without exiting on failure (used for optional files like attestations) +try_download() { + if check_cmd curl; then + curl --proto '=https' --tlsv1.2 --silent --fail --location "$1" --output "$2" 2>/dev/null + elif check_cmd wget; then + wget --https-only --secure-protocol=TLSv1_2 -q "$1" -O "$2" 2>/dev/null + else + return 1 + fi +} + ensure() { if ! "$@"; then err "command failed: $*" diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 2550803..3a8b0f5 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -308,7 +308,6 @@ fn script_downloads_foundryup() { .args(["foundryup-init.sh", "-y"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("FOUNDRY_DIR", &temp_dir) - .env("FOUNDRYUP_SKIP_VERIFY", "true") .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() From d0bea6bdbfd41354e4f0207a5a6f9261fac44a05 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 11:53:08 +0100 Subject: [PATCH 06/30] temporarily disable -arm until release is ready --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5580e6b..555cec3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,18 +30,18 @@ jobs: target: x86_64-unknown-linux-gnu platform: linux arch: amd64 - - os: depot-ubuntu-22.04-arm - target: aarch64-unknown-linux-gnu - platform: linux - arch: arm64 + # - os: depot-ubuntu-22.04-arm + # target: aarch64-unknown-linux-gnu + # platform: linux + # arch: arm64 - os: depot-ubuntu-22.04 target: x86_64-unknown-linux-musl platform: alpine arch: amd64 - - os: depot-ubuntu-22.04-arm - target: aarch64-unknown-linux-musl - platform: alpine - arch: arm64 + # - os: depot-ubuntu-22.04-arm + # target: aarch64-unknown-linux-musl + # platform: alpine + # arch: arm64 - os: macos-14 target: x86_64-apple-darwin platform: darwin From 4c41da644eae2960977a301a094a02d3676c24a8 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 13:04:07 +0100 Subject: [PATCH 07/30] rename FOUNDRYUP_SKIP_VERIFY to FOUNDRYUP_IGNORE_VERIFICATION --- foundryup-init.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index bee549f..b3a1f75 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -23,7 +23,7 @@ FOUNDRYUP_REPO="foundry-rs/foundryup" BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" FOUNDRY_DIR="${FOUNDRY_DIR:-$BASE_DIR/.foundry}" FOUNDRYUP_BIN_DIR="$FOUNDRY_DIR/bin" -FOUNDRYUP_SKIP_VERIFY="${FOUNDRYUP_SKIP_VERIFY:-false}" +FOUNDRYUP_IGNORE_VERIFICATION="${FOUNDRYUP_IGNORE_VERIFICATION:-false}" usage() { cat < Date: Wed, 21 Jan 2026 13:24:30 +0100 Subject: [PATCH 08/30] fix: use set -eo pipefail instead of set -u for Windows compatibility --- foundryup-init.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index b3a1f75..3a89865 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -17,7 +17,7 @@ has_local() { has_local 2>/dev/null || alias local=typeset -set -u +set -eo pipefail FOUNDRYUP_REPO="foundry-rs/foundryup" BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" From 52cf5be6af7835b3a49dcf52342173c3b56e1cbb Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 13:28:44 +0100 Subject: [PATCH 09/30] add shellcheck --- .github/scripts/shellcheck.sh | 24 ++++++++++++++++++++++++ .github/workflows/ci.yml | 14 ++++++++++++++ foundryup-init.sh | 7 ++----- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100755 .github/scripts/shellcheck.sh diff --git a/.github/scripts/shellcheck.sh b/.github/scripts/shellcheck.sh new file mode 100755 index 0000000..c36bbea --- /dev/null +++ b/.github/scripts/shellcheck.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# runs shellcheck and prints GitHub Actions annotations for each warning and error +# https://github.com/koalaman/shellcheck + +IGNORE_DIRS=( + "./.git/*" + "./target/*" +) + +ignore_args=() +for dir in "${IGNORE_DIRS[@]}"; do + ignore_args+=(-not -path "$dir") +done + +find . -name "*.sh" "${ignore_args[@]}" -exec shellcheck -f gcc {} + | \ + while IFS=: read -r file line col severity msg; do + level="warning" + [[ "$severity" == *error* ]] && level="error" + file="${file#./}" + echo "::${level} file=${file},line=${line},col=${col}::${file}:${line}:${col}:${msg}" + done + +exit "${PIPESTATUS[0]}" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5580e6b..5d91379 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,19 @@ jobs: persist-credentials: false - uses: crate-ci/typos@65120634e79d8374d1aa2f27e54baa0c364fff5a # v1 + shellcheck: + runs-on: depot-ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Shellcheck + shell: bash + run: ./.github/scripts/shellcheck.sh + clippy: runs-on: ubuntu-latest timeout-minutes: 30 @@ -131,6 +144,7 @@ jobs: needs: - test - typos + - shellcheck - clippy - rustfmt timeout-minutes: 30 diff --git a/foundryup-init.sh b/foundryup-init.sh index 3a89865..325d800 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -187,9 +187,8 @@ Use --force to skip verification (INSECURE)" ensure chmod u+x "$_file" if [ ! -x "$_file" ]; then - err "cannot execute $_file (likely because of mounting /tmp as noexec)." - err "please copy the file to a location where you can execute binaries and run ./foundryup" - exit 1 + err "cannot execute $_file (likely because of mounting /tmp as noexec). +please copy the file to a location where you can execute binaries and run ./foundryup" fi say "installing foundryup to $FOUNDRYUP_BIN_DIR..." @@ -247,7 +246,6 @@ get_architecture() { ;; *) err "unsupported OS: $_ostype" - exit 1 ;; esac @@ -265,7 +263,6 @@ get_architecture() { ;; *) err "unsupported architecture: $_cputype" - exit 1 ;; esac From f3311a972dcdd035225bef90ef61902681f6ce5f Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 13:32:16 +0100 Subject: [PATCH 10/30] just use bash --- foundryup-init.sh | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index 325d800..c5fdb94 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -1,24 +1,9 @@ -#!/bin/sh -# shellcheck shell=dash -# shellcheck disable=SC2039 # local is non-POSIX +#!/usr/bin/env bash +set -eo pipefail # This script downloads and installs foundryup, the Foundry toolchain manager. # It detects the platform, downloads the appropriate binary, and runs it. -# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local` -# extension. Note: Most shells limit `local` to 1 var per line, contra bash. - -# Some versions of ksh have no `local` keyword. Alias it to `typeset`, but -# beware this makes variables global with f()-style function syntax in ksh93. -has_local() { - # shellcheck disable=SC2034 # deliberately unused - local _has_local -} - -has_local 2>/dev/null || alias local=typeset - -set -eo pipefail - FOUNDRYUP_REPO="foundry-rs/foundryup" BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" FOUNDRY_DIR="${FOUNDRY_DIR:-$BASE_DIR/.foundry}" From 8d6f40648db5e475bcf413d066bb22b2e5231061 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 13:36:09 +0100 Subject: [PATCH 11/30] fix: update shebang test to expect bash --- tests/it/installer_script.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 3a8b0f5..a870630 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -20,7 +20,7 @@ fn run_script_function(function_body: &str) -> std::process::Output { #[test] fn script_has_shebang() { let script = include_str!("../../foundryup-init.sh"); - assert!(script.starts_with("#!/bin/sh"), "script should start with shebang"); + assert!(script.starts_with("#!/usr/bin/env bash"), "script should start with shebang"); } #[test] From d8f35c2bf2b05b3189417211c7609e4efd774da3 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 13:41:41 +0100 Subject: [PATCH 12/30] fix: use bash instead of sh in tests --- tests/it/installer_script.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index a870630..a8f8c9e 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -8,7 +8,7 @@ fn script_without_main() -> String { fn run_script_function(function_body: &str) -> std::process::Output { let script = script_without_main(); let full_script = format!("{script}\n\n{function_body}"); - Command::new("sh") + Command::new("bash") .arg("-c") .arg(&full_script) .stdout(Stdio::piped()) @@ -35,7 +35,7 @@ fn script_usage_works() { #[test] fn script_help_flag() { - let output = Command::new("sh") + let output = Command::new("bash") .args(["foundryup-init.sh", "--help"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .stdout(Stdio::piped()) @@ -50,7 +50,7 @@ fn script_help_flag() { #[test] fn script_get_architecture_linux_amd64() { let script = script_without_main(); - let output = Command::new("sh") + let output = Command::new("bash") .args([ "-c", &format!( @@ -79,7 +79,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_darwin_arm64() { let script = script_without_main(); - let output = Command::new("sh") + let output = Command::new("bash") .args([ "-c", &format!( @@ -107,7 +107,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_alpine() { let script = script_without_main(); - let output = Command::new("sh") + let output = Command::new("bash") .args([ "-c", &format!( @@ -136,7 +136,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_windows() { let script = script_without_main(); - let output = Command::new("sh") + let output = Command::new("bash") .args([ "-c", &format!( @@ -253,7 +253,7 @@ fn script_foundryup_repo_defined() { #[test] fn script_foundryup_bin_dir_default() { let script = script_without_main(); - let output = Command::new("sh") + let output = Command::new("bash") .args([ "-c", &format!( @@ -277,7 +277,7 @@ echo "$FOUNDRYUP_BIN_DIR" #[test] fn script_foundryup_bin_dir_custom() { let script = script_without_main(); - let output = Command::new("sh") + let output = Command::new("bash") .args([ "-c", &format!( @@ -304,7 +304,7 @@ fn script_downloads_foundryup() { let bin_dir = temp_dir.join("bin"); let foundryup_path = bin_dir.join("foundryup"); - let output = Command::new("sh") + let output = Command::new("bash") .args(["foundryup-init.sh", "-y"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("FOUNDRY_DIR", &temp_dir) From c660bda2ab310bf9e664f625f00e5978acf72192 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 13:54:46 +0100 Subject: [PATCH 13/30] fix: use Git Bash for installer script tests on Windows --- tests/it/installer_script.rs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index a8f8c9e..1dffee3 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -5,10 +5,28 @@ fn script_without_main() -> String { script.replace("main \"$@\" || exit 1", "") } +/// Returns the bash command to use. On Windows, uses Git Bash. +fn bash_cmd() -> Command { + #[cfg(windows)] + { + // Git Bash location on Windows + let git_bash = r"C:\Program Files\Git\bin\bash.exe"; + if std::path::Path::new(git_bash).exists() { + return Command::new(git_bash); + } + // Fallback + Command::new("bash") + } + #[cfg(not(windows))] + { + Command::new("bash") + } +} + fn run_script_function(function_body: &str) -> std::process::Output { let script = script_without_main(); let full_script = format!("{script}\n\n{function_body}"); - Command::new("bash") + bash_cmd() .arg("-c") .arg(&full_script) .stdout(Stdio::piped()) @@ -35,7 +53,7 @@ fn script_usage_works() { #[test] fn script_help_flag() { - let output = Command::new("bash") + let output = bash_cmd() .args(["foundryup-init.sh", "--help"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .stdout(Stdio::piped()) @@ -50,7 +68,7 @@ fn script_help_flag() { #[test] fn script_get_architecture_linux_amd64() { let script = script_without_main(); - let output = Command::new("bash") + let output = bash_cmd() .args([ "-c", &format!( @@ -79,7 +97,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_darwin_arm64() { let script = script_without_main(); - let output = Command::new("bash") + let output = bash_cmd() .args([ "-c", &format!( @@ -107,7 +125,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_alpine() { let script = script_without_main(); - let output = Command::new("bash") + let output = bash_cmd() .args([ "-c", &format!( @@ -136,7 +154,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_windows() { let script = script_without_main(); - let output = Command::new("bash") + let output = bash_cmd() .args([ "-c", &format!( @@ -253,7 +271,7 @@ fn script_foundryup_repo_defined() { #[test] fn script_foundryup_bin_dir_default() { let script = script_without_main(); - let output = Command::new("bash") + let output = bash_cmd() .args([ "-c", &format!( @@ -277,7 +295,7 @@ echo "$FOUNDRYUP_BIN_DIR" #[test] fn script_foundryup_bin_dir_custom() { let script = script_without_main(); - let output = Command::new("bash") + let output = bash_cmd() .args([ "-c", &format!( @@ -304,7 +322,7 @@ fn script_downloads_foundryup() { let bin_dir = temp_dir.join("bin"); let foundryup_path = bin_dir.join("foundryup"); - let output = Command::new("bash") + let output = bash_cmd() .args(["foundryup-init.sh", "-y"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("FOUNDRY_DIR", &temp_dir) From af6b61dd16982f590988eb552e9e9e4be503da66 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 14:07:42 +0100 Subject: [PATCH 14/30] fix: add debug output and strip pipefail for Windows compatibility --- tests/it/installer_script.rs | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 1dffee3..708bb0f 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -2,7 +2,10 @@ use std::process::{Command, Stdio}; fn script_without_main() -> String { let script = include_str!("../../foundryup-init.sh"); - script.replace("main \"$@\" || exit 1", "") + script + .replace("main \"$@\" || exit 1", "") + // Remove strict error handling for function tests to avoid platform differences + .replace("set -eo pipefail", "set -e") } /// Returns the bash command to use. On Windows, uses Git Bash. @@ -207,17 +210,35 @@ fn script_need_cmd_fails_for_missing() { #[test] fn script_assert_nz_success() { let output = run_script_function("assert_nz 'value' 'test' && echo 'ok'"); - assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("ok")); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "expected success, got {:?}\nstdout: {}\nstderr: {}", + output.status, + stdout, + stderr + ); + assert!(stdout.contains("ok"), "stdout missing 'ok': {}", stdout); } #[test] fn script_assert_nz_failure() { let output = run_script_function("assert_nz '' 'test'"); - assert!(!output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("assert_nz test")); + assert!( + !output.status.success(), + "expected failure, got {:?}\nstdout: {}\nstderr: {}", + output.status, + stdout, + stderr + ); + assert!( + stderr.contains("assert_nz test"), + "stderr missing 'assert_nz test': {}", + stderr + ); } #[test] From 481bcb45d7da4093151c81f67d4b5efbfc2646bc Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 14:08:36 +0100 Subject: [PATCH 15/30] fixes --- foundryup-init.sh | 2 +- tests/it/installer_script.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index c5fdb94..59c1332 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -29,7 +29,7 @@ Options: All other options are passed to foundryup after installation. Environment variables: - FOUNDRYUP_VERSION Install a specific version of foundryup + FOUNDRYUP_VERSION Install a specific version of foundryup FOUNDRYUP_IGNORE_VERIFICATION Skip attestation verification if set to "true" EOF } diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 708bb0f..2f1f61b 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -234,11 +234,7 @@ fn script_assert_nz_failure() { stdout, stderr ); - assert!( - stderr.contains("assert_nz test"), - "stderr missing 'assert_nz test': {}", - stderr - ); + assert!(stderr.contains("assert_nz test"), "stderr missing 'assert_nz test': {}", stderr); } #[test] From ed65a28afcf9a42c90b44b964e5ed858e70f2703 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 14:31:35 +0100 Subject: [PATCH 16/30] fix: normalize CRLF to LF in script tests for Windows --- tests/it/installer_script.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 2f1f61b..d0aeee2 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -3,22 +3,22 @@ use std::process::{Command, Stdio}; fn script_without_main() -> String { let script = include_str!("../../foundryup-init.sh"); script + // Normalize CRLF to LF (Windows git checkout may add CR) + .replace("\r\n", "\n") + .replace('\r', "") .replace("main \"$@\" || exit 1", "") // Remove strict error handling for function tests to avoid platform differences .replace("set -eo pipefail", "set -e") } -/// Returns the bash command to use. On Windows, uses Git Bash. -fn bash_cmd() -> Command { +/// Returns the shell command to use. +/// On Windows, uses `sh` (Git's POSIX shell) since we strip pipefail for tests. +/// On Unix, uses `bash` to match the script's shebang. +fn shell_cmd() -> Command { #[cfg(windows)] { - // Git Bash location on Windows - let git_bash = r"C:\Program Files\Git\bin\bash.exe"; - if std::path::Path::new(git_bash).exists() { - return Command::new(git_bash); - } - // Fallback - Command::new("bash") + // Use sh on Windows - it's Git's POSIX shell and handles the script better + Command::new("sh") } #[cfg(not(windows))] { @@ -29,7 +29,7 @@ fn bash_cmd() -> Command { fn run_script_function(function_body: &str) -> std::process::Output { let script = script_without_main(); let full_script = format!("{script}\n\n{function_body}"); - bash_cmd() + shell_cmd() .arg("-c") .arg(&full_script) .stdout(Stdio::piped()) @@ -56,7 +56,7 @@ fn script_usage_works() { #[test] fn script_help_flag() { - let output = bash_cmd() + let output = shell_cmd() .args(["foundryup-init.sh", "--help"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .stdout(Stdio::piped()) @@ -71,7 +71,7 @@ fn script_help_flag() { #[test] fn script_get_architecture_linux_amd64() { let script = script_without_main(); - let output = bash_cmd() + let output = shell_cmd() .args([ "-c", &format!( @@ -100,7 +100,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_darwin_arm64() { let script = script_without_main(); - let output = bash_cmd() + let output = shell_cmd() .args([ "-c", &format!( @@ -128,7 +128,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_alpine() { let script = script_without_main(); - let output = bash_cmd() + let output = shell_cmd() .args([ "-c", &format!( @@ -157,7 +157,7 @@ echo "$RETVAL" #[test] fn script_get_architecture_windows() { let script = script_without_main(); - let output = bash_cmd() + let output = shell_cmd() .args([ "-c", &format!( @@ -288,7 +288,7 @@ fn script_foundryup_repo_defined() { #[test] fn script_foundryup_bin_dir_default() { let script = script_without_main(); - let output = bash_cmd() + let output = shell_cmd() .args([ "-c", &format!( @@ -312,7 +312,7 @@ echo "$FOUNDRYUP_BIN_DIR" #[test] fn script_foundryup_bin_dir_custom() { let script = script_without_main(); - let output = bash_cmd() + let output = shell_cmd() .args([ "-c", &format!( @@ -339,7 +339,7 @@ fn script_downloads_foundryup() { let bin_dir = temp_dir.join("bin"); let foundryup_path = bin_dir.join("foundryup"); - let output = bash_cmd() + let output = shell_cmd() .args(["foundryup-init.sh", "-y"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("FOUNDRY_DIR", &temp_dir) From c09bb383b087acbbcfc5b3c95c8544f0290b60d5 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 14:47:49 +0100 Subject: [PATCH 17/30] fix: write script to temp file instead of passing via -c argument --- tests/it/installer_script.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index d0aeee2..c569911 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -27,11 +27,18 @@ fn shell_cmd() -> Command { } fn run_script_function(function_body: &str) -> std::process::Output { + use std::io::Write; + let script = script_without_main(); let full_script = format!("{script}\n\n{function_body}"); + + // Write script to temp file to avoid shell argument parsing issues on Windows + let mut temp_file = tempfile::NamedTempFile::new().unwrap(); + temp_file.write_all(full_script.as_bytes()).unwrap(); + temp_file.flush().unwrap(); + shell_cmd() - .arg("-c") - .arg(&full_script) + .arg(temp_file.path()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() From 74c54991b97f3929d091803adebbe0cb1a582bb7 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 14:59:15 +0100 Subject: [PATCH 18/30] fix: enforce LF line endings for .rs and .sh files --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..036a06d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Ensure consistent line endings across platforms +*.rs text eol=lf +*.sh text eol=lf From e501c3629fc186d8d5a9c1b11fb1979dede84986 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 15:30:24 +0100 Subject: [PATCH 19/30] fix: use temp file for all script tests to avoid Windows shell issues --- tests/it/installer_script.rs | 105 +++++++++++------------------------ 1 file changed, 33 insertions(+), 72 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index c569911..ade0dcc 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -26,15 +26,12 @@ fn shell_cmd() -> Command { } } -fn run_script_function(function_body: &str) -> std::process::Output { +/// Run a script via temp file to avoid shell argument parsing issues on Windows +fn run_script(script: &str) -> std::process::Output { use std::io::Write; - let script = script_without_main(); - let full_script = format!("{script}\n\n{function_body}"); - - // Write script to temp file to avoid shell argument parsing issues on Windows let mut temp_file = tempfile::NamedTempFile::new().unwrap(); - temp_file.write_all(full_script.as_bytes()).unwrap(); + temp_file.write_all(script.as_bytes()).unwrap(); temp_file.flush().unwrap(); shell_cmd() @@ -45,6 +42,12 @@ fn run_script_function(function_body: &str) -> std::process::Output { .unwrap() } +fn run_script_function(function_body: &str) -> std::process::Output { + let script = script_without_main(); + let full_script = format!("{script}\n\n{function_body}"); + run_script(&full_script) +} + #[test] fn script_has_shebang() { let script = include_str!("../../foundryup-init.sh"); @@ -78,11 +81,8 @@ fn script_help_flag() { #[test] fn script_get_architecture_linux_amd64() { let script = script_without_main(); - let output = shell_cmd() - .args([ - "-c", - &format!( - r#" + let full_script = format!( + r#" {script} uname() {{ case "$1" in @@ -94,12 +94,8 @@ is_musl() {{ return 1; }} get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + ); + let output = run_script(&full_script); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "linux_amd64"); } @@ -107,11 +103,8 @@ echo "$RETVAL" #[test] fn script_get_architecture_darwin_arm64() { let script = script_without_main(); - let output = shell_cmd() - .args([ - "-c", - &format!( - r#" + let full_script = format!( + r#" {script} uname() {{ case "$1" in @@ -122,12 +115,8 @@ uname() {{ get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + ); + let output = run_script(&full_script); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "darwin_arm64"); } @@ -135,11 +124,8 @@ echo "$RETVAL" #[test] fn script_get_architecture_alpine() { let script = script_without_main(); - let output = shell_cmd() - .args([ - "-c", - &format!( - r#" + let full_script = format!( + r#" {script} uname() {{ case "$1" in @@ -151,12 +137,8 @@ is_musl() {{ return 0; }} get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + ); + let output = run_script(&full_script); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "alpine_amd64"); } @@ -164,11 +146,8 @@ echo "$RETVAL" #[test] fn script_get_architecture_windows() { let script = script_without_main(); - let output = shell_cmd() - .args([ - "-c", - &format!( - r#" + let full_script = format!( + r#" {script} uname() {{ case "$1" in @@ -179,12 +158,8 @@ uname() {{ get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + ); + let output = run_script(&full_script); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "win32_amd64"); } @@ -295,23 +270,16 @@ fn script_foundryup_repo_defined() { #[test] fn script_foundryup_bin_dir_default() { let script = script_without_main(); - let output = shell_cmd() - .args([ - "-c", - &format!( - r#" + let full_script = format!( + r#" unset FOUNDRY_DIR unset XDG_CONFIG_HOME HOME=/tmp/test_home {script} echo "$FOUNDRYUP_BIN_DIR" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + ); + let output = run_script(&full_script); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("/tmp/test_home/.foundry/bin")); } @@ -319,21 +287,14 @@ echo "$FOUNDRYUP_BIN_DIR" #[test] fn script_foundryup_bin_dir_custom() { let script = script_without_main(); - let output = shell_cmd() - .args([ - "-c", - &format!( - r#" + let full_script = format!( + r#" FOUNDRY_DIR=/custom/path {script} echo "$FOUNDRYUP_BIN_DIR" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + ); + let output = run_script(&full_script); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("/custom/path/bin")); } From 0d8fddc8778a7ce06ad4abe0785b8d224c574494 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 15:34:36 +0100 Subject: [PATCH 20/30] fix: use POSIX sh instead of bash for better portability --- foundryup-init.sh | 4 ++-- tests/it/installer_script.rs | 16 ++-------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index 59c1332..c7f880c 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -1,5 +1,5 @@ -#!/usr/bin/env bash -set -eo pipefail +#!/bin/sh +set -eu # This script downloads and installs foundryup, the Foundry toolchain manager. # It detects the platform, downloads the appropriate binary, and runs it. diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index ade0dcc..e85eee7 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -7,23 +7,11 @@ fn script_without_main() -> String { .replace("\r\n", "\n") .replace('\r', "") .replace("main \"$@\" || exit 1", "") - // Remove strict error handling for function tests to avoid platform differences - .replace("set -eo pipefail", "set -e") } /// Returns the shell command to use. -/// On Windows, uses `sh` (Git's POSIX shell) since we strip pipefail for tests. -/// On Unix, uses `bash` to match the script's shebang. fn shell_cmd() -> Command { - #[cfg(windows)] - { - // Use sh on Windows - it's Git's POSIX shell and handles the script better - Command::new("sh") - } - #[cfg(not(windows))] - { - Command::new("bash") - } + Command::new("sh") } /// Run a script via temp file to avoid shell argument parsing issues on Windows @@ -51,7 +39,7 @@ fn run_script_function(function_body: &str) -> std::process::Output { #[test] fn script_has_shebang() { let script = include_str!("../../foundryup-init.sh"); - assert!(script.starts_with("#!/usr/bin/env bash"), "script should start with shebang"); + assert!(script.starts_with("#!/bin/sh"), "script should start with shebang"); } #[test] From 20e9926468a91dd84948214a205fefefdc6be54a Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 15:37:51 +0100 Subject: [PATCH 21/30] refactor: simplify tests by removing temp file approach --- tests/it/installer_script.rs | 77 +++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index e85eee7..86bae7e 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -9,33 +9,18 @@ fn script_without_main() -> String { .replace("main \"$@\" || exit 1", "") } -/// Returns the shell command to use. -fn shell_cmd() -> Command { +fn run_script_function(function_body: &str) -> std::process::Output { + let script = script_without_main(); + let full_script = format!("{script}\n\n{function_body}"); Command::new("sh") -} - -/// Run a script via temp file to avoid shell argument parsing issues on Windows -fn run_script(script: &str) -> std::process::Output { - use std::io::Write; - - let mut temp_file = tempfile::NamedTempFile::new().unwrap(); - temp_file.write_all(script.as_bytes()).unwrap(); - temp_file.flush().unwrap(); - - shell_cmd() - .arg(temp_file.path()) + .arg("-c") + .arg(&full_script) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .unwrap() } -fn run_script_function(function_body: &str) -> std::process::Output { - let script = script_without_main(); - let full_script = format!("{script}\n\n{function_body}"); - run_script(&full_script) -} - #[test] fn script_has_shebang() { let script = include_str!("../../foundryup-init.sh"); @@ -54,7 +39,7 @@ fn script_usage_works() { #[test] fn script_help_flag() { - let output = shell_cmd() + let output = Command::new("sh") .args(["foundryup-init.sh", "--help"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .stdout(Stdio::piped()) @@ -83,7 +68,13 @@ get_architecture echo "$RETVAL" "# ); - let output = run_script(&full_script); + let output = Command::new("sh") + .arg("-c") + .arg(&full_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "linux_amd64"); } @@ -104,7 +95,13 @@ get_architecture echo "$RETVAL" "# ); - let output = run_script(&full_script); + let output = Command::new("sh") + .arg("-c") + .arg(&full_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "darwin_arm64"); } @@ -126,7 +123,13 @@ get_architecture echo "$RETVAL" "# ); - let output = run_script(&full_script); + let output = Command::new("sh") + .arg("-c") + .arg(&full_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "alpine_amd64"); } @@ -147,7 +150,13 @@ get_architecture echo "$RETVAL" "# ); - let output = run_script(&full_script); + let output = Command::new("sh") + .arg("-c") + .arg(&full_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "win32_amd64"); } @@ -267,7 +276,13 @@ HOME=/tmp/test_home echo "$FOUNDRYUP_BIN_DIR" "# ); - let output = run_script(&full_script); + let output = Command::new("sh") + .arg("-c") + .arg(&full_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("/tmp/test_home/.foundry/bin")); } @@ -282,7 +297,13 @@ FOUNDRY_DIR=/custom/path echo "$FOUNDRYUP_BIN_DIR" "# ); - let output = run_script(&full_script); + let output = Command::new("sh") + .arg("-c") + .arg(&full_script) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("/custom/path/bin")); } @@ -295,7 +316,7 @@ fn script_downloads_foundryup() { let bin_dir = temp_dir.join("bin"); let foundryup_path = bin_dir.join("foundryup"); - let output = shell_cmd() + let output = Command::new("sh") .args(["foundryup-init.sh", "-y"]) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("FOUNDRY_DIR", &temp_dir) From 02dd09a3c6bcf179d2c6f656be9763421d4dc846 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 15:40:02 +0100 Subject: [PATCH 22/30] fix: add shellcheck directives and has_local workaround for POSIX compatibility --- foundryup-init.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index c7f880c..8f7b8e4 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -1,9 +1,24 @@ #!/bin/sh -set -eu +# shellcheck shell=dash +# shellcheck disable=SC2039 # local is non-POSIX # This script downloads and installs foundryup, the Foundry toolchain manager. # It detects the platform, downloads the appropriate binary, and runs it. +# It runs on Unix shells like {a,ba,da,k,z}sh. It uses the common `local` +# extension. Note: Most shells limit `local` to 1 var per line, contra bash. + +# Some versions of ksh have no `local` keyword. Alias it to `typeset`, but +# beware this makes variables global with f()-style function syntax in ksh93. +has_local() { + # shellcheck disable=SC2034 # deliberately unused + local _has_local +} + +has_local 2>/dev/null || alias local=typeset + +set -eu + FOUNDRYUP_REPO="foundry-rs/foundryup" BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" FOUNDRY_DIR="${FOUNDRY_DIR:-$BASE_DIR/.foundry}" From 76467c2bc132327211e6124586796b629594b05e Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 15:47:15 +0100 Subject: [PATCH 23/30] refactor: simplify test script execution pattern --- tests/it/installer_script.rs | 78 +++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 86bae7e..a188d37 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -54,8 +54,11 @@ fn script_help_flag() { #[test] fn script_get_architecture_linux_amd64() { let script = script_without_main(); - let full_script = format!( - r#" + let output = Command::new("sh") + .args([ + "-c", + &format!( + r#" {script} uname() {{ case "$1" in @@ -67,10 +70,8 @@ is_musl() {{ return 1; }} get_architecture echo "$RETVAL" "# - ); - let output = Command::new("sh") - .arg("-c") - .arg(&full_script) + ), + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -82,8 +83,11 @@ echo "$RETVAL" #[test] fn script_get_architecture_darwin_arm64() { let script = script_without_main(); - let full_script = format!( - r#" + let output = Command::new("sh") + .args([ + "-c", + &format!( + r#" {script} uname() {{ case "$1" in @@ -94,10 +98,8 @@ uname() {{ get_architecture echo "$RETVAL" "# - ); - let output = Command::new("sh") - .arg("-c") - .arg(&full_script) + ), + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -109,8 +111,11 @@ echo "$RETVAL" #[test] fn script_get_architecture_alpine() { let script = script_without_main(); - let full_script = format!( - r#" + let output = Command::new("sh") + .args([ + "-c", + &format!( + r#" {script} uname() {{ case "$1" in @@ -122,10 +127,8 @@ is_musl() {{ return 0; }} get_architecture echo "$RETVAL" "# - ); - let output = Command::new("sh") - .arg("-c") - .arg(&full_script) + ), + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -137,8 +140,11 @@ echo "$RETVAL" #[test] fn script_get_architecture_windows() { let script = script_without_main(); - let full_script = format!( - r#" + let output = Command::new("sh") + .args([ + "-c", + &format!( + r#" {script} uname() {{ case "$1" in @@ -149,10 +155,8 @@ uname() {{ get_architecture echo "$RETVAL" "# - ); - let output = Command::new("sh") - .arg("-c") - .arg(&full_script) + ), + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -267,18 +271,19 @@ fn script_foundryup_repo_defined() { #[test] fn script_foundryup_bin_dir_default() { let script = script_without_main(); - let full_script = format!( - r#" + let output = Command::new("sh") + .args([ + "-c", + &format!( + r#" unset FOUNDRY_DIR unset XDG_CONFIG_HOME HOME=/tmp/test_home {script} echo "$FOUNDRYUP_BIN_DIR" "# - ); - let output = Command::new("sh") - .arg("-c") - .arg(&full_script) + ), + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -290,16 +295,17 @@ echo "$FOUNDRYUP_BIN_DIR" #[test] fn script_foundryup_bin_dir_custom() { let script = script_without_main(); - let full_script = format!( - r#" + let output = Command::new("sh") + .args([ + "-c", + &format!( + r#" FOUNDRY_DIR=/custom/path {script} echo "$FOUNDRYUP_BIN_DIR" "# - ); - let output = Command::new("sh") - .arg("-c") - .arg(&full_script) + ), + ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() From e07c1ad392c180ad00e47bbe86a0e538ff22db5a Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 16:02:21 +0100 Subject: [PATCH 24/30] fix: use temp file instead of -c flag for Windows compatibility --- tests/it/installer_script.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index a188d37..f01dca6 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -1,20 +1,27 @@ -use std::process::{Command, Stdio}; +use std::{ + io::Write, + process::{Command, Stdio}, +}; + +fn normalize_line_endings(s: &str) -> String { + s.replace("\r\n", "\n").replace('\r', "") +} fn script_without_main() -> String { let script = include_str!("../../foundryup-init.sh"); - script - // Normalize CRLF to LF (Windows git checkout may add CR) - .replace("\r\n", "\n") - .replace('\r', "") - .replace("main \"$@\" || exit 1", "") + normalize_line_endings(script).replace("main \"$@\" || exit 1", "") } fn run_script_function(function_body: &str) -> std::process::Output { let script = script_without_main(); - let full_script = format!("{script}\n\n{function_body}"); + let full_script = format!("{script}\n\n{}", normalize_line_endings(function_body)); + + let mut temp_file = tempfile::NamedTempFile::new().unwrap(); + temp_file.write_all(full_script.as_bytes()).unwrap(); + temp_file.flush().unwrap(); + Command::new("sh") - .arg("-c") - .arg(&full_script) + .arg(temp_file.path()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() From 7d8b93734e1ffadf5d657057659044c82e29e76c Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 16:15:08 +0100 Subject: [PATCH 25/30] fix: use temp file for all shell script tests on Windows --- tests/it/installer_script.rs | 98 ++++++++++-------------------------- 1 file changed, 27 insertions(+), 71 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index f01dca6..b594e52 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -12,12 +12,10 @@ fn script_without_main() -> String { normalize_line_endings(script).replace("main \"$@\" || exit 1", "") } -fn run_script_function(function_body: &str) -> std::process::Output { - let script = script_without_main(); - let full_script = format!("{script}\n\n{}", normalize_line_endings(function_body)); - +fn run_script(script: &str) -> std::process::Output { + let normalized = normalize_line_endings(script); let mut temp_file = tempfile::NamedTempFile::new().unwrap(); - temp_file.write_all(full_script.as_bytes()).unwrap(); + temp_file.write_all(normalized.as_bytes()).unwrap(); temp_file.flush().unwrap(); Command::new("sh") @@ -28,6 +26,12 @@ fn run_script_function(function_body: &str) -> std::process::Output { .unwrap() } +fn run_script_function(function_body: &str) -> std::process::Output { + let script = script_without_main(); + let full_script = format!("{script}\n\n{function_body}"); + run_script(&full_script) +} + #[test] fn script_has_shebang() { let script = include_str!("../../foundryup-init.sh"); @@ -61,11 +65,8 @@ fn script_help_flag() { #[test] fn script_get_architecture_linux_amd64() { let script = script_without_main(); - let output = Command::new("sh") - .args([ - "-c", - &format!( - r#" + let output = run_script(&format!( + r#" {script} uname() {{ case "$1" in @@ -77,12 +78,7 @@ is_musl() {{ return 1; }} get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + )); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "linux_amd64"); } @@ -90,11 +86,8 @@ echo "$RETVAL" #[test] fn script_get_architecture_darwin_arm64() { let script = script_without_main(); - let output = Command::new("sh") - .args([ - "-c", - &format!( - r#" + let output = run_script(&format!( + r#" {script} uname() {{ case "$1" in @@ -105,12 +98,7 @@ uname() {{ get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + )); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "darwin_arm64"); } @@ -118,11 +106,8 @@ echo "$RETVAL" #[test] fn script_get_architecture_alpine() { let script = script_without_main(); - let output = Command::new("sh") - .args([ - "-c", - &format!( - r#" + let output = run_script(&format!( + r#" {script} uname() {{ case "$1" in @@ -134,12 +119,7 @@ is_musl() {{ return 0; }} get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + )); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "alpine_amd64"); } @@ -147,11 +127,8 @@ echo "$RETVAL" #[test] fn script_get_architecture_windows() { let script = script_without_main(); - let output = Command::new("sh") - .args([ - "-c", - &format!( - r#" + let output = run_script(&format!( + r#" {script} uname() {{ case "$1" in @@ -162,12 +139,7 @@ uname() {{ get_architecture echo "$RETVAL" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + )); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!(stdout, "win32_amd64"); } @@ -278,23 +250,15 @@ fn script_foundryup_repo_defined() { #[test] fn script_foundryup_bin_dir_default() { let script = script_without_main(); - let output = Command::new("sh") - .args([ - "-c", - &format!( - r#" + let output = run_script(&format!( + r#" unset FOUNDRY_DIR unset XDG_CONFIG_HOME HOME=/tmp/test_home {script} echo "$FOUNDRYUP_BIN_DIR" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + )); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("/tmp/test_home/.foundry/bin")); } @@ -302,21 +266,13 @@ echo "$FOUNDRYUP_BIN_DIR" #[test] fn script_foundryup_bin_dir_custom() { let script = script_without_main(); - let output = Command::new("sh") - .args([ - "-c", - &format!( - r#" + let output = run_script(&format!( + r#" FOUNDRY_DIR=/custom/path {script} echo "$FOUNDRYUP_BIN_DIR" "# - ), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); + )); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("/custom/path/bin")); } From 428b1ec098fc1f98eb03aa0f6e5acc242499bbad Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 17:27:05 +0100 Subject: [PATCH 26/30] chore: release v0.0.3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 601ce29..e8a0d5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -524,7 +524,7 @@ dependencies = [ [[package]] name = "foundryup" -version = "2.0.0" +version = "0.0.3" dependencies = [ "base64", "clap", diff --git a/Cargo.toml b/Cargo.toml index a3a961c..4fbcbfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "foundryup" -version = "2.0.0" +version = "0.0.3" edition = "2024" rust-version = "1.85" license = "MIT OR Apache-2.0" From 1b96d9b8c4bec47a8aaf8f7bf8e327716ee4ffa6 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 18:02:29 +0100 Subject: [PATCH 27/30] test: add attestation verification tests for installer script --- tests/it/installer_script.rs | 161 +++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index b594e52..e7adc3a 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -313,3 +313,164 @@ fn script_downloads_foundryup() { std::fs::remove_dir_all(&temp_dir).ok(); } + +#[test] +fn script_compute_sha256_known_value() { + let output = run_script_function( + r#" +echo -n "hello" > /tmp/sha_test_file +hash=$(compute_sha256 /tmp/sha_test_file) +rm -f /tmp/sha_test_file +echo "$hash" +"#, + ); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + assert_eq!( + stdout, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + "compute_sha256 should match known SHA256 of 'hello'" + ); +} + +#[test] +fn script_compute_sha256_format() { + let output = run_script_function( + r#" +echo -n "test" > /tmp/sha_format_test +hash=$(compute_sha256 /tmp/sha_format_test) +rm -f /tmp/sha_format_test + +# Check length is 64 and format is hex +if [ "${#hash}" -eq 64 ] && printf '%s' "$hash" | grep -qE '^[a-fA-F0-9]{64}$'; then + echo "format_ok" +else + echo "format_bad: $hash" +fi +"#, + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("format_ok"), "SHA256 should be 64 hex characters"); +} + +#[test] +fn script_try_download_success() { + let output = run_script_function( + r#" +if try_download "https://github.com" /tmp/try_download_test; then + echo "download_ok" + rm -f /tmp/try_download_test +else + echo "download_failed" +fi +"#, + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("download_ok"), "try_download should succeed for valid URL"); +} + +#[test] +fn script_try_download_failure_no_exit() { + let output = run_script_function( + r#" +if try_download "https://github.com/nonexistent-404-page-xyz" /tmp/try_download_fail; then + echo "unexpected_success" +else + echo "graceful_failure" +fi +"#, + ); + assert!(output.status.success(), "script should not exit on try_download failure"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("graceful_failure"), + "try_download should return false without exiting" + ); +} + +#[test] +fn script_force_flag_documented() { + let output = Command::new("sh") + .args(["foundryup-init.sh", "--help"]) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("-f, --force") && stdout.contains("attestation"), + "help should document --force flag for skipping attestation" + ); +} + +#[test] +fn script_downloads_with_attestation_verification() { + let temp_dir = + std::env::temp_dir().join(format!("foundryup-attest-test-{}", std::process::id())); + std::fs::create_dir_all(&temp_dir).unwrap(); + + let bin_dir = temp_dir.join("bin"); + let foundryup_path = bin_dir.join("foundryup"); + + let output = Command::new("sh") + .args(["foundryup-init.sh", "-y"]) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .env("FOUNDRY_DIR", &temp_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "script failed:\nstdout: {stdout}\nstderr: {stderr}" + ); + assert!(foundryup_path.exists(), "foundryup binary should be installed"); + + let combined = format!("{stdout}{stderr}"); + let attestation_attempted = combined.contains("downloading attestation") + || combined.contains("verifying attestation") + || combined.contains("binary verified") + || combined.contains("no attestation found"); + assert!( + attestation_attempted, + "attestation verification should be attempted. Output: {combined}" + ); + + std::fs::remove_dir_all(&temp_dir).ok(); +} + +#[test] +fn script_downloads_with_force_skips_attestation() { + let temp_dir = + std::env::temp_dir().join(format!("foundryup-force-test-{}", std::process::id())); + std::fs::create_dir_all(&temp_dir).unwrap(); + + let output = Command::new("sh") + .args(["foundryup-init.sh", "-y", "--force"]) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .env("FOUNDRY_DIR", &temp_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!( + output.status.success(), + "script failed:\nstdout: {stdout}\nstderr: {stderr}" + ); + + let combined = format!("{stdout}{stderr}"); + assert!( + combined.contains("skipping attestation verification"), + "--force should skip attestation. Output: {combined}" + ); + + std::fs::remove_dir_all(&temp_dir).ok(); +} From 56ebc83e02ff7b52b3c37b5c6b9a0fb7e1024de0 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 18:04:04 +0100 Subject: [PATCH 28/30] fmt --- tests/it/installer_script.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index e7adc3a..cb62b10 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -424,10 +424,7 @@ fn script_downloads_with_attestation_verification() { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - output.status.success(), - "script failed:\nstdout: {stdout}\nstderr: {stderr}" - ); + assert!(output.status.success(), "script failed:\nstdout: {stdout}\nstderr: {stderr}"); assert!(foundryup_path.exists(), "foundryup binary should be installed"); let combined = format!("{stdout}{stderr}"); @@ -461,10 +458,7 @@ fn script_downloads_with_force_skips_attestation() { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - output.status.success(), - "script failed:\nstdout: {stdout}\nstderr: {stderr}" - ); + assert!(output.status.success(), "script failed:\nstdout: {stdout}\nstderr: {stderr}"); let combined = format!("{stdout}{stderr}"); assert!( From 27658ec576d38a103810f0dfe8cc3bb744e0ab96 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 18:15:56 +0100 Subject: [PATCH 29/30] fix: use printf instead of echo -n for POSIX compatibility --- tests/it/installer_script.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index cb62b10..698b027 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -318,7 +318,7 @@ fn script_downloads_foundryup() { fn script_compute_sha256_known_value() { let output = run_script_function( r#" -echo -n "hello" > /tmp/sha_test_file +printf 'hello' > /tmp/sha_test_file hash=$(compute_sha256 /tmp/sha_test_file) rm -f /tmp/sha_test_file echo "$hash" @@ -335,7 +335,7 @@ echo "$hash" fn script_compute_sha256_format() { let output = run_script_function( r#" -echo -n "test" > /tmp/sha_format_test +printf 'test' > /tmp/sha_format_test hash=$(compute_sha256 /tmp/sha_format_test) rm -f /tmp/sha_format_test From 5ced3b91b30f0deec125600024e06ee44fb61487 Mon Sep 17 00:00:00 2001 From: zerosnacks Date: Wed, 21 Jan 2026 18:31:06 +0100 Subject: [PATCH 30/30] fix: add .exe extension for Windows binaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add get_ext helper function to return .exe for win32 platforms - Simplify attestation verification test to assert 'binary verified ✓' - Add tests for get_ext function - Remove redundant tests --- foundryup-init.sh | 13 ++++- tests/it/installer_script.rs | 94 ++++++------------------------------ 2 files changed, 27 insertions(+), 80 deletions(-) diff --git a/foundryup-init.sh b/foundryup-init.sh index 8f7b8e4..74eebda 100755 --- a/foundryup-init.sh +++ b/foundryup-init.sh @@ -101,15 +101,17 @@ main() { local _url local _attestation_url local _base_url + local _ext + _ext=$(get_ext "$_arch") if [ "${FOUNDRYUP_VERSION+set}" = 'set' ]; then say "installing foundryup version $FOUNDRYUP_VERSION" _base_url="https://github.com/${FOUNDRYUP_REPO}/releases/download/v${FOUNDRYUP_VERSION}" - _url="${_base_url}/foundryup_${_arch}" + _url="${_base_url}/foundryup_${_arch}${_ext}" _attestation_url="${_base_url}/foundryup_${_arch}.attestation.txt" else say "installing latest foundryup" _base_url="https://github.com/${FOUNDRYUP_REPO}/releases/latest/download" - _url="${_base_url}/foundryup_${_arch}" + _url="${_base_url}/foundryup_${_arch}${_ext}" _attestation_url="${_base_url}/foundryup_${_arch}.attestation.txt" fi @@ -269,6 +271,13 @@ get_architecture() { RETVAL="${_ostype}_${_cputype}" } +get_ext() { + case "$1" in + win32_*) echo ".exe" ;; + *) echo "" ;; + esac +} + is_musl() { if [ -f /etc/os-release ]; then grep -qi "alpine" /etc/os-release 2>/dev/null diff --git a/tests/it/installer_script.rs b/tests/it/installer_script.rs index 698b027..d745d9d 100644 --- a/tests/it/installer_script.rs +++ b/tests/it/installer_script.rs @@ -314,6 +314,20 @@ fn script_downloads_foundryup() { std::fs::remove_dir_all(&temp_dir).ok(); } +#[test] +fn script_get_ext_windows() { + let output = run_script_function(r#"echo "$(get_ext win32_amd64)""#); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + assert_eq!(stdout, ".exe", "get_ext should return .exe for win32"); +} + +#[test] +fn script_get_ext_unix() { + let output = run_script_function(r#"echo "$(get_ext linux_amd64)""#); + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + assert_eq!(stdout, "", "get_ext should return empty for linux"); +} + #[test] fn script_compute_sha256_known_value() { let output = run_script_function( @@ -331,78 +345,6 @@ echo "$hash" ); } -#[test] -fn script_compute_sha256_format() { - let output = run_script_function( - r#" -printf 'test' > /tmp/sha_format_test -hash=$(compute_sha256 /tmp/sha_format_test) -rm -f /tmp/sha_format_test - -# Check length is 64 and format is hex -if [ "${#hash}" -eq 64 ] && printf '%s' "$hash" | grep -qE '^[a-fA-F0-9]{64}$'; then - echo "format_ok" -else - echo "format_bad: $hash" -fi -"#, - ); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("format_ok"), "SHA256 should be 64 hex characters"); -} - -#[test] -fn script_try_download_success() { - let output = run_script_function( - r#" -if try_download "https://github.com" /tmp/try_download_test; then - echo "download_ok" - rm -f /tmp/try_download_test -else - echo "download_failed" -fi -"#, - ); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("download_ok"), "try_download should succeed for valid URL"); -} - -#[test] -fn script_try_download_failure_no_exit() { - let output = run_script_function( - r#" -if try_download "https://github.com/nonexistent-404-page-xyz" /tmp/try_download_fail; then - echo "unexpected_success" -else - echo "graceful_failure" -fi -"#, - ); - assert!(output.status.success(), "script should not exit on try_download failure"); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - stdout.contains("graceful_failure"), - "try_download should return false without exiting" - ); -} - -#[test] -fn script_force_flag_documented() { - let output = Command::new("sh") - .args(["foundryup-init.sh", "--help"]) - .current_dir(env!("CARGO_MANIFEST_DIR")) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .unwrap(); - - let stdout = String::from_utf8_lossy(&output.stdout); - assert!( - stdout.contains("-f, --force") && stdout.contains("attestation"), - "help should document --force flag for skipping attestation" - ); -} - #[test] fn script_downloads_with_attestation_verification() { let temp_dir = @@ -428,13 +370,9 @@ fn script_downloads_with_attestation_verification() { assert!(foundryup_path.exists(), "foundryup binary should be installed"); let combined = format!("{stdout}{stderr}"); - let attestation_attempted = combined.contains("downloading attestation") - || combined.contains("verifying attestation") - || combined.contains("binary verified") - || combined.contains("no attestation found"); assert!( - attestation_attempted, - "attestation verification should be attempted. Output: {combined}" + combined.contains("binary verified"), + "attestation verification should succeed with 'binary verified ✓'. Output: {combined}" ); std::fs::remove_dir_all(&temp_dir).ok();