Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/installer-scripts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# The installers are the one part of Wayfinder that users run before they have
# Wayfinder, and they are not Dart, so nothing else in CI parses them. These
# checks need no release archive and no network, so they answer in a minute;
# ci.yml installs the archive it just built, and verify-public-install.yml
# installs from the published URLs.
name: Installer scripts

on:
push:
branches: [main]
paths:
- .github/workflows/installer-scripts.yml
- tool/install.ps1
- tool/install.sh
- tool/test_install_arch.ps1
- tool/test_install_platform.sh
- tool/verify_installer.ps1
pull_request:
paths:
- .github/workflows/installer-scripts.yml
- tool/install.ps1
- tool/install.sh
- tool/test_install_arch.ps1
- tool/test_install_platform.sh
- tool/verify_installer.ps1

permissions:
contents: read

concurrency:
group: pr-${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
posix:
name: install.sh
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Check the POSIX scripts
run: |
for script in tool/install.sh tool/test_install_platform.sh; do
sh -n "$script"
shellcheck --shell=sh "$script"
echo "OK: $script"
done
- name: Test platform detection
run: sh tool/test_install_platform.sh

windows:
# Windows PowerShell 5.1 is what a fresh Windows session runs, and it is
# where the #98 report came from; PowerShell 7 is what a developer machine
# usually has. The installer has to work in both.
name: install.ps1 (${{ matrix.powershell }})
strategy:
fail-fast: false
matrix:
powershell: [pwsh, powershell]
runs-on: windows-latest
timeout-minutes: 10
defaults:
run:
shell: ${{ matrix.powershell }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Parse the PowerShell scripts
run: |
foreach ($File in 'tool/install.ps1', 'tool/test_install_arch.ps1', 'tool/verify_installer.ps1') {
$Tokens = $null
$Errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path $File).Path, [ref]$Tokens, [ref]$Errors) | Out-Null
if ($Errors.Count) { throw "$File has a parse error: $($Errors[0].Message)" }
Write-Host "OK: $File"
}
- name: Test architecture detection
run: ./tool/test_install_arch.ps1
13 changes: 13 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,19 @@ installer. If search reports a missing or stale index, run `wayfinder index` on
the same bundle. Preserve any operating-system security warning and inspect the
release source; the installer does not disable platform protections.

On Windows, `Could not determine this machine's processor architecture` means
the session's .NET architecture probe returned nothing rather than that the
machine is unsupported; some interactive Windows PowerShell 5.1 sessions do
this. Install from a clean session instead:

```powershell
powershell -NoProfile -Command "irm https://raw.githubusercontent.com/conceptadev/wayfinder/main/tool/install.ps1 | iex"
```

A message naming a machine, such as `Windows Arm64 has no prebuilt Wayfinder
bundle` or `Darwin-x86_64 has no prebuilt Wayfinder bundle`, reports a machine
both installers did detect and that we do not yet publish a verified bundle for.

To uninstall on macOS/Linux, remove the installer-owned command symlinks and
runtime directories. On Windows, remove its recorded runtime bin entry from your
user `PATH` and delete the runtime directory. Remove the plugin separately.
Expand Down
21 changes: 0 additions & 21 deletions tool/ci/verify-installer.sh

This file was deleted.

