Add checksum-verified release installers

This commit is contained in:
Eric Wendland 2026-07-18 17:37:54 +02:00
commit cb8c4e6fd4
9 changed files with 477 additions and 15 deletions

110
scripts/install.ps1 Normal file
View file

@ -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
}
}

216
scripts/install.sh Executable file
View file

@ -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 <vX.Y.Z> [--install-dir <dir>] [--release-base-url <url>]
install.sh --version <vX.Y.Z> --archive <file> [--checksum <file>]
install.sh --version <vX.Y.Z> --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"