From cb8c4e6fd4b1319fec3482d099ec9440be87ec48 Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Sat, 18 Jul 2026 17:37:54 +0200 Subject: [PATCH] Add checksum-verified release installers --- .github/workflows/release.yml | 79 +++++++++- Cargo.toml | 2 +- README.md | 54 ++++++- crates/geth-node/src/service.rs | 2 +- docs/production-readiness-roadmap.md | 4 + docs/release-support-policy.md | 9 +- docs/roadmap.md | 16 +- scripts/install.ps1 | 110 ++++++++++++++ scripts/install.sh | 216 +++++++++++++++++++++++++++ 9 files changed, 477 insertions(+), 15 deletions(-) create mode 100644 scripts/install.ps1 create mode 100755 scripts/install.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3157cb..adb0154 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,8 +30,11 @@ jobs: - os: ubuntu-latest target: linux-x86_64 binary: geth - - os: macos-latest - target: macos + - os: macos-15-intel + target: macos-x86_64 + binary: geth + - os: macos-15 + target: macos-aarch64 binary: geth - os: windows-latest target: windows-x86_64 @@ -62,6 +65,11 @@ jobs: cp LICENSE-* "${pkg}/" cp -R docs "${pkg}/docs" tar -czf "dist/${pkg}.tar.gz" "${pkg}" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "dist/${pkg}.tar.gz" | awk '{ print $1 }' > "dist/${pkg}.tar.gz.sha256" + else + shasum -a 256 "dist/${pkg}.tar.gz" | awk '{ print $1 }' > "dist/${pkg}.tar.gz.sha256" + fi - name: Smoke-test Unix artifact if: runner.os != 'Windows' @@ -70,9 +78,40 @@ jobs: set -euo pipefail version="${GITHUB_REF_NAME:-manual}" pkg="geth-${version}-${{ matrix.target }}" + expected="$(cat "dist/${pkg}.tar.gz.sha256")" + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "dist/${pkg}.tar.gz" | awk '{ print $1 }')" + else + actual="$(shasum -a 256 "dist/${pkg}.tar.gz" | awk '{ print $1 }')" + fi + test "${actual}" = "${expected}" + case "${version}" in + v[0-9]*) test "$(scripts/install.sh --version "${version}" --print-asset)" = "${pkg}.tar.gz" ;; + esac mkdir -p smoke tar -xzf "dist/${pkg}.tar.gz" -C smoke "smoke/${pkg}/${{ matrix.binary }}" --version + installer_version="${version}" + case "${installer_version}" in + v[0-9]*) ;; + *) installer_version="v0.0.0-smoke" ;; + esac + scripts/install.sh \ + --version "${installer_version}" \ + --archive "dist/${pkg}.tar.gz" \ + --install-dir "${PWD}/smoke/install/bin" \ + --no-modify-path + "smoke/install/bin/geth" --version + printf '%064d\n' 0 > smoke/bad.sha256 + if scripts/install.sh \ + --version "${installer_version}" \ + --archive "dist/${pkg}.tar.gz" \ + --checksum "smoke/bad.sha256" \ + --install-dir "${PWD}/smoke/rejected" \ + --no-modify-path; then + echo "installer accepted an invalid checksum" >&2 + exit 1 + fi test -f "smoke/${pkg}/README.md" test -f "smoke/${pkg}/LICENSE-MIT" test -f "smoke/${pkg}/LICENSE-APACHE" @@ -90,6 +129,8 @@ jobs: Copy-Item LICENSE-* "$pkg/" Copy-Item docs "$pkg/docs" -Recurse Compress-Archive -Path "$pkg/*" -DestinationPath "dist/$pkg.zip" -Force + (Get-FileHash -LiteralPath "dist/$pkg.zip" -Algorithm SHA256).Hash.ToLowerInvariant() | + Set-Content -NoNewline "dist/$pkg.zip.sha256" - name: Smoke-test Windows artifact if: runner.os == 'Windows' @@ -97,9 +138,32 @@ jobs: run: | $version = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { "manual" } $pkg = "geth-$version-${{ matrix.target }}" + $expected = (Get-Content -LiteralPath "dist/$pkg.zip.sha256" -Raw).Trim() + $actual = (Get-FileHash -LiteralPath "dist/$pkg.zip" -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $expected) { throw "release archive checksum mismatch" } + if ($version -match '^v[0-9]') { + $installerAsset = & scripts/install.ps1 -Version $version -PrintAsset + if ($installerAsset -ne "$pkg.zip") { throw "installer selected $installerAsset instead of $pkg.zip" } + } New-Item -ItemType Directory -Force -Path smoke | Out-Null Expand-Archive -Path "dist/$pkg.zip" -DestinationPath "smoke/$pkg" -Force & "smoke/$pkg/${{ matrix.binary }}" --version + $installerVersion = if ($version -match '^v[0-9]') { $version } else { 'v0.0.0-smoke' } + $installerArgs = @{ + Version = $installerVersion + ArchivePath = "dist/$pkg.zip" + InstallDir = "$pwd/smoke/install/bin" + NoModifyPath = $true + } + & scripts/install.ps1 @installerArgs + & "smoke/install/bin/geth.exe" --version + Set-Content -NoNewline -LiteralPath "smoke/bad.sha256" -Value ('0' * 64) + $invalidChecksumArgs = $installerArgs.Clone() + $invalidChecksumArgs['ChecksumPath'] = "smoke/bad.sha256" + $invalidChecksumArgs['InstallDir'] = "$pwd/smoke/rejected" + $checksumRejected = $false + try { & scripts/install.ps1 @invalidChecksumArgs } catch { $checksumRejected = $true } + if (-not $checksumRejected) { throw "installer accepted an invalid checksum" } if (!(Test-Path "smoke/$pkg/README.md")) { throw "README.md missing from archive" } if (!(Test-Path "smoke/$pkg/LICENSE-MIT")) { throw "LICENSE-MIT missing from archive" } if (!(Test-Path "smoke/$pkg/LICENSE-APACHE")) { throw "LICENSE-APACHE missing from archive" } @@ -118,12 +182,23 @@ jobs: needs: build if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' steps: + - name: Checkout installer entrypoints + uses: actions/checkout@v4 + - name: Download artifacts uses: actions/download-artifact@v4 with: path: dist merge-multiple: true + - name: Add installer entrypoints and aggregate checksums + shell: bash + run: | + set -euo pipefail + cp scripts/install.sh scripts/install.ps1 dist/ + cd dist + sha256sum geth-*.tar.gz geth-*.zip install.sh install.ps1 > SHA256SUMS + - name: Publish release uses: softprops/action-gh-release@v2 with: diff --git a/Cargo.toml b/Cargo.toml index c88ef7e..4aee968 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ resolver = "3" [workspace.package] edition = "2024" license = "MIT OR Apache-2.0" -repository = "https://example.invalid/local/geth" +repository = "https://forge.tionis.dev/eric/geth" rust-version = "1.91" [workspace.dependencies] diff --git a/README.md b/README.md index 1b27356..d8fa276 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,53 @@ documents, blobs, pipes, and future multi-user collaboration. This project is not the Ethereum `geth` client. The project and executable are still named `geth`. +## Install A Release + +Choose an explicit release tag. On Linux x86_64 or macOS x86_64/arm64: + +```sh +version=v0.1.0 +curl -fLO "https://forge.tionis.dev/eric/geth/releases/download/$version/install.sh" +sh install.sh --version "$version" +``` + +On Windows x86_64 in PowerShell: + +```powershell +$Version = 'v0.1.0' +Invoke-WebRequest "https://forge.tionis.dev/eric/geth/releases/download/$Version/install.ps1" -OutFile install.ps1 +Unblock-File .\install.ps1 +.\install.ps1 -Version $Version +``` + +Replace `v0.1.0` with the release you intend to install. Both installers fetch +the platform archive and its `.sha256` file, reject a checksum mismatch, and +install only the single `geth` executable. They do not initialize trust state +or start a daemon. The Unix default is `$HOME/.local/bin`; the installer prints +the profile it updates when that directory is not already on `PATH`. Windows +defaults to the current user's local program directory and adds that directory +to the user `PATH`. Pass `--no-modify-path` or `-NoModifyPath` to opt out and +receive manual setup guidance. A mirror can be selected with +`GETH_RELEASE_BASE_URL`, and downloaded archives can be verified and installed +offline with `--archive` or `-ArchivePath`. + +For an upgrade, create a backup, stop the daemon, rerun the installer with the +new explicit tag, then start and check it: + +```sh +geth backup create --out ./geth-backup +geth daemon stop +sh install.sh --version v0.2.0 +geth daemon start +geth wait daemon +geth status +``` + +Uninstall the user service first with `geth daemon uninstall`, then remove the +installed `geth`/`geth.exe` file. This intentionally preserves the selected +geth home. Back it up and remove it separately only when you explicitly want to +delete identity, trust, metadata, and CAS state. + ## First 10 Minutes Build the single binary and start a disposable daemon: @@ -779,9 +826,10 @@ GitHub Actions workflows live under `.github/workflows/`: - `codeql.yml` builds the Rust workspace for GitHub CodeQL analysis. - `dependency-review.yml` blocks pull requests that introduce vulnerable dependency changes at moderate severity or higher. -- `release.yml` builds release archives for Linux, macOS, and Windows, includes - README/docs/license files, smoke-tests the packaged binary from the archive, - uploads artifacts, and publishes them on `v*` tags or manual dispatch. +- `release.yml` builds release archives for Linux, Intel/Apple Silicon macOS, + and Windows, includes README/docs/license files, creates individual and + aggregate SHA-256 checksums, smoke-tests the packaged binary through the + release installers, and publishes them on `v*` tags or manual dispatch. - `docs/release-support-policy.md` defines supported platforms, compatibility expectations, security update handling, and the user-level service boundary. - `.github/dependabot.yml` opens weekly Cargo and GitHub Actions update PRs. diff --git a/crates/geth-node/src/service.rs b/crates/geth-node/src/service.rs index b578781..4219394 100644 --- a/crates/geth-node/src/service.rs +++ b/crates/geth-node/src/service.rs @@ -809,7 +809,7 @@ fn systemd_unit(paths: &GethPaths, executable: &Path) -> String { format!( r#"[Unit] Description=geth personal mesh daemon -Documentation=https://example.invalid/local/geth +Documentation=https://forge.tionis.dev/eric/geth [Service] Type=simple diff --git a/docs/production-readiness-roadmap.md b/docs/production-readiness-roadmap.md index 08a9e42..0f828d3 100644 --- a/docs/production-readiness-roadmap.md +++ b/docs/production-readiness-roadmap.md @@ -309,6 +309,10 @@ Goal: make first deployment the start of a controlled compatibility story. - `[x]` Release archives are smoke-tested directly, not only through `cargo run`. - `[x]` Archives include relevant docs and license files. + - `[x]` Archives publish individual and aggregate SHA-256 checksums and are + smoke-tested through the documented Unix and Windows installers. + - `[x]` Binary installation remains separate from home initialization and + user-service startup. - `[x]` Add upgrade tests. Acceptance criteria: diff --git a/docs/release-support-policy.md b/docs/release-support-policy.md index 2e19337..d007d7a 100644 --- a/docs/release-support-policy.md +++ b/docs/release-support-policy.md @@ -8,7 +8,7 @@ pre-releases intended for dogfood and controlled automation only. Release archives are built for: - Linux x86_64 -- macOS +- macOS x86_64 and arm64 - Windows x86_64 The daemon and control CLI are supported as a single `geth` executable on those @@ -65,6 +65,11 @@ For a tagged release: - CI must pass formatting, check, tests, and clippy. - Security workflows must pass or have an explicit documented exception. - Release archives must be built by `.github/workflows/release.yml`. +- Every archive must have a published SHA-256 checksum, and the release must + include `SHA256SUMS` plus the platform installer entrypoints. - Packaged archives must include README, docs, and license files. -- Packaged binaries must be smoke-tested from the archive. +- Packaged binaries must be checksum-verified and smoke-tested from the archive + through the same installer path documented for users. +- Installers must never initialize a home, create trust state, or start a user + service; `geth daemon install` remains a separate explicit action. - Any breaking changes must be called out in release notes. diff --git a/docs/roadmap.md b/docs/roadmap.md index 081f850..37bb92d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -142,14 +142,16 @@ For deployment-readiness work that cuts across feature areas, see - `[x]` Tests cover the full catalog, family filtering, common aliases, and unknown-family recovery. -- `[ ]` Publish copy-paste installation entrypoints for release artifacts. +- `[x]` Publish copy-paste installation entrypoints for release artifacts. Acceptance criteria: - - `[ ]` Linux, macOS, and Windows installation instructions verify artifact + - `[x]` Linux, macOS, and Windows installation instructions verify artifact checksums and put the single `geth` executable on `PATH`. - - `[ ]` Installation stays separate from explicit `geth daemon install` so + - `[x]` Installation stays separate from explicit `geth daemon install` so downloading a binary never silently creates trust state or starts a service. - - `[ ]` Upgrade and uninstall instructions preserve or explicitly remove the + - `[x]` Upgrade and uninstall instructions preserve or explicitly remove the selected geth home. + - `[x]` Tagged release CI smoke-tests checksum verification and the installed + binary through the same Unix and PowerShell entrypoints users run. ## Long-Term Goal: Distributed Homelab Overlay @@ -425,8 +427,10 @@ control, local CAS, service installation, and written architecture decisions. dispatch, and a weekly schedule. - `[x]` CodeQL and dependency review workflows are present for GitHub-native security scanning. - - `[x]` Release workflow builds Linux, macOS, and Windows archives for `v*` - tags and manual dispatch. + - `[x]` Release workflow builds Linux x86_64, macOS x86_64/arm64, and Windows + x86_64 archives for `v*` tags and manual dispatch. + - `[x]` Release archives have SHA-256 files, an aggregate checksum manifest, + and checksum-verifying installer smoke tests. - `[x]` Dependabot is configured for Cargo and GitHub Actions updates. - `[x]` Bootstrap docs and ADRs. diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..fff5b80 --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,110 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^v[0-9][0-9A-Za-z._-]*$')] + [string]$Version, + + [string]$InstallDir = $( + if ($env:GETH_INSTALL_DIR) { $env:GETH_INSTALL_DIR } + else { Join-Path $env:LOCALAPPDATA 'Programs\geth\bin' } + ), + + [string]$ReleaseBaseUrl = $( + if ($env:GETH_RELEASE_BASE_URL) { $env:GETH_RELEASE_BASE_URL } + else { 'https://forge.tionis.dev/eric/geth/releases/download' } + ), + + [string]$ArchivePath, + + [string]$ChecksumPath, + + [switch]$NoModifyPath, + + [switch]$PrintAsset +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +if (-not [Environment]::Is64BitOperatingSystem) { + throw 'No geth release artifact is available for 32-bit Windows.' +} + +$asset = "geth-$Version-windows-x86_64.zip" +if ($PrintAsset) { + Write-Output $asset + return +} + +$releaseBase = $ReleaseBaseUrl.TrimEnd('/') +$url = "$releaseBase/$Version/$asset" +$tempDir = Join-Path ([IO.Path]::GetTempPath()) ("geth-install-" + [Guid]::NewGuid()) +$extractDir = Join-Path $tempDir 'extract' + +try { + New-Item -ItemType Directory -Force -Path $tempDir, $extractDir | Out-Null + if ($ArchivePath) { + $archive = (Resolve-Path -LiteralPath $ArchivePath).Path + $checksumCandidate = if ($ChecksumPath) { $ChecksumPath } else { "$ArchivePath.sha256" } + $checksum = (Resolve-Path -LiteralPath $checksumCandidate).Path + Write-Host "using local archive $archive" + } else { + if ($ChecksumPath) { throw '-ChecksumPath requires -ArchivePath.' } + if (([Uri]$url).Scheme -ne 'https') { throw 'Release downloads require an HTTPS base URL.' } + $archive = Join-Path $tempDir $asset + $checksum = "$archive.sha256" + Write-Host "downloading $url" + Invoke-WebRequest -Uri $url -OutFile $archive + Invoke-WebRequest -Uri "$url.sha256" -OutFile $checksum + } + + $expected = ((Get-Content -LiteralPath $checksum -Raw).Trim() -split '\s+')[0].ToLowerInvariant() + if ($expected -notmatch '^[0-9a-f]{64}$') { + throw "Invalid SHA-256 file for $asset." + } + $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + throw "Checksum mismatch for $asset." + } + Write-Host "verified SHA-256: $actual" + + Expand-Archive -LiteralPath $archive -DestinationPath $extractDir -Force + $sourceBinary = Join-Path $extractDir 'geth.exe' + if (-not (Test-Path -LiteralPath $sourceBinary -PathType Leaf)) { + throw 'Archive does not contain geth.exe.' + } + + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + $destination = Join-Path $InstallDir 'geth.exe' + $staged = Join-Path $InstallDir ('.geth.install.' + [Guid]::NewGuid() + '.exe') + try { + Copy-Item -LiteralPath $sourceBinary -Destination $staged + Move-Item -LiteralPath $staged -Destination $destination -Force + } catch { + Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue + throw "Could not replace $destination. Stop the daemon with 'geth daemon stop' and retry. $($_.Exception.Message)" + } + + if (-not $NoModifyPath) { + $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') + $pathParts = @($userPath -split ';' | Where-Object { $_ }) + if ($pathParts -notcontains $InstallDir) { + $newUserPath = (@($pathParts) + $InstallDir) -join ';' + try { + [Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User') + Write-Host "added $InstallDir to the current user's PATH; open a new terminal to use it" + } catch { + Write-Warning "Could not update the user PATH. Add this directory manually: $InstallDir" + } + } else { + Write-Host "$InstallDir is already on the current user's PATH" + } + } + + Write-Host "installed geth to $destination" + Write-Host "the daemon was not started; run 'geth daemon install' when ready" +} finally { + if (Test-Path -LiteralPath $tempDir) { + Remove-Item -LiteralPath $tempDir -Recurse -Force + } +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..4ae45e4 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,216 @@ +#!/bin/sh +set -eu + +default_release_base="https://forge.tionis.dev/eric/geth/releases/download" +version="" +install_dir="${GETH_INSTALL_DIR:-${HOME}/.local/bin}" +release_base="${GETH_RELEASE_BASE_URL:-$default_release_base}" +print_asset=false +archive_path="" +checksum_path="" +modify_path=true + +usage() { + cat <<'EOF' +Install a checksum-verified geth release for the current user. + +Usage: + install.sh --version [--install-dir ] [--release-base-url ] + install.sh --version --archive [--checksum ] + install.sh --version --print-asset + +The installer only places the single geth executable. It never initializes a +geth home or starts a daemon. GETH_INSTALL_DIR and GETH_RELEASE_BASE_URL provide +the corresponding option defaults. +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --version) + [ "$#" -ge 2 ] || { echo "error: --version requires a value" >&2; exit 2; } + version=$2 + shift 2 + ;; + --install-dir) + [ "$#" -ge 2 ] || { echo "error: --install-dir requires a value" >&2; exit 2; } + install_dir=$2 + shift 2 + ;; + --release-base-url) + [ "$#" -ge 2 ] || { echo "error: --release-base-url requires a value" >&2; exit 2; } + release_base=$2 + shift 2 + ;; + --archive) + [ "$#" -ge 2 ] || { echo "error: --archive requires a value" >&2; exit 2; } + archive_path=$2 + shift 2 + ;; + --checksum) + [ "$#" -ge 2 ] || { echo "error: --checksum requires a value" >&2; exit 2; } + checksum_path=$2 + shift 2 + ;; + --no-modify-path) + modify_path=false + shift + ;; + --print-asset) + print_asset=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +[ -n "$version" ] || { echo "error: --version is required" >&2; usage >&2; exit 2; } +if printf '%s' "$install_dir" | LC_ALL=C grep '[[:cntrl:]]' >/dev/null 2>&1; then + echo "error: install directory must not contain control characters" >&2 + exit 2 +fi +case "$version" in + v[0-9]*[!0-9A-Za-z._-]*|v[0-9]*/*) + echo "error: version contains unsupported characters" >&2 + exit 2 + ;; + v[0-9]*) ;; + *) echo "error: version must be an explicit tag such as v0.1.0" >&2; exit 2 ;; +esac +case "$install_dir" in + /*) ;; + *) echo "error: install directory must be an absolute path" >&2; exit 2 ;; +esac + +os=$(uname -s) +arch=$(uname -m) +case "$os:$arch" in + Linux:x86_64|Linux:amd64) + platform=linux-x86_64 + ;; + Darwin:x86_64|Darwin:amd64) + platform=macos-x86_64 + ;; + Darwin:arm64|Darwin:aarch64) + platform=macos-aarch64 + ;; + *) + echo "error: no geth release artifact for $os $arch" >&2 + exit 1 + ;; +esac + +asset="geth-${version}-${platform}.tar.gz" +if [ "$print_asset" = true ]; then + printf '%s\n' "$asset" + exit 0 +fi + +[ -n "$archive_path" ] || command -v curl >/dev/null 2>&1 || { echo "error: curl is required" >&2; exit 1; } +if command -v sha256sum >/dev/null 2>&1; then + hash_command=sha256sum +elif command -v shasum >/dev/null 2>&1; then + hash_command='shasum -a 256' +else + echo "error: sha256sum or shasum is required" >&2 + exit 1 +fi + +tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/geth-install.XXXXXX") +staged_binary="" +cleanup() { + rm -rf "$tmp_dir" + if [ -n "$staged_binary" ] && [ -f "$staged_binary" ]; then + rm -f "$staged_binary" + fi +} +trap cleanup EXIT HUP INT TERM + +if [ -n "$archive_path" ]; then + archive=$archive_path + checksum=${checksum_path:-$archive_path.sha256} + [ -f "$archive" ] || { echo "error: archive not found: $archive" >&2; exit 1; } + [ -f "$checksum" ] || { echo "error: checksum not found: $checksum" >&2; exit 1; } + echo "using local archive $archive" +else + [ -z "$checksum_path" ] || { echo "error: --checksum requires --archive" >&2; exit 2; } + archive="$tmp_dir/$asset" + checksum="$archive.sha256" + url="${release_base%/}/${version}/${asset}" + echo "downloading $url" + curl -fL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$archive" "$url" + curl -fL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$checksum" "$url.sha256" +fi + +expected=$(awk 'NR == 1 { print tolower($1) }' "$checksum") +case "$expected" in + *[!0-9a-f]*|'') echo "error: invalid SHA-256 file for $asset" >&2; exit 1 ;; +esac +[ "${#expected}" -eq 64 ] || { echo "error: invalid SHA-256 length for $asset" >&2; exit 1; } +if [ "$hash_command" = sha256sum ]; then + actual=$(sha256sum "$archive" | awk '{ print tolower($1) }') +else + actual=$(shasum -a 256 "$archive" | awk '{ print tolower($1) }') +fi +[ "$actual" = "$expected" ] || { echo "error: checksum mismatch for $asset" >&2; exit 1; } +echo "verified SHA-256: $actual" + +if [ -n "$archive_path" ]; then + package=$(basename "$archive") + package=${package%.tar.gz} +else + package=${asset%.tar.gz} +fi +tar -xzf "$archive" -C "$tmp_dir" +source_binary="$tmp_dir/$package/geth" +[ -f "$source_binary" ] || { echo "error: archive does not contain $package/geth" >&2; exit 1; } + +mkdir -p "$install_dir" +staged_binary="$install_dir/.geth.install.$$" +cp "$source_binary" "$staged_binary" +chmod 755 "$staged_binary" +mv -f "$staged_binary" "$install_dir/geth" + +echo "installed geth to $install_dir/geth" +case ":${PATH}:" in + *":${install_dir}:"*) ;; + *) + if [ "$modify_path" = true ]; then + case "${SHELL:-}" in + */zsh) path_profile=${GETH_PATH_PROFILE:-$HOME/.zprofile} ;; + *) path_profile=${GETH_PATH_PROFILE:-$HOME/.profile} ;; + esac + escaped_install_dir=$(printf '%s' "$install_dir" | sed 's/[\\`"$]/\\&/g') + path_marker="# geth installer PATH: $install_dir" + path_added=false + path_update_failed=false + if [ ! -f "$path_profile" ] || ! grep -F "$path_marker" "$path_profile" >/dev/null 2>&1; then + if printf '\n%s\nexport PATH="%s:\044PATH"\n' "$path_marker" "$escaped_install_dir" >> "$path_profile"; then + path_added=true + else + path_update_failed=true + fi + fi + if [ "$path_added" = true ]; then + echo "added $install_dir to PATH in $path_profile; open a new terminal to use it" + elif [ "$path_update_failed" = true ]; then + echo "warning: could not update $path_profile; add this directory to PATH:" >&2 + echo " export PATH=\"$install_dir:\$PATH\"" >&2 + else + echo "$install_dir is already configured in $path_profile; open a new terminal to use it" + fi + else + echo "add this directory to PATH before using geth:" + echo " export PATH=\"$install_dir:\$PATH\"" + fi + ;; +esac +echo "the daemon was not started; run 'geth daemon install' when ready"