#!/usr/bin/env bash set -eo pipefail # NOTE: if you make modifications to this script, please increment the version number. # WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. FOUNDRYUP_INSTALLER_VERSION="1.3.0" BASE_DIR=${XDG_CONFIG_HOME:-$HOME} FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"} FOUNDRY_VERSIONS_DIR="$FOUNDRY_DIR/versions" FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" FOUNDRY_MAN_DIR="$FOUNDRY_DIR/share/man/man1" FOUNDRY_BIN_URL="https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/foundryup" FOUNDRY_BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" FOUNDRYUP_JOBS="" FOUNDRYUP_IGNORE_VERIFICATION=false BINS=(forge cast anvil chisel) HASH_NAMES=() HASH_VALUES=() export RUSTFLAGS="${RUSTFLAGS:--C target-cpu=native}" main() { need_cmd git need_cmd curl while [[ -n $1 ]]; do case $1 in --) shift; break;; -v|--version) shift; version;; -U|--update) shift; update;; -r|--repo) shift; FOUNDRYUP_REPO=$1;; -b|--branch) shift; FOUNDRYUP_BRANCH=$1;; -i|--install) shift; FOUNDRYUP_VERSION=$1;; -l|--list) shift; list;; -u|--use) shift; FOUNDRYUP_VERSION=$1; use;; -p|--path) shift; FOUNDRYUP_LOCAL_REPO=$1;; -P|--pr) shift; FOUNDRYUP_PR=$1;; -C|--commit) shift; FOUNDRYUP_COMMIT=$1;; -j|--jobs) shift; FOUNDRYUP_JOBS=$1;; -f|--force) FOUNDRYUP_IGNORE_VERIFICATION=true;; --arch) shift; FOUNDRYUP_ARCH=$1;; --platform) shift; FOUNDRYUP_PLATFORM=$1;; -h|--help) usage exit 0 ;; *) warn "unknown option: $1" usage exit 1 esac; shift done CARGO_BUILD_ARGS=(--release) if [ -n "$FOUNDRYUP_JOBS" ]; then CARGO_BUILD_ARGS+=(--jobs "$FOUNDRYUP_JOBS") fi # Print the banner after successfully parsing args banner # Check if the foundryup installer is up to date, warn the user if not check_installer_up_to_date if [ -n "$FOUNDRYUP_PR" ]; then if [ -z "$FOUNDRYUP_BRANCH" ]; then FOUNDRYUP_BRANCH="refs/pull/$FOUNDRYUP_PR/head" else err "can't use --pr and --branch at the same time" fi fi check_bins_in_use # Installs foundry from a local repository if --path parameter is provided if [[ -n "$FOUNDRYUP_LOCAL_REPO" ]]; then need_cmd cargo # Ignore branches/versions as we do not want to modify local git state if [ -n "$FOUNDRYUP_REPO" ] || [ -n "$FOUNDRYUP_BRANCH" ] || [ -n "$FOUNDRYUP_VERSION" ]; then warn "--branch, --install, --use, and --repo arguments are ignored during local install" fi # Enter local repo and build say "installing from $FOUNDRYUP_LOCAL_REPO" cd "$FOUNDRYUP_LOCAL_REPO" ensure cargo build --bins "${CARGO_BUILD_ARGS[@]}" for bin in "${BINS[@]}"; do # Remove prior installations if they exist rm -f "$FOUNDRY_BIN_DIR/$bin" # Symlink from local repo binaries to bin dir ensure ln -s "$PWD/target/release/$bin" "$FOUNDRY_BIN_DIR/$bin" done say "done" exit 0 fi FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry} # Install by downloading binaries if [[ "$FOUNDRYUP_REPO" == "foundry-rs/foundry" && -z "$FOUNDRYUP_BRANCH" && -z "$FOUNDRYUP_COMMIT" ]]; then FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-stable} FOUNDRYUP_TAG=$FOUNDRYUP_VERSION # Normalize versions (handle channels, versions without v prefix) if [[ "$FOUNDRYUP_VERSION" =~ ^nightly ]]; then FOUNDRYUP_VERSION="nightly" elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then # Add v prefix FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}" FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}" fi say "installing foundry (version ${FOUNDRYUP_VERSION}, tag ${FOUNDRYUP_TAG})" uname_s=$(uname -s) PLATFORM=$(tolower "${FOUNDRYUP_PLATFORM:-$uname_s}") EXT="tar.gz" case $PLATFORM in linux|alpine) ;; darwin|mac*) PLATFORM="darwin" ;; mingw*|win*) EXT="zip" PLATFORM="win32" ;; *) err "unsupported platform: $PLATFORM" ;; esac uname_m=$(uname -m) ARCHITECTURE=$(tolower "${FOUNDRYUP_ARCH:-$uname_m}") if [ "${ARCHITECTURE}" = "x86_64" ]; then # Redirect stderr to /dev/null to avoid printing errors if non Rosetta. if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then ARCHITECTURE="arm64" # Rosetta. else ARCHITECTURE="amd64" # Intel. fi elif [ "${ARCHITECTURE}" = "arm64" ] ||[ "${ARCHITECTURE}" = "aarch64" ] ; then ARCHITECTURE="arm64" # Arm. else ARCHITECTURE="amd64" # Amd. fi # Compute the URL of the release tarball in the Foundry repository. RELEASE_URL="https://github.com/${FOUNDRYUP_REPO}/releases/download/${FOUNDRYUP_TAG}/" ATTESTATION_URL="${RELEASE_URL}foundry_${FOUNDRYUP_VERSION}_${PLATFORM}_${ARCHITECTURE}.attestation.txt" BIN_ARCHIVE_URL="${RELEASE_URL}foundry_${FOUNDRYUP_VERSION}_${PLATFORM}_${ARCHITECTURE}.$EXT" MAN_TARBALL_URL="${RELEASE_URL}foundry_man_${FOUNDRYUP_VERSION}.tar.gz" ensure mkdir -p "$FOUNDRY_VERSIONS_DIR" # If `--force` is set, skip the SHA verification. if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = false ]; then # Check if the version is already installed by downloading the attestation file. say "checking if forge, cast, anvil, and chisel for $FOUNDRYUP_TAG version are already installed" # Create a temporary directory to store the attestation link and artifact. tmp_dir="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" tmp="$tmp_dir/attestation.txt" ensure download "$ATTESTATION_URL" "$tmp" # Read the first line of the attestation file to get the artifact link. # The first line should contain the link to the attestation artifact. attestation_artifact_link="$(head -n1 "$tmp" | tr -d '\r')" attestation_missing=false # If the attestation artifact link is empty or the file contains 'Not Found', # we consider the attestation missing and skip the SHA verification. if [ -z "$attestation_artifact_link" ] || grep -q 'Not Found' "$tmp"; then attestation_missing=true fi # Clean up the temporary attestation file. rm -f "$tmp" if $attestation_missing; then say "no attestation found for this release, skipping SHA verification" else say "found attestation for $FOUNDRYUP_TAG version, downloading attestation artifact, checking..." # Download the attestation artifact JSON file. tmp="$tmp_dir/foundry-attestation.sigstore.json" ensure download "${attestation_artifact_link}/download" "$tmp" # Extract the payload from the JSON file. payload_b64=$(awk '/"payload":/ {gsub(/[",]/, "", $2); print $2; exit}' "$tmp") payload_json=$(printf '%s' "$payload_b64" | base64 -d 2>/dev/null || printf '%s' "$payload_b64" | base64 -D) # Extract the names and hashes from the payload JSON. # The payload is expected to be a JSON array of objects with "name" and "sha256" fields. while read -r name_line && read -r sha_line; do name=$(echo "$name_line" | sed -nE 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p') sha=$(echo "$sha_line" | sed -nE 's/.*"sha256"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p') if [ -n "$name" ] && [ -n "$sha" ]; then HASH_NAMES+=("$name") HASH_VALUES+=("$sha") fi done < <(echo "$payload_json" | tr '{}' '\n' | grep -E '"name"|sha256') # Clean up the temporary attestation artifact. # The hashes are now stored in the HASHES associative array. rm -f "$tmp" # Check if the binaries are already installed and match the expected hashes. # If they do, skip the download. version_dir="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" all_match=true for bin in "${BINS[@]}"; do expected="" for i in "${!HASH_NAMES[@]}"; do if [ "${HASH_NAMES[$i]}" = "$bin" ] || [ "${HASH_NAMES[$i]}" = "$bin.exe" ]; then expected="${HASH_VALUES[$i]}" break fi done path="$version_dir/$bin" if [ -z "$expected" ] || [ ! -x "$path" ]; then all_match=false break fi actual=$(compute_sha256 "$path") if [ "$actual" != "$expected" ]; then all_match=false break fi done if $all_match; then say "version $FOUNDRYUP_TAG already installed and verified, activating..." FOUNDRYUP_VERSION=$FOUNDRYUP_TAG use say "done!" exit 0 fi fi # If we reach here, we need to download the binaries. say "binaries not found or do not match expected hashes, downloading new binaries" fi # Download and extract the binaries archive say "downloading forge, cast, anvil, and chisel for $FOUNDRYUP_TAG version" if [ "$PLATFORM" = "win32" ]; then tmp="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" tmp="$tmp/foundry.zip" ensure download "$BIN_ARCHIVE_URL" "$tmp" ensure unzip "$tmp" -d "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" rm -f "$tmp" else tmp="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir" tmp="$tmp/foundry.tar.gz" ensure download "$BIN_ARCHIVE_URL" "$tmp" # Make sure it's a valid tar archive. ensure tar tf "$tmp" 1> /dev/null ensure mkdir -p "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" ensure tar -C "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG" -xvf "$tmp" rm -f "$tmp" fi # Optionally download the manuals if check_cmd tar; then say "downloading manpages" mkdir -p "$FOUNDRY_MAN_DIR" if ! download "$MAN_TARBALL_URL" | tar -xzC "$FOUNDRY_MAN_DIR"; then warn "skipping manpage download: unavailable or invalid archive" fi else say 'skipping manpage download: missing "tar"' fi if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = true ]; then say "skipped SHA verification for downloaded binaries due to --force flag" else # Verify the downloaded binaries against the attestation file. # If the attestation file was not found or is empty, we skip the verification. if $attestation_missing; then say "no attestation found for these binaries, skipping SHA verification for downloaded binaries" else say "verifying downloaded binaries against the attestation file" failed=false for bin in "${BINS[@]}"; do expected="" for i in "${!HASH_NAMES[@]}"; do if [ "${HASH_NAMES[$i]}" = "$bin" ] || [ "${HASH_NAMES[$i]}" = "$bin.exe" ]; then expected="${HASH_VALUES[$i]}" break fi done path="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG/$bin" if [ -z "$expected" ]; then say "no expected hash for $bin" failed=true continue fi if [ ! -x "$path" ]; then say "binary $bin not found at $path" failed=true continue fi actual=$(compute_sha256 "$path") if [ "$actual" != "$expected" ]; then say "$bin hash verification failed:" say " expected: $expected" say " actual: $actual" failed=true else say "$bin verified ✓" fi done if $failed; then err "one or more binaries failed post-installation verification" fi fi fi # Use newly installed version. FOUNDRYUP_VERSION=$FOUNDRYUP_TAG use say "done!" # Install by cloning the repo with the provided branch/tag else need_cmd cargo FOUNDRYUP_BRANCH=${FOUNDRYUP_BRANCH:-master} REPO_PATH="$FOUNDRY_DIR/$FOUNDRYUP_REPO" AUTHOR="$(echo "$FOUNDRYUP_REPO" | cut -d'/' -f1 -)" # If repo path does not exist, grab the author from the repo, make a directory in .foundry, cd to it and clone. if [ ! -d "$REPO_PATH" ]; then ensure mkdir -p "$FOUNDRY_DIR/$AUTHOR" cd "$FOUNDRY_DIR/$AUTHOR" ensure git clone "https://github.com/$FOUNDRYUP_REPO" fi # Force checkout, discarding any local changes cd "$REPO_PATH" ensure git fetch origin "${FOUNDRYUP_BRANCH}:remotes/origin/${FOUNDRYUP_BRANCH}" ensure git checkout "origin/${FOUNDRYUP_BRANCH}" # Create custom version based on the install method, e.g.: # - foundry-rs-commit-c22c4cc96b0535cd989ee94b79da1b19d236b8db # - foundry-rs-pr-1 # - foundry-rs-branch-chore-bump-forge-std if [ -n "$FOUNDRYUP_COMMIT" ]; then # If set, checkout specific commit from branch ensure git checkout "$FOUNDRYUP_COMMIT" FOUNDRYUP_VERSION=$AUTHOR-commit-$FOUNDRYUP_COMMIT elif [ -n "$FOUNDRYUP_PR" ]; then FOUNDRYUP_VERSION=$AUTHOR-pr-$FOUNDRYUP_PR else if [ -n "$FOUNDRYUP_BRANCH" ]; then NORMALIZED_BRANCH="$(echo "$FOUNDRYUP_BRANCH" | tr / -)" FOUNDRYUP_VERSION=$AUTHOR-branch-$NORMALIZED_BRANCH fi fi say "installing version $FOUNDRYUP_VERSION" # Build the repo. ensure cargo build --bins "${CARGO_BUILD_ARGS[@]}" # Create foundry custom version directory. ensure mkdir -p "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION" for bin in "${BINS[@]}"; do for try_path in target/release/$bin target/release/$bin.exe; do if [ -f "$try_path" ]; then mv -f "$try_path" "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION" fi done done # Use newly built version. use # If help2man is installed, use it to add Foundry man pages. if check_cmd help2man; then for bin in "${BINS[@]}"; do help2man -N "$FOUNDRY_BIN_DIR/$bin" > "$FOUNDRY_MAN_DIR/$bin.1" done fi say "done" fi } usage() { cat 1>&2 < OPTIONS: -h, --help Print help information -v, --version Print the version of foundryup -U, --update Update foundryup to the latest version -i, --install Install a specific version from built binaries -l, --list List versions installed from built binaries -u, --use Use a specific installed version from built binaries -b, --branch Build and install a specific branch -P, --pr Build and install a specific Pull Request -C, --commit Build and install a specific commit -r, --repo Build and install from a remote GitHub repo (uses default branch if no other options are set) -p, --path Build and install a local repository -j, --jobs Number of CPUs to use for building Foundry (default: all CPUs) -f, --force Skip SHA verification for downloaded binaries (INSECURE - use with caution) --arch Install a specific architecture (supports amd64 and arm64) --platform Install a specific platform (supports win32, linux, darwin and alpine) EOF } version() { say "$FOUNDRYUP_INSTALLER_VERSION" exit 0 } update() { say "updating foundryup..." current_version="$FOUNDRYUP_INSTALLER_VERSION" # Download the new version. tmp_file="$(mktemp)" ensure download "$FOUNDRY_BIN_URL" "$tmp_file" # Extract new version from downloaded file. new_version=$(grep -Eo 'FOUNDRYUP_INSTALLER_VERSION="[0-9]+\.[0-9]+\.[0-9]+"' "$tmp_file" | cut -d'"' -f2) # If the new version could not be determined, exit gracefully. # This prevents from upgrading to an empty or invalid version. if [ -z "$new_version" ]; then warn "could not determine new foundryup version. Exiting." rm -f "$tmp_file" exit 0 fi # If the new version is not greater than the current version, skip the update. # This is to prevent downgrades or unnecessary updates. if ! version_gt "$new_version" "$current_version"; then say "foundryup is already up to date (installed: $current_version, remote: $new_version)." rm -f "$tmp_file" exit 0 fi # Overwrite existing foundryup ensure mv "$tmp_file" "$FOUNDRY_BIN_PATH" ensure chmod +x "$FOUNDRY_BIN_PATH" say "successfully updated foundryup: $current_version → $new_version" exit 0 } list() { if [ -d "$FOUNDRY_VERSIONS_DIR" ]; then for VERSION in $FOUNDRY_VERSIONS_DIR/*; do say "${VERSION##*/}" for bin in "${BINS[@]}"; do bin_path="$VERSION/$bin" say "- $(ensure "$bin_path" -V)" done printf "\n" done else for bin in "${BINS[@]}"; do bin_path="$FOUNDRY_BIN_DIR/$bin" say "- $(ensure "$bin_path" -V)" done fi exit 0 } use() { [ -z "$FOUNDRYUP_VERSION" ] && err "no version provided" FOUNDRY_VERSION_DIR="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION" if [ -d "$FOUNDRY_VERSION_DIR" ]; then check_bins_in_use for bin in "${BINS[@]}"; do bin_path="$FOUNDRY_BIN_DIR/$bin" cp "$FOUNDRY_VERSION_DIR/$bin" "$bin_path" # Print usage msg say "use - $(ensure "$bin_path" -V)" # Check if the default path of the binary is not in FOUNDRY_BIN_DIR which_path="$(command -v "$bin" || true)" if [ -n "$which_path" ] && [ "$which_path" != "$bin_path" ]; then warn "" cat 1>&2 <&2 } err() { say "$1" >&2 exit 1 } tolower() { echo "$1" | awk '{print tolower($0)}' } compute_sha256() { if check_cmd sha256sum; then sha256sum "$1" | cut -d' ' -f1 else shasum -a 256 "$1" | awk '{print $1}' fi } need_cmd() { if ! check_cmd "$1"; then err "need '$1' (command not found)" fi } check_cmd() { command -v "$1" &>/dev/null } check_installer_up_to_date() { say "checking if foundryup is up to date..." if check_cmd curl; then remote_version=$(curl -fsSL "$FOUNDRY_BIN_URL" | grep -Eo 'FOUNDRYUP_INSTALLER_VERSION="[0-9]+\.[0-9]+\.[0-9]+"' | cut -d'"' -f2) else remote_version=$(wget -qO- "$FOUNDRY_BIN_URL" | grep -Eo 'FOUNDRYUP_INSTALLER_VERSION="[0-9]+\.[0-9]+\.[0-9]+"' | cut -d'"' -f2) fi if [ -z "$remote_version" ]; then warn "Could not determine remote foundryup version. Skipping version check." return 0 fi if version_gt "$remote_version" "$FOUNDRYUP_INSTALLER_VERSION"; then printf ' Your installation of foundryup is out of date. Installed: %s → Latest: %s To update, run: foundryup --update Updating is highly recommended as it gives you access to the latest features and bug fixes. ' "$FOUNDRYUP_INSTALLER_VERSION" "$remote_version" >&2 else say "foundryup is up to date." fi } # Compares two version strings in the format "major.minor.patch". # Returns 0 if $1 is greater than $2, 1 if $1 is less than $2, and 1 if they are equal. # # Assumes that the version strings are well-formed and contain three numeric components separated by dots. # # Example: version_gt "1.2.3" "1.2.4" # returns 1 (1.2.3 < 1.2.4) # version_gt "1.2.3" "1.2.3" # returns 1 (1.2.3 == 1.2.3) # version_gt "1.2.4" "1.2.3" # returns 0 (1.2.4 > 1.2.3) version_gt() { [ "$1" = "$2" ] && return 1 IFS=. read -r major1 minor1 patch1 </dev/null; then err "Error: '$bin' is currently running. Please stop the process and try again." fi done else warn "Make sure no foundry process is running during the install process!" fi } # Run a command that should never fail. If the command fails execution # will immediately terminate with an error showing the failing command. ensure() { if ! "$@"; then err "command failed: $*"; fi } # Downloads $1 into $2 or stdout download() { if [ -n "$2" ]; then # output into $2 if check_cmd curl; then curl -#o "$2" -L "$1" else wget --show-progress -qO "$2" "$1" fi else # output to stdout if check_cmd curl; then curl -#L "$1" else wget --show-progress -qO- "$1" fi fi } # Banner prompt for Foundry banner() { printf ' .xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx ╔═╗ ╔═╗ ╦ ╦ ╔╗╔ ╔╦╗ ╦═╗ ╦ ╦ Portable and modular toolkit ╠╣ ║ ║ ║ ║ ║║║ ║║ ╠╦╝ ╚╦╝ for Ethereum Application Development ╚ ╚═╝ ╚═╝ ╝╚╝ ═╩╝ ╩╚═ ╩ written in Rust. .xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx Repo : https://github.com/foundry-rs/foundry Book : https://book.getfoundry.sh/ Chat : https://t.me/foundry_rs/ Support : https://t.me/foundry_support/ Contribute : https://github.com/foundry-rs/foundry/blob/master/CONTRIBUTING.md .xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx ' } main "$@"