From 5408b0837ca3e1399b542ec19e1e03848fc6cda9 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 21 Jul 2026 17:35:16 +0800 Subject: [PATCH] add installer --- .github/workflows/installers.yml | 40 ++++++ README.md | 20 ++- scripts/install.ps1 | 236 +++++++++++++++++++++++++++++++ scripts/install.sh | 208 +++++++++++++++++++++++++++ scripts/test-install.ps1 | 85 +++++++++++ scripts/test-install.sh | 81 +++++++++++ 6 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/installers.yml create mode 100644 scripts/install.ps1 create mode 100644 scripts/install.sh create mode 100644 scripts/test-install.ps1 create mode 100644 scripts/test-install.sh diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml new file mode 100644 index 0000000..3b8b938 --- /dev/null +++ b/.github/workflows/installers.yml @@ -0,0 +1,40 @@ +name: Installers + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +jobs: + powershell: + name: Windows PowerShell + runs-on: windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Test PowerShell installer + shell: pwsh + run: ./scripts/test-install.ps1 + + shell: + name: ${{ matrix.os }} shell + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Test shell installer + run: sh ./scripts/test-install.sh diff --git a/README.md b/README.md index 058995a..b7bd58d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,25 @@ Etherscan's API documentation remains as the main reference for endpoint paramet ## Installation -Download a prebuilt binary from [GitHub Releases](https://github.com/etherscan/etherscan-cli/releases) +### Windows + +```powershell +irm https://raw.githubusercontent.com/etherscan/etherscan-cli/master/scripts/install.ps1 | iex +``` + +### macOS and Linux + +```sh +curl -fsSL https://raw.githubusercontent.com/etherscan/etherscan-cli/master/scripts/install.sh | sh +``` + +The installers select the correct binary, verify its checksum, and add `etherscan` to your `PATH`. Open a new terminal after installation. + +### Manual installation + +Download the appropriate archive and `checksums.txt` from [GitHub Releases](https://github.com/etherscan/etherscan-cli/releases). Verify the checksum, extract the archive, and place `etherscan` on your `PATH`. + +If installation fails, [open a GitHub issue](https://github.com/etherscan/etherscan-cli/issues/new). ## Quickstart diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..ba7ad0e --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,236 @@ +[CmdletBinding()] +param( + [string]$Version = $env:ETHERSCAN_VERSION, + [string]$InstallDir = $env:ETHERSCAN_INSTALL_DIR, + [switch]$NoPathUpdate +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$Repository = "etherscan/etherscan-cli" +$DownloadBaseUrl = $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL + +function Get-EtherscanArchitecture { + $architecture = $env:PROCESSOR_ARCHITEW6432 + if ([string]::IsNullOrWhiteSpace($architecture)) { + $architecture = $env:PROCESSOR_ARCHITECTURE + } + + switch -Regex ($architecture) { + "^(AMD64|x86_64)$" { return "amd64" } + "^(ARM64|aarch64)$" { return "arm64" } + default { throw "Unsupported Windows architecture: $architecture. Etherscan CLI supports amd64 and arm64." } + } +} + +function Get-GitHubApiHeaders { + $headers = @{ + Accept = "application/vnd.github+json" + "User-Agent" = "etherscan-cli-installer" + "X-GitHub-Api-Version" = "2022-11-28" + } + if (-not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) { + $headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)" + } + return $headers +} + +function Resolve-EtherscanVersion { + param([string]$RequestedVersion) + + if (-not [string]::IsNullOrWhiteSpace($RequestedVersion) -and $RequestedVersion -ne "latest") { + $tag = if ($RequestedVersion.StartsWith("v")) { $RequestedVersion } else { "v$RequestedVersion" } + } + else { + if (-not [string]::IsNullOrWhiteSpace($DownloadBaseUrl)) { + throw "A version is required when the installer test download source is used." + } + $release = Invoke-RestMethod ` + -Uri "https://api.github.com/repos/$Repository/releases/latest" ` + -Headers (Get-GitHubApiHeaders) + $tag = [string]$release.tag_name + } + + if ($tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$') { + throw "Invalid release version: $tag" + } + + return @{ + Tag = $tag + Version = $tag.Substring(1) + } +} + +function Copy-InstallerFile { + param( + [string]$Base, + [string]$Name, + [string]$Destination + ) + + if (Test-Path -LiteralPath $Base -PathType Container) { + Copy-Item -LiteralPath (Join-Path $Base $Name) -Destination $Destination + return + } + + $uri = "$($Base.TrimEnd('/'))/$Name" + $parsedUri = [Uri]$uri + if ($parsedUri.Scheme -ne "https") { + throw "Remote downloads must use HTTPS: $uri" + } + + $headers = @{ + "User-Agent" = "etherscan-cli-installer" + } + if ($parsedUri.Host -in @("github.com", "api.github.com") -and + -not [string]::IsNullOrWhiteSpace($env:ETHERSCAN_GITHUB_TOKEN)) { + $headers.Authorization = "Bearer $($env:ETHERSCAN_GITHUB_TOKEN)" + } + + Invoke-WebRequest -Uri $uri -OutFile $Destination -Headers $headers -UseBasicParsing +} + +function Add-EtherscanToUserPath { + param([string]$Directory) + + $fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd('\') + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + $entries = @($userPath -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + $alreadyPresent = $entries | Where-Object { + try { + $expandedEntry = [Environment]::ExpandEnvironmentVariables($_) + [IO.Path]::GetFullPath($expandedEntry).TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) + } + catch { + $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) + } + } + + if (-not $alreadyPresent) { + $newUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) { + $fullDirectory + } + else { + "$($userPath.TrimEnd(';'));$fullDirectory" + } + [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") + Write-Host "Added $fullDirectory to your user PATH." + } + + $processEntries = @($env:Path -split ';') + if (-not ($processEntries | Where-Object { $_.TrimEnd('\').Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase) })) { + $env:Path = "$env:Path;$fullDirectory" + } +} + +if ($env:OS -ne "Windows_NT") { + throw "This installer supports Windows only. Use install.sh on macOS or Linux." +} + +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + $InstallDir = Join-Path $localAppData "Programs\Etherscan\bin" +} +if ($InstallDir.Contains(';')) { + throw "The installation directory cannot contain a semicolon." +} +if ($InstallDir.IndexOfAny([char[]]"`r`n") -ge 0) { + throw "The installation directory cannot contain a line break." +} + +$resolved = Resolve-EtherscanVersion -RequestedVersion $Version +$architecture = Get-EtherscanArchitecture +$archiveName = "etherscan_$($resolved.Version)_windows_$architecture.zip" +$baseUrl = if ([string]::IsNullOrWhiteSpace($DownloadBaseUrl)) { + "https://github.com/$Repository/releases/download/$($resolved.Tag)" +} +else { + $DownloadBaseUrl +} + +$tempDirectory = Join-Path ([IO.Path]::GetTempPath()) "etherscan-install-$PID-$([Guid]::NewGuid().ToString('N'))" +$archivePath = Join-Path $tempDirectory $archiveName +$checksumPath = Join-Path $tempDirectory "checksums.txt" +$sourceExecutable = Join-Path $tempDirectory "etherscan.exe" + +try { + New-Item -ItemType Directory -Path $tempDirectory -Force | Out-Null + + Write-Host "Downloading Etherscan CLI $($resolved.Version) for windows/$architecture..." + Copy-InstallerFile -Base $baseUrl -Name $archiveName -Destination $archivePath + Copy-InstallerFile -Base $baseUrl -Name "checksums.txt" -Destination $checksumPath + + $pattern = '^([0-9A-Fa-f]{64})\s+\*?' + [Regex]::Escape($archiveName) + '$' + $checksumLine = Get-Content -LiteralPath $checksumPath | Where-Object { $_ -match $pattern } | Select-Object -First 1 + if (-not $checksumLine -or $checksumLine -notmatch $pattern) { + throw "No checksum was published for $archiveName." + } + + $expectedHash = $Matches[1].ToLowerInvariant() + $actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -ne $expectedHash) { + throw "Checksum verification failed for $archiveName. Expected $expectedHash, received $actualHash." + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [IO.Compression.ZipFile]::OpenRead($archivePath) + try { + $executableEntries = @($zip.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq "etherscan.exe" }) + if ($executableEntries.Count -ne 1) { + throw "$archiveName must contain exactly one root-level etherscan.exe." + } + + $inputStream = $executableEntries[0].Open() + $outputStream = [IO.File]::Create($sourceExecutable) + try { + $inputStream.CopyTo($outputStream) + } + finally { + $outputStream.Dispose() + $inputStream.Dispose() + } + } + finally { + $zip.Dispose() + } + + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + $targetExecutable = Join-Path $InstallDir "etherscan.exe" + $stagedExecutable = Join-Path $InstallDir ".etherscan.exe.new-$PID" + $backupExecutable = Join-Path $InstallDir ".etherscan.exe.old-$PID" + Copy-Item -LiteralPath $sourceExecutable -Destination $stagedExecutable -Force + + try { + if (Test-Path -LiteralPath $targetExecutable) { + Move-Item -LiteralPath $targetExecutable -Destination $backupExecutable -Force + } + Move-Item -LiteralPath $stagedExecutable -Destination $targetExecutable -Force + Remove-Item -LiteralPath $backupExecutable -Force -ErrorAction SilentlyContinue + } + catch { + Remove-Item -LiteralPath $stagedExecutable -Force -ErrorAction SilentlyContinue + if ((Test-Path -LiteralPath $backupExecutable) -and -not (Test-Path -LiteralPath $targetExecutable)) { + Move-Item -LiteralPath $backupExecutable -Destination $targetExecutable -Force + } + throw + } + + if (-not $NoPathUpdate) { + Add-EtherscanToUserPath -Directory $InstallDir + } + + Write-Host "" + Write-Host "Etherscan CLI $($resolved.Version) installed successfully." + Write-Host "Installed to: $targetExecutable" + if ($NoPathUpdate) { + Write-Host "Add $InstallDir to PATH to run etherscan from any directory." + } + else { + Write-Host "Run 'etherscan version' to verify the installation." + Write-Host "Open a new terminal if the command is not yet available." + } +} +finally { + Remove-Item -LiteralPath $tempDirectory -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..4302b23 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,208 @@ +#!/bin/sh + +set -eu + +repository="etherscan/etherscan-cli" +version=${ETHERSCAN_VERSION:-} +install_dir=${ETHERSCAN_INSTALL_DIR:-"$HOME/.local/bin"} +download_base=${ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL:-} +update_path=1 + +usage() { + cat <<'EOF' +Install Etherscan CLI. + +Usage: install.sh [options] + +Options: + --version VERSION Install a specific version (for example, v1.1.0). + --install-dir DIRECTORY Install into DIRECTORY (default: ~/.local/bin). + --no-path-update Do not update the shell profile. + -h, --help Show this help. +EOF +} + +die() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --version) + [ "$#" -ge 2 ] || die "--version requires a value" + version=$2 + shift 2 + ;; + --install-dir) + [ "$#" -ge 2 ] || die "--install-dir requires a value" + install_dir=$2 + shift 2 + ;; + --no-path-update) + update_path=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac +done + +if printf '%s' "$install_dir" | LC_ALL=C grep '[[:cntrl:]]' >/dev/null 2>&1; then + die "the installation directory cannot contain control characters" +fi + +fetch_stdout() { + url=$1 + if command -v curl >/dev/null 2>&1; then + curl -fsSL -A etherscan-cli-installer "$url" + elif command -v wget >/dev/null 2>&1; then + wget -qO- --user-agent=etherscan-cli-installer "$url" + else + die "curl or wget is required" + fi +} + +fetch_file() { + base=$1 + name=$2 + destination=$3 + + if [ -d "$base" ]; then + cp "$base/$name" "$destination" + return + fi + + case "$base" in + file://*) + cp "${base#file://}/$name" "$destination" + ;; + https://*) + if command -v curl >/dev/null 2>&1; then + curl -fsSL -A etherscan-cli-installer "$base/$name" -o "$destination" + elif command -v wget >/dev/null 2>&1; then + wget -q --user-agent=etherscan-cli-installer "$base/$name" -O "$destination" + else + die "curl or wget is required" + fi + ;; + *) + die "invalid download base URL or directory: $base" + ;; + esac +} + +if [ -z "$version" ] || [ "$version" = latest ]; then + [ -z "$download_base" ] || die "a version is required with the installer test download source" + release_json=$(fetch_stdout "https://api.github.com/repos/$repository/releases/latest") + version=$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | sed -n '1p') + [ -n "$version" ] || die "could not resolve the latest Etherscan CLI version" +fi + +case "$version" in + v*) tag=$version; release_version=${version#v} ;; + *) tag="v$version"; release_version=$version ;; +esac + +printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$' || die "invalid release version: $tag" + +system_name=${ETHERSCAN_INSTALL_TEST_OS:-$(uname -s)} +case "$system_name" in + Linux|linux) os=linux ;; + Darwin|darwin) os=darwin ;; + *) die "unsupported operating system: $system_name" ;; +esac + +machine_arch=${ETHERSCAN_INSTALL_TEST_ARCH:-$(uname -m)} +case "$machine_arch" in + x86_64|amd64) arch=amd64 ;; + arm64|aarch64) arch=arm64 ;; + *) die "unsupported architecture: $machine_arch. Etherscan CLI supports amd64 and arm64." ;; +esac + +archive_name="etherscan_${release_version}_${os}_${arch}.tar.gz" +if [ -z "$download_base" ]; then + download_base="https://github.com/$repository/releases/download/$tag" +fi + +temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t etherscan-install) +trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM +archive_path="$temp_dir/$archive_name" +checksum_path="$temp_dir/checksums.txt" +source_executable="$temp_dir/etherscan" + +printf 'Downloading Etherscan CLI %s for %s/%s...\n' "$release_version" "$os" "$arch" +fetch_file "$download_base" "$archive_name" "$archive_path" +fetch_file "$download_base" checksums.txt "$checksum_path" + +expected_hash=$(awk -v name="$archive_name" '$2 == name || $2 == ("*" name) { print tolower($1); exit }' "$checksum_path") +[ -n "$expected_hash" ] || die "no checksum was published for $archive_name" +printf '%s\n' "$expected_hash" | grep -Eq '^[0-9a-f]{64}$' || die "invalid checksum published for $archive_name" + +if command -v sha256sum >/dev/null 2>&1; then + actual_hash=$(sha256sum "$archive_path" | awk '{ print tolower($1) }') +elif command -v shasum >/dev/null 2>&1; then + actual_hash=$(shasum -a 256 "$archive_path" | awk '{ print tolower($1) }') +else + die "sha256sum or shasum is required to verify the download" +fi + +[ "$actual_hash" = "$expected_hash" ] || die "checksum verification failed for $archive_name" + +entry_count=$(tar -tzf "$archive_path" | awk '$0 == "etherscan" { count++ } END { print count + 0 }') +[ "$entry_count" -eq 1 ] || die "$archive_name must contain exactly one root-level etherscan" +tar -xOzf "$archive_path" etherscan >"$source_executable" +[ -s "$source_executable" ] || die "$archive_name contains an empty etherscan executable" + +mkdir -p "$install_dir" +staged_executable="$install_dir/.etherscan.new.$$" +cp "$source_executable" "$staged_executable" +chmod 0755 "$staged_executable" +mv -f "$staged_executable" "$install_dir/etherscan" + +path_updated=0 +if [ "$update_path" -eq 1 ]; then + case ":$PATH:" in + *:"$install_dir":*) ;; + *) + shell_name=${SHELL:-sh} + shell_name=${shell_name##*/} + escaped_install_dir=$(printf '%s' "$install_dir" | sed 's/[\\"$`]/\\&/g') + if [ "$shell_name" = fish ]; then + profile="$HOME/.config/fish/config.fish" + mkdir -p "$(dirname "$profile")" + path_line="fish_add_path \"$escaped_install_dir\"" + else + case "$shell_name" in + zsh) profile="$HOME/.zshrc" ;; + bash) profile="$HOME/.bashrc" ;; + *) profile="$HOME/.profile" ;; + esac + path_line="export PATH=\"$escaped_install_dir:\$PATH\"" + fi + + if ! [ -f "$profile" ] || ! grep -F -e "$install_dir" "$profile" >/dev/null 2>&1; then + { + printf '\n# Etherscan CLI\n' + printf '%s\n' "$path_line" + } >>"$profile" + path_updated=1 + fi + ;; + esac +fi + +printf '\nEtherscan CLI %s installed successfully.\n' "$release_version" +printf 'Installed to: %s\n' "$install_dir/etherscan" +if [ "$update_path" -eq 0 ]; then + printf 'Add %s to PATH to run etherscan from any directory.\n' "$install_dir" +elif [ "$path_updated" -eq 1 ]; then + printf 'Open a new terminal, then run: etherscan version\n' +else + printf 'Run: etherscan version\n' +fi diff --git a/scripts/test-install.ps1 b/scripts/test-install.ps1 new file mode 100644 index 0000000..cab98b9 --- /dev/null +++ b/scripts/test-install.ps1 @@ -0,0 +1,85 @@ +$ErrorActionPreference = "Stop" + +$installer = Join-Path $PSScriptRoot "install.ps1" +$tempDirectory = Join-Path ([IO.Path]::GetTempPath()) "etherscan-installer-test-$PID-$([Guid]::NewGuid().ToString('N'))" +$fixtureDirectory = Join-Path $tempDirectory "fixtures" +$bundleDirectory = Join-Path $tempDirectory "bundle" +$installDirectory = Join-Path $tempDirectory "install dir" +$version = "9.9.9-test.1" + +$architecture = $env:PROCESSOR_ARCHITEW6432 +if ([string]::IsNullOrWhiteSpace($architecture)) { + $architecture = $env:PROCESSOR_ARCHITECTURE +} +$goArchitecture = switch -Regex ($architecture) { + "^(AMD64|x86_64)$" { "amd64" } + "^(ARM64|aarch64)$" { "arm64" } + default { throw "Unsupported test architecture: $architecture" } +} +$archiveName = "etherscan_${version}_windows_$goArchitecture.zip" +$archivePath = Join-Path $fixtureDirectory $archiveName +$checksumPath = Join-Path $fixtureDirectory "checksums.txt" +$previousDownloadBaseUrl = $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL + +function Write-Fixture { + param([string]$Content) + + Remove-Item -LiteralPath $bundleDirectory -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $archivePath -Force -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $bundleDirectory -Force | Out-Null + Set-Content -LiteralPath (Join-Path $bundleDirectory "etherscan.exe") -Value $Content -NoNewline + Compress-Archive -Path (Join-Path $bundleDirectory "*") -DestinationPath $archivePath + $hash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant() + Set-Content -LiteralPath $checksumPath -Value "$hash $archiveName" +} + +try { + New-Item -ItemType Directory -Path $fixtureDirectory -Force | Out-Null + $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL = $fixtureDirectory + + Write-Fixture -Content "first" + & $installer -Version $version -InstallDir $installDirectory -NoPathUpdate + $installed = Join-Path $installDirectory "etherscan.exe" + if ((Get-Content -LiteralPath $installed -Raw) -ne "first") { + throw "fresh installation did not install the expected executable" + } + + Write-Fixture -Content "second" + & $installer -Version $version -InstallDir $installDirectory -NoPathUpdate + if ((Get-Content -LiteralPath $installed -Raw) -ne "second") { + throw "reinstallation did not replace the executable" + } + + Set-Content -LiteralPath $checksumPath -Value "$('0' * 64) $archiveName" + $failed = $false + try { + & $installer -Version $version -InstallDir $installDirectory -NoPathUpdate + } + catch { + $failed = $_.Exception.Message -like "Checksum verification failed*" + } + if (-not $failed) { + throw "installer accepted an invalid checksum" + } + if ((Get-Content -LiteralPath $installed -Raw) -ne "second") { + throw "failed verification modified the installed executable" + } + + $failed = $false + try { + $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL = "http://example.invalid" + & $installer -Version $version -InstallDir $installDirectory -NoPathUpdate + } + catch { + $failed = $_.Exception.Message -like "Remote downloads must use HTTPS*" + } + if (-not $failed) { + throw "installer accepted an insecure download URL" + } + + Write-Host "PowerShell installer tests passed." +} +finally { + $env:ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL = $previousDownloadBaseUrl + Remove-Item -LiteralPath $tempDirectory -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/scripts/test-install.sh b/scripts/test-install.sh new file mode 100644 index 0000000..b606082 --- /dev/null +++ b/scripts/test-install.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +installer="$script_dir/install.sh" +temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t etherscan-installer-test) +trap 'rm -rf "$temp_dir"' EXIT HUP INT TERM + +fixture_dir="$temp_dir/fixtures" +bundle_dir="$temp_dir/bundle" +install_dir="$temp_dir/install dir" +version=9.9.9-test.1 + +case "$(uname -s)" in + Linux) os=linux ;; + Darwin) os=darwin ;; + MINGW*|MSYS*|CYGWIN*) + os=linux + export ETHERSCAN_INSTALL_TEST_OS=linux + ;; + *) printf 'unsupported test OS\n' >&2; exit 1 ;; +esac + +case "$(uname -m)" in + x86_64|amd64) arch=amd64 ;; + arm64|aarch64) arch=arm64 ;; + *) printf 'unsupported test architecture\n' >&2; exit 1 ;; +esac +export ETHERSCAN_INSTALL_TEST_ARCH=$arch + +archive_name="etherscan_${version}_${os}_${arch}.tar.gz" +archive_path="$fixture_dir/$archive_name" +checksum_path="$fixture_dir/checksums.txt" +mkdir -p "$fixture_dir" +export ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL=$fixture_dir + +write_fixture() { + content=$1 + rm -rf "$bundle_dir" + mkdir -p "$bundle_dir" + printf '%s' "$content" >"$bundle_dir/etherscan" + chmod 0755 "$bundle_dir/etherscan" + tar -czf "$archive_path" -C "$bundle_dir" etherscan + if command -v sha256sum >/dev/null 2>&1; then + hash=$(sha256sum "$archive_path" | awk '{ print tolower($1) }') + else + hash=$(shasum -a 256 "$archive_path" | awk '{ print tolower($1) }') + fi + printf '%s %s\n' "$hash" "$archive_name" >"$checksum_path" +} + +write_fixture first +sh "$installer" --version "$version" --install-dir "$install_dir" --no-path-update +[ "$(cat "$install_dir/etherscan")" = first ] || { printf 'fresh installation failed\n' >&2; exit 1; } + +write_fixture second +sh "$installer" --version "$version" --install-dir "$install_dir" --no-path-update +[ "$(cat "$install_dir/etherscan")" = second ] || { printf 'reinstallation failed\n' >&2; exit 1; } + +profile_home="$temp_dir/profile-home" +profile_install_dir="$profile_home/bin with spaces" +mkdir -p "$profile_home" +HOME="$profile_home" SHELL=/bin/sh sh "$installer" --version "$version" --install-dir "$profile_install_dir" +grep -F "$profile_install_dir" "$profile_home/.profile" >/dev/null || { printf 'PATH profile update failed\n' >&2; exit 1; } +HOME="$profile_home" SHELL=/bin/sh sh "$installer" --version "$version" --install-dir "$profile_install_dir" +[ "$(grep -Fc '# Etherscan CLI' "$profile_home/.profile")" -eq 1 ] || { printf 'PATH profile update was not idempotent\n' >&2; exit 1; } + +printf '%064d %s\n' 0 "$archive_name" >"$checksum_path" +if sh "$installer" --version "$version" --install-dir "$install_dir" --no-path-update >/dev/null 2>&1; then + printf 'installer accepted an invalid checksum\n' >&2 + exit 1 +fi +[ "$(cat "$install_dir/etherscan")" = second ] || { printf 'failed verification changed installation\n' >&2; exit 1; } + +if ETHERSCAN_INSTALL_TEST_DOWNLOAD_BASE_URL="http://example.invalid" sh "$installer" --version "$version" --install-dir "$install_dir" --no-path-update >/dev/null 2>&1; then + printf 'installer accepted an insecure download URL\n' >&2 + exit 1 +fi + +printf 'Shell installer tests passed.\n'