72 changes: 57 additions & 15 deletions tool/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,62 @@
$ErrorActionPreference = 'Stop'
# Windows PowerShell 5.1 downloads far slower while drawing progress.
$ProgressPreference = 'SilentlyContinue'
# Windows PowerShell 5.1 hosts can still default to TLS 1.0, which GitHub
# refuses with an unhelpful transport error. PowerShell 7 already negotiates
# TLS 1.2, and this setting outlives the script, so only older hosts pay it.
if ($PSVersionTable.PSVersion.Major -lt 6) {
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
}
function Get-MachineArchitecture {
# The .NET probe is absent before .NET 4.7.1, and some interactive Windows
# PowerShell 5.1 sessions return nothing from it instead of an architecture,
# so an empty result means unknown rather than unsupported (#98).
try {
$Probe = [string][System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
if ($Probe) { return $Probe }
} catch {
# The name resolved to a type without a usable OSArchitecture.
}
# Windows' own machine-scope environment reports the hardware, even inside
# an emulated process, where the process variable claims AMD64 on an Arm64
# machine. PROCESSOR_ARCHITEW6432 reports it for a 32-bit process on 64-bit
# Windows, where PROCESSOR_ARCHITECTURE reports x86.
$Fallback = ''
try { $Fallback = [Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE', 'Machine') } catch { }
if (-not $Fallback) { $Fallback = $env:PROCESSOR_ARCHITEW6432 }
if (-not $Fallback) { $Fallback = $env:PROCESSOR_ARCHITECTURE }
if ($Fallback -eq 'AMD64') { return 'X64' }
return $Fallback
}
# Check the machine, its tools and every caller input before any download, so
# a typo costs no round trip and no partial state. install.sh does the same.
$Architecture = Get-MachineArchitecture
if (-not $Architecture) {
throw ('Could not determine this machine''s processor architecture; the installer ' +
'did not assume one. On Windows x64, install from a clean session with: ' +
'powershell -NoProfile -Command "irm ' +
'https://raw.githubusercontent.com/conceptadev/wayfinder/main/tool/install.ps1 | iex"')
}
if ($Architecture -ne 'X64') {
throw "Windows $Architecture has no prebuilt Wayfinder bundle; only Windows x64 has one."
}
# Windows has shipped tar since Windows 10 1803; name it when it is missing.
if (-not (Get-Command tar -ErrorAction SilentlyContinue)) {
throw 'Required command is missing: tar'
}
$Skills = if ($env:WAYFINDER_SKILLS) { $env:WAYFINDER_SKILLS } else { 'all' }
if ($Skills -notin @('all', 'claude', 'agents', 'none')) {
throw 'WAYFINDER_SKILLS must be all, claude, agents or none.'
}
# Windows exposes the current bundle bin directory through the user PATH. Each
# version stays intact, so updating never replaces DLLs held by running programs.
$RuntimeRoot = if ($env:WAYFINDER_INSTALL_ROOT) { $env:WAYFINDER_INSTALL_ROOT } else {
Join-Path $env:LOCALAPPDATA 'WayfinderRuntime'
}
if (-not [System.IO.Path]::IsPathRooted($RuntimeRoot)) {
throw 'WAYFINDER_INSTALL_ROOT must be an absolute path.'
}
# The default is the newest stable Wayfinder release. WAYFINDER_VERSION selects
# another published release; CI and `wayfinder update` use it to pin a version.
$WayfinderVersion = $env:WAYFINDER_VERSION
Expand All @@ -26,27 +82,13 @@ if (-not $WayfinderVersion) {
if ($WayfinderVersion -notmatch '^\d+\.\d+\.\d+(-[A-Za-z0-9.-]+)?$') {
throw 'WAYFINDER_VERSION must be a published release version.'
}
$Skills = if ($env:WAYFINDER_SKILLS) { $env:WAYFINDER_SKILLS } else { 'all' }
if ($Skills -notin @('all', 'claude', 'agents', 'none')) {
throw 'WAYFINDER_SKILLS must be all, claude, agents or none.'
}
$ReleaseRoot = "https://github.com/conceptadev/wayfinder/releases/download/wayfinder-v$WayfinderVersion"
if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -ne 'X64') {
throw 'Only Windows x64 has a prebuilt Wayfinder bundle.'
}
$RuntimeRoot = if ($env:WAYFINDER_INSTALL_ROOT) { $env:WAYFINDER_INSTALL_ROOT } else {
Join-Path $env:LOCALAPPDATA 'WayfinderRuntime'
}
if (-not [System.IO.Path]::IsPathRooted($RuntimeRoot)) {
throw 'WAYFINDER_INSTALL_ROOT must be an absolute path.'
}
# Windows exposes the current bundle bin directory through the user PATH. Each
# version stays intact, so updating never replaces DLLs held by running programs.
$Asset = 'wayfinder-windows-x64.tar.gz'
$Stage = Join-Path $RuntimeRoot ('.install.' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $Stage -Force | Out-Null
try {
$Archive = Join-Path $Stage $Asset
Write-Host "Downloading Wayfinder $WayfinderVersion for windows-x64..."
Invoke-WebRequest "$ReleaseRoot/$Asset" -OutFile $Archive -UseBasicParsing
$Checksum = Join-Path $Stage 'checksum'
Invoke-WebRequest "$ReleaseRoot/$Asset.sha256" -OutFile $Checksum -UseBasicParsing
Expand Down
29 changes: 14 additions & 15 deletions tool/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ case "$skills" in
esac
install_dir="${WAYFINDER_INSTALL_DIR:-$HOME/.local/bin}"
runtime_root="${WAYFINDER_INSTALL_ROOT:-$HOME/.local/share/wayfinder-runtime}"
case "$(uname -s)-$(uname -m)" in
# Name the detected platform, so an unsupported machine is never confused with
# a failed detection, and install.ps1 reports its architecture the same way.
machine="$(uname -s)-$(uname -m)"
case "$machine" in
Darwin-arm64) platform=macos-arm64 ;;
Linux-x86_64) platform=linux-x64 ;;
*) fail 'This platform has no verified prebuilt bundle. See the installation guide.' ;;
*) fail "$machine has no prebuilt Wayfinder bundle; macOS Apple Silicon and Linux x64 have one. See the installation guide." ;;
esac
case "$install_dir:$runtime_root" in
/*:/*) ;;
Expand Down Expand Up @@ -58,16 +61,14 @@ verify_contents() {
}
mkdir -p "$runtime_root" "$install_dir"
# Refuse to replace a command managed by another installation method.
for name in wayfinder; do
target="$install_dir/$name"
if [ -e "$target" ] || [ -L "$target" ]; then
[ -L "$target" ] || fail "$target already exists; choose another WAYFINDER_INSTALL_DIR."
case "$(readlink "$target")" in
"$runtime_root"/*) ;;
*) fail "$target belongs to another installation; choose another WAYFINDER_INSTALL_DIR." ;;
esac
fi
done
target="$install_dir/wayfinder"
if [ -e "$target" ] || [ -L "$target" ]; then
[ -L "$target" ] || fail "$target already exists; choose another WAYFINDER_INSTALL_DIR."
case "$(readlink "$target")" in
"$runtime_root"/*) ;;
*) fail "$target belongs to another installation; choose another WAYFINDER_INSTALL_DIR." ;;
esac
fi
stage="$(mktemp -d "$runtime_root/.install.XXXXXXXX")"
trap 'rm -rf "$stage"' EXIT HUP INT TERM
asset="wayfinder-$platform.tar.gz"
Expand All @@ -85,9 +86,7 @@ verify_contents "$stage/bundle" || fail 'Bundle contents failed verification.'
# processes, previous runtimes and user indexes. Reinstalling also repairs assets.
destination="$runtime_root/$version-$(basename "$stage")"
mv "$stage/bundle" "$destination"
for name in wayfinder; do
ln -sfn "$destination/bin/$name" "$install_dir/$name"
done
ln -sfn "$destination/bin/wayfinder" "$install_dir/wayfinder"
# wayfinder update reinstalls into the same command directory.
printf '%s\n' "$install_dir" > "$runtime_root/install-dir.txt"
printf 'Installed Wayfinder %s in %s\n' "$version" "$install_dir"
Expand Down
90 changes: 90 additions & 0 deletions tool/test_install_arch.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Architecture detection is the one part of install.ps1 that running the real
# installer cannot cover: a .NET static property cannot be shadowed the way
# tool/verify_installer.ps1 shadows Invoke-WebRequest. Each case runs a copy of
# install.ps1 with that probe substituted, and asserts every substitution
# matched, so a rewritten installer fails here instead of passing vacuously.
#
# Every case stops at a check that precedes the first download, so the test
# needs no network and installs nothing. Cases that must clear the architecture
# gate set an unusable WAYFINDER_VERSION and expect its rejection as proof.
$ErrorActionPreference = 'Stop'
$Source = Get-Content (Join-Path $PSScriptRoot 'install.ps1') -Raw
$ProbeExpression = '[string][System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture'
$MachineExpression = "[Environment]::GetEnvironmentVariable('PROCESSOR_ARCHITECTURE', 'Machine')"
$PassedGate = 'WAYFINDER_VERSION must be a published release version.'
$TestRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('wayfinder arch ' + [guid]::NewGuid().ToString('N'))
$OriginalVersion = $env:WAYFINDER_VERSION
$OriginalRoot = $env:WAYFINDER_INSTALL_ROOT
$OriginalWow = $env:PROCESSOR_ARCHITEW6432
$OriginalProcess = $env:PROCESSOR_ARCHITECTURE
New-Item -ItemType Directory $TestRoot | Out-Null
# Nothing here reaches an installation, but no case may depend on the host's
# LOCALAPPDATA to clear the caller-input checks that precede the version check.
$env:WAYFINDER_INSTALL_ROOT = Join-Path $TestRoot 'runtime'

# Returns the message install.ps1 fails with, or '' when it does not fail.
function Get-Refusal {
param(
[Parameter(Mandatory = $true)][string]$Case,
[Parameter(Mandatory = $true)][hashtable]$Substitutions
)
$Text = $Source
foreach ($Substitution in $Substitutions.GetEnumerator()) {
if (-not $Text.Contains($Substitution.Key)) {
throw "install.ps1 no longer contains, so $Case cannot be tested: $($Substitution.Key)"
}
$Text = $Text.Replace($Substitution.Key, $Substitution.Value)
}
$Copy = Join-Path $TestRoot "install-$Case.ps1"
Set-Content $Copy $Text -Encoding UTF8
try { & $Copy } catch { return $_.Exception.Message }
return ''
}

function Assert-Refusal {
param(
[Parameter(Mandatory = $true)][string]$Case,
[Parameter(Mandatory = $true)][hashtable]$Substitutions,
[Parameter(Mandatory = $true)][string]$Expected
)
$Message = Get-Refusal -Case $Case -Substitutions $Substitutions
if ($Message -notlike "*$Expected*") {
throw "$Case`: expected a failure like '$Expected', got '$Message'."
}
Write-Host "OK: $Case"
}

try {
# The #98 regression. An empty probe is unknown, not unsupported, so the
# machine environment decides and this x64 runner clears the gate.
$env:WAYFINDER_VERSION = 'unusable'
Assert-Refusal -Case 'empty-probe-uses-the-environment' -Expected $PassedGate `
-Substitutions @{ $ProbeExpression = "''" }

# AMD64 is how Windows names x64 in the environment.
Assert-Refusal -Case 'amd64-environment-is-x64' -Expected $PassedGate `
-Substitutions @{ $ProbeExpression = "''"; $MachineExpression = "'AMD64'" }

# A probe that does report the machine still decides.
Assert-Refusal -Case 'arm64-probe-is-refused' -Expected 'Windows Arm64 has no prebuilt' `
-Substitutions @{ $ProbeExpression = "'Arm64'" }

# An Arm64 machine running an emulated x64 process: the machine scope
# reports the hardware that the process variable would misreport as AMD64.
Assert-Refusal -Case 'arm64-environment-is-refused' -Expected 'Windows ARM64 has no prebuilt' `
-Substitutions @{ $ProbeExpression = "''"; $MachineExpression = "'ARM64'" }

# Nothing to go on: say so, rather than name an architecture (#98).
$env:PROCESSOR_ARCHITEW6432 = ''
$env:PROCESSOR_ARCHITECTURE = ''
Assert-Refusal -Case 'indeterminate-is-reported' -Expected 'Could not determine' `
-Substitutions @{ $ProbeExpression = "''"; $MachineExpression = '$null' }

Write-Host 'PASS: install.ps1 architecture detection, fallback and refusals.'
} finally {
$env:WAYFINDER_VERSION = $OriginalVersion
$env:WAYFINDER_INSTALL_ROOT = $OriginalRoot
$env:PROCESSOR_ARCHITEW6432 = $OriginalWow
$env:PROCESSOR_ARCHITECTURE = $OriginalProcess
Remove-Item $TestRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Loading
Loading