Repository navigation
Expand file tree
/
Copy pathinstall.ps1
More file actions
187 lines (164 loc) · 8.13 KB
/
Copy pathinstall.ps1
File metadata and controls
187 lines (164 loc) · 8.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<#
.SYNOPSIS
Install io on Windows.
.DESCRIPTION
irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex
Downloads the artifact for this machine and the SHA256SUMS beside it,
VERIFIES THE ARTIFACT BEFORE EXPANDING IT, and installs into
%LOCALAPPDATA%\io\bin — a directory the current user owns. No administrator
rights, nothing written outside that directory and a temporary one it removes
on the way out — not your PATH either, which is printed for you to set rather
than set for you — and nothing left behind if anything fails.
The checksum defends against a truncated download and a tampered asset. It
does not defend against a compromised repository; piping a script from the
internet into a shell is a trust-the-publisher model however it is written.
It says all of that out loud while it happens: the target it resolved, where
the version came from, every URL it fetches, BOTH checksums before it compares
them, where the binary landed and what that binary says its version is.
Printing both checksums rather than "checksum ok" is the point — the operator
can see the comparison instead of being told its result, and an install that
only announces its own success is exactly the one nobody can audit. Failures
go through Fail, which writes to the error stream, so a log of what went right
never buries the one line somebody greps for.
.PARAMETER Version
Install this version instead of the latest. Also read from $env:IO_VERSION.
.PARAMETER InstallDir
Install here instead of %LOCALAPPDATA%\io\bin. Also $env:IO_INSTALL_DIR.
.PARAMETER BaseUrl
Download from here instead of the GitHub Release. Also $env:IO_BASE_URL.
#>
[CmdletBinding()]
param(
[string]$Version = $env:IO_VERSION,
[string]$InstallDir = $env:IO_INSTALL_DIR,
[string]$BaseUrl = $env:IO_BASE_URL
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$repo = 'initorigin/io-cli'
function Fail([string]$message) {
Write-Error "io install: $message"
exit 1
}
# Only the one target exists today. Saying so is better than installing an x64
# build on an arm64 machine and letting it fail at run time.
$arch = $env:PROCESSOR_ARCHITECTURE
if ($arch -ne 'AMD64') {
Fail "there is no Windows $arch build yet. Build from source: cargo build --release"
}
$target = 'x86_64-pc-windows-msvc'
Write-Host "detected Windows $arch -> target $target"
if ($Version) {
$versionFrom = 'IO_VERSION'
} else {
# The Release page redirects to the newest tag, so the newest version is
# readable without an API token and without parsing JSON.
$versionFrom = "https://github.com/$repo/releases/latest"
try {
$response = Invoke-WebRequest -Uri $versionFrom -MaximumRedirection 5 -UseBasicParsing
$final = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
} catch {
Fail "could not reach GitHub to find the latest version: $_"
}
if ($final -notmatch '/tag/v(?<v>[^/]+)$') {
Fail "could not work out the latest version from '$final'"
}
$Version = $Matches['v']
}
# Where the version came from decides which of two very different things just
# happened: an operator pinning a version, or this script trusting GitHub to name
# the newest one. Printing the number without its source hides that.
Write-Host "version $Version (from $versionFrom)"
if (-not $BaseUrl) {
$BaseUrl = "https://github.com/$repo/releases/download/v$Version"
}
if (-not $InstallDir) {
$InstallDir = Join-Path $env:LOCALAPPDATA 'io\bin'
}
$stage = "io-$Version-$target"
$archive = "$stage.zip"
# Everything happens in here until it has been verified, so a failure at any
# point leaves the target directory exactly as it was.
$work = Join-Path ([System.IO.Path]::GetTempPath()) ("io-install-" + [System.Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $work -Force | Out-Null
try {
$archivePath = Join-Path $work $archive
$sumsPath = Join-Path $work 'SHA256SUMS'
try {
# Announced before the fetch rather than after it, so a download that
# hangs or fails names the URL that was in flight.
Write-Host "downloading $BaseUrl/$archive"
Invoke-WebRequest -Uri "$BaseUrl/$archive" -OutFile $archivePath -UseBasicParsing
Write-Host "downloading $BaseUrl/SHA256SUMS"
Invoke-WebRequest -Uri "$BaseUrl/SHA256SUMS" -OutFile $sumsPath -UseBasicParsing
} catch {
Fail "could not download from $BaseUrl : $_"
}
$line = Get-Content $sumsPath | Where-Object { $_ -match "\s$([regex]::Escape($archive))$" } | Select-Object -First 1
if (-not $line) {
Fail "SHA256SUMS does not mention $archive"
}
$expected = ($line -split '\s+')[0].ToLowerInvariant()
$actual = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
# Both numbers, before the comparison. "checksum ok" on its own is a claim;
# these two lines are the evidence for it, and they are what makes a
# wrong-but-matching SHA256SUMS visible to somebody reading the output.
Write-Host "expected $expected"
Write-Host "computed $actual"
if ($expected -ne $actual) {
Fail "checksum mismatch for $archive`n expected $expected`n actual $actual`nNothing was installed."
}
Write-Host 'checksum ok'
Expand-Archive -Path $archivePath -DestinationPath $work -Force
$binary = Join-Path $work "$stage\io.exe"
if (-not (Test-Path $binary)) {
Fail "$archive does not contain io.exe"
}
Write-Host "unpacked $archive"
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
$installed = Join-Path $InstallDir 'io.exe'
Copy-Item -Path $binary -Destination $installed -Force
Write-Host "installed $installed"
# The PATH line is printed rather than written. A user PATH write is not a
# shell profile — it goes to the user's own environment rather than a file
# they keep — but it is still a change to the machine that the operator did
# not ask for, made by a script they piped into a shell, and it outlives the
# install in a place they have no reason to look. The line below is one they
# can read before they run it. Nothing is un-written either, so an install
# from before this changed keeps whatever user PATH entry it added — this
# script cannot tell its own old edit from one the operator made on purpose,
# so it takes neither away.
#
# What is read is the PROCESS PATH, and that is not interchangeable with the
# user PATH in the registry now that nothing is written. $env:Path is what
# the advice below actually changes, and it is what the machine, user and
# session entries all resolve into — so an entry made any of those ways is
# seen here on the next run. Reading the registry value instead would be
# reading the one place the printed line never reaches: an operator who did
# exactly as they were told would be told to do it again on every update,
# forever, and the other branch would become unreachable for everybody who
# complied. install.sh reads $PATH for the same reason.
$entries = @()
if ($env:Path) { $entries = $env:Path -split ';' | Where-Object { $_ } }
if ($entries -notcontains $InstallDir) {
Write-Host ''
Write-Host "$InstallDir is not on your PATH. Add this to your PowerShell profile:"
Write-Host ''
Write-Host " `$env:Path += ';$InstallDir'"
Write-Host ''
Write-Host 'then open a new terminal and run: io'
Write-Host 'That line reaches PowerShell. For cmd.exe, Explorer and GUI'
Write-Host 'programs, add it through System > Environment Variables.'
} else {
Write-Host "$InstallDir is on your PATH; run: io"
}
# The last line of the narration is the binary's own, not this script's: the
# only proof that what was verified and copied is a program this machine can
# run. A native command's exit code does not throw, so it is checked by hand.
& $installed --version
if ($LASTEXITCODE -ne 0) {
Fail "installed $installed but it will not run here"
}
} finally {
Remove-Item -Path $work -Recurse -Force -ErrorAction SilentlyContinue
}