Add checksum-verified release installers
This commit is contained in:
parent
47d16eb714
commit
cb8c4e6fd4
9 changed files with 477 additions and 15 deletions
79
.github/workflows/release.yml
vendored
79
.github/workflows/release.yml
vendored
|
|
@ -30,8 +30,11 @@ jobs:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: linux-x86_64
|
target: linux-x86_64
|
||||||
binary: geth
|
binary: geth
|
||||||
- os: macos-latest
|
- os: macos-15-intel
|
||||||
target: macos
|
target: macos-x86_64
|
||||||
|
binary: geth
|
||||||
|
- os: macos-15
|
||||||
|
target: macos-aarch64
|
||||||
binary: geth
|
binary: geth
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: windows-x86_64
|
target: windows-x86_64
|
||||||
|
|
@ -62,6 +65,11 @@ jobs:
|
||||||
cp LICENSE-* "${pkg}/"
|
cp LICENSE-* "${pkg}/"
|
||||||
cp -R docs "${pkg}/docs"
|
cp -R docs "${pkg}/docs"
|
||||||
tar -czf "dist/${pkg}.tar.gz" "${pkg}"
|
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
|
- name: Smoke-test Unix artifact
|
||||||
if: runner.os != 'Windows'
|
if: runner.os != 'Windows'
|
||||||
|
|
@ -70,9 +78,40 @@ jobs:
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version="${GITHUB_REF_NAME:-manual}"
|
version="${GITHUB_REF_NAME:-manual}"
|
||||||
pkg="geth-${version}-${{ matrix.target }}"
|
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
|
mkdir -p smoke
|
||||||
tar -xzf "dist/${pkg}.tar.gz" -C smoke
|
tar -xzf "dist/${pkg}.tar.gz" -C smoke
|
||||||
"smoke/${pkg}/${{ matrix.binary }}" --version
|
"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}/README.md"
|
||||||
test -f "smoke/${pkg}/LICENSE-MIT"
|
test -f "smoke/${pkg}/LICENSE-MIT"
|
||||||
test -f "smoke/${pkg}/LICENSE-APACHE"
|
test -f "smoke/${pkg}/LICENSE-APACHE"
|
||||||
|
|
@ -90,6 +129,8 @@ jobs:
|
||||||
Copy-Item LICENSE-* "$pkg/"
|
Copy-Item LICENSE-* "$pkg/"
|
||||||
Copy-Item docs "$pkg/docs" -Recurse
|
Copy-Item docs "$pkg/docs" -Recurse
|
||||||
Compress-Archive -Path "$pkg/*" -DestinationPath "dist/$pkg.zip" -Force
|
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
|
- name: Smoke-test Windows artifact
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
|
|
@ -97,9 +138,32 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
$version = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { "manual" }
|
$version = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { "manual" }
|
||||||
$pkg = "geth-$version-${{ matrix.target }}"
|
$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
|
New-Item -ItemType Directory -Force -Path smoke | Out-Null
|
||||||
Expand-Archive -Path "dist/$pkg.zip" -DestinationPath "smoke/$pkg" -Force
|
Expand-Archive -Path "dist/$pkg.zip" -DestinationPath "smoke/$pkg" -Force
|
||||||
& "smoke/$pkg/${{ matrix.binary }}" --version
|
& "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/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-MIT")) { throw "LICENSE-MIT missing from archive" }
|
||||||
if (!(Test-Path "smoke/$pkg/LICENSE-APACHE")) { throw "LICENSE-APACHE missing from archive" }
|
if (!(Test-Path "smoke/$pkg/LICENSE-APACHE")) { throw "LICENSE-APACHE missing from archive" }
|
||||||
|
|
@ -118,12 +182,23 @@ jobs:
|
||||||
needs: build
|
needs: build
|
||||||
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout installer entrypoints
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: dist
|
path: dist
|
||||||
merge-multiple: true
|
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
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ resolver = "3"
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://example.invalid/local/geth"
|
repository = "https://forge.tionis.dev/eric/geth"
|
||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
||||||
54
README.md
54
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
|
This project is not the Ethereum `geth` client. The project and executable are
|
||||||
still named `geth`.
|
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
|
## First 10 Minutes
|
||||||
|
|
||||||
Build the single binary and start a disposable daemon:
|
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.
|
- `codeql.yml` builds the Rust workspace for GitHub CodeQL analysis.
|
||||||
- `dependency-review.yml` blocks pull requests that introduce vulnerable
|
- `dependency-review.yml` blocks pull requests that introduce vulnerable
|
||||||
dependency changes at moderate severity or higher.
|
dependency changes at moderate severity or higher.
|
||||||
- `release.yml` builds release archives for Linux, macOS, and Windows, includes
|
- `release.yml` builds release archives for Linux, Intel/Apple Silicon macOS,
|
||||||
README/docs/license files, smoke-tests the packaged binary from the archive,
|
and Windows, includes README/docs/license files, creates individual and
|
||||||
uploads artifacts, and publishes them on `v*` tags or manual dispatch.
|
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
|
- `docs/release-support-policy.md` defines supported platforms, compatibility
|
||||||
expectations, security update handling, and the user-level service boundary.
|
expectations, security update handling, and the user-level service boundary.
|
||||||
- `.github/dependabot.yml` opens weekly Cargo and GitHub Actions update PRs.
|
- `.github/dependabot.yml` opens weekly Cargo and GitHub Actions update PRs.
|
||||||
|
|
|
||||||
|
|
@ -809,7 +809,7 @@ fn systemd_unit(paths: &GethPaths, executable: &Path) -> String {
|
||||||
format!(
|
format!(
|
||||||
r#"[Unit]
|
r#"[Unit]
|
||||||
Description=geth personal mesh daemon
|
Description=geth personal mesh daemon
|
||||||
Documentation=https://example.invalid/local/geth
|
Documentation=https://forge.tionis.dev/eric/geth
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|
|
||||||
|
|
@ -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
|
- `[x]` Release archives are smoke-tested directly, not only through
|
||||||
`cargo run`.
|
`cargo run`.
|
||||||
- `[x]` Archives include relevant docs and license files.
|
- `[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.
|
- `[x]` Add upgrade tests.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pre-releases intended for dogfood and controlled automation only.
|
||||||
Release archives are built for:
|
Release archives are built for:
|
||||||
|
|
||||||
- Linux x86_64
|
- Linux x86_64
|
||||||
- macOS
|
- macOS x86_64 and arm64
|
||||||
- Windows x86_64
|
- Windows x86_64
|
||||||
|
|
||||||
The daemon and control CLI are supported as a single `geth` executable on those
|
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.
|
- CI must pass formatting, check, tests, and clippy.
|
||||||
- Security workflows must pass or have an explicit documented exception.
|
- Security workflows must pass or have an explicit documented exception.
|
||||||
- Release archives must be built by `.github/workflows/release.yml`.
|
- 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 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.
|
- Any breaking changes must be called out in release notes.
|
||||||
|
|
|
||||||
|
|
@ -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
|
- `[x]` Tests cover the full catalog, family filtering, common aliases, and
|
||||||
unknown-family recovery.
|
unknown-family recovery.
|
||||||
|
|
||||||
- `[ ]` Publish copy-paste installation entrypoints for release artifacts.
|
- `[x]` Publish copy-paste installation entrypoints for release artifacts.
|
||||||
Acceptance criteria:
|
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`.
|
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.
|
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.
|
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
|
## Long-Term Goal: Distributed Homelab Overlay
|
||||||
|
|
||||||
|
|
@ -425,8 +427,10 @@ control, local CAS, service installation, and written architecture decisions.
|
||||||
dispatch, and a weekly schedule.
|
dispatch, and a weekly schedule.
|
||||||
- `[x]` CodeQL and dependency review workflows are present for GitHub-native
|
- `[x]` CodeQL and dependency review workflows are present for GitHub-native
|
||||||
security scanning.
|
security scanning.
|
||||||
- `[x]` Release workflow builds Linux, macOS, and Windows archives for `v*`
|
- `[x]` Release workflow builds Linux x86_64, macOS x86_64/arm64, and Windows
|
||||||
tags and manual dispatch.
|
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]` Dependabot is configured for Cargo and GitHub Actions updates.
|
||||||
|
|
||||||
- `[x]` Bootstrap docs and ADRs.
|
- `[x]` Bootstrap docs and ADRs.
|
||||||
|
|
|
||||||
110
scripts/install.ps1
Normal file
110
scripts/install.ps1
Normal 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
216
scripts/install.sh
Executable 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"
|
||||||
Loading…
Reference in a new issue