-
-
Notifications
You must be signed in to change notification settings - Fork 227
Expand file tree
/
Copy pathinstall.ps1
More file actions
432 lines (387 loc) · 16.2 KB
/
Copy pathinstall.ps1
File metadata and controls
432 lines (387 loc) · 16.2 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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
param(
[string]$Distro = $env:HARBOR_WSL_DISTRO,
[string]$InstallPath = $env:HARBOR_INSTALL_SOURCE_PATH
)
$ErrorActionPreference = "Stop"
$InstallUrl = "https://raw.githubusercontent.com/av/harbor/refs/heads/main/install.sh"
$DockerDesktopArch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "amd64" }
$DockerDesktopInstallerUrl = "https://desktop.docker.com/win/main/$DockerDesktopArch/Docker%20Desktop%20Installer.exe"
# Supported WSL distro name prefixes, in preference order.
# Keep in sync with WSL_DISTRO_PREFIXES in app/src-tauri/src/setup.rs.
$SupportedDistroPrefixes = @("Ubuntu", "Debian", "Fedora", "openSUSE", "Kali", "Arch")
function Write-SetupStage {
param([string]$Stage)
Write-Output "HARBOR_SETUP_STAGE=$Stage"
}
function Invoke-Wsl {
param([string[]]$Arguments)
& wsl.exe @Arguments
if ($LASTEXITCODE -ne 0) {
throw "wsl.exe $($Arguments -join ' ') exited with code $LASTEXITCODE"
}
}
function Test-WslAvailable {
if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) {
return $false
}
# Capture output without piping through Write-Output, which can
# reset $LASTEXITCODE in some PowerShell versions.
#
# Use a local $ErrorActionPreference override because 2>&1 converts
# native-command stderr lines into ErrorRecords. Under the script's
# global $ErrorActionPreference = "Stop" (PowerShell 5.1), even a
# single ErrorRecord triggers a terminating exception — killing the
# whole script instead of returning $false.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "SilentlyContinue"
try {
$output = & wsl.exe --status 2>&1
$result = $LASTEXITCODE -eq 0
} finally {
$ErrorActionPreference = $prevEAP
}
# Write-Host routes to the PTY display without entering the output stream,
# keeping the return value a pure boolean.
if ($output) { Write-Host ($output -join "`n") }
return $result
}
function Normalize-WslOutput {
param([string]$Text)
# Remove null bytes (UTF-16LE artifacts) and the BOM (U+FEFF) that
# wsl.exe sometimes emits. Without this, regex anchors like ^ fail to
# match the first line.
$Text -replace [string][char]0, "" -replace ([string][char]0xFEFF), ""
}
function ConvertTo-BashSingleQuoted {
param([string]$Value)
"'" + $Value.Replace("'", "'\''") + "'"
}
function Get-HarborInstallCommand {
if ([string]::IsNullOrWhiteSpace($InstallPath)) {
return "curl -fsSL --connect-timeout 15 --max-time 60 '$InstallUrl' | bash"
}
$quotedPath = ConvertTo-BashSingleQuoted $InstallPath
return @(
"source_path=`$(wslpath -a $quotedPath)"
"bash `"`$source_path/install.sh`" --source-path `"`$source_path`" --requirements-path `"`$source_path/requirements.sh`" --version source"
) -join " && "
}
function Get-DockerDesktopPath {
$candidates = @()
if (-not [string]::IsNullOrWhiteSpace($env:LOCALAPPDATA)) {
$candidates += Join-Path $env:LOCALAPPDATA "Programs\Docker\Docker\Docker Desktop.exe"
}
if (-not [string]::IsNullOrWhiteSpace(${env:ProgramFiles})) {
$candidates += Join-Path ${env:ProgramFiles} "Docker\Docker\Docker Desktop.exe"
}
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
return $candidate
}
}
return $null
}
function Test-DockerDesktopInstalled {
if (Get-DockerDesktopPath) {
return $true
}
$registryRoots = @(
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($root in $registryRoots) {
$match = Get-ItemProperty $root -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like "Docker Desktop*" } |
Select-Object -First 1
if ($match) {
return $true
}
}
return $false
}
function Start-DockerDesktop {
$desktopPath = Get-DockerDesktopPath
if ([string]::IsNullOrWhiteSpace($desktopPath)) {
# Fallback: look up the install location from the registry (same roots
# that Test-DockerDesktopInstalled uses to detect presence).
$registryRoots = @(
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($root in $registryRoots) {
$match = Get-ItemProperty $root -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like "Docker Desktop*" } |
Select-Object -First 1
if ($match -and $match.InstallLocation) {
$candidate = Join-Path $match.InstallLocation "Docker Desktop.exe"
if (Test-Path $candidate) {
$desktopPath = $candidate
break
}
}
}
}
if ([string]::IsNullOrWhiteSpace($desktopPath)) {
Write-Output "WARNING: Could not find Docker Desktop executable. Please start Docker Desktop manually."
return
}
Write-Output "Starting Docker Desktop."
try {
Start-Process -FilePath $desktopPath
} catch {
Write-Output "WARNING: Failed to start Docker Desktop ($($_.Exception.Message)). Please start it manually."
}
}
function Install-DockerDesktop {
$installerPath = Join-Path $env:TEMP "Docker Desktop Installer.exe"
Remove-Item -Path $installerPath -Force -ErrorAction SilentlyContinue
if (Get-Command winget -ErrorAction SilentlyContinue) {
Write-Output "Installing Docker Desktop with winget."
& winget install --exact --id Docker.DockerDesktop --accept-package-agreements --accept-source-agreements
if ($LASTEXITCODE -eq 0) {
return
}
# winget failed — fall back to direct download. This can happen when
# winget is present but misconfigured (broken source, network proxy
# blocking the msstore, expired agreements, etc.).
Write-Output "WARNING: winget install failed (exit code $LASTEXITCODE). Falling back to direct download."
}
Write-Output "Installing Docker Desktop with the official Docker Desktop installer."
# Suppress the progress bar for the duration of the download. Under
# Windows PowerShell 5.1 the progress bar makes Invoke-WebRequest orders
# of magnitude slower for large files. Function-scoped preference variables
# in PowerShell revert automatically when the function returns, so this
# does not leak to the caller's session.
$ProgressPreference = 'SilentlyContinue'
# Retry the download up to 3 times. The installer is ~600MB and network
# interruptions are common, especially on slower connections.
$maxAttempts = 3
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
try {
if ($attempt -gt 1) {
Write-Output "Download attempt $attempt of $maxAttempts..."
}
Invoke-WebRequest -UseBasicParsing -Uri $DockerDesktopInstallerUrl -OutFile $installerPath
break
} catch {
if ($attempt -eq $maxAttempts) {
throw "Failed to download Docker Desktop installer after $maxAttempts attempts: $_"
}
Write-Output "Download failed: $_. Retrying in 5 seconds..."
Start-Sleep -Seconds 5
}
}
if (-not (Test-Path $installerPath)) {
throw "Docker Desktop installer was not downloaded. Check your network connection and try again."
}
try {
$installer = Start-Process -FilePath $installerPath -Wait -PassThru -ArgumentList @("install", "--user")
if ($installer.ExitCode -ne 0) {
throw "Docker Desktop installer exited with code $($installer.ExitCode)"
}
} finally {
# Clean up the ~600MB installer to avoid leaving stale files in TEMP.
Remove-Item -Path $installerPath -Force -ErrorAction SilentlyContinue
}
}
function Test-WslDockerReady {
param([string]$TargetDistro)
# Use timeout to prevent docker info from hanging when the daemon is
# starting up or unresponsive. The 15-second limit matches DETECT_TIMEOUT
# in setup.rs.
$job = Start-Job -ScriptBlock {
param($d)
# Suppress all output so that only the exit code marker reaches the
# output stream. Without this, wsl.exe startup messages (e.g.
# "Starting <distro>...") end up in the Receive-Job result, turning
# it into an array and breaking the exit-code comparison.
& wsl.exe -d $d -e bash -lic "docker info >/dev/null 2>&1 && docker compose version >/dev/null 2>&1" *> $null
$LASTEXITCODE
} -ArgumentList $TargetDistro
$completed = Wait-Job $job -Timeout 15
if ($null -eq $completed) {
Stop-Job $job
Remove-Job $job -Force
return $false
}
$output = Receive-Job $job
Remove-Job $job -Force
# If Receive-Job still returns multiple objects (e.g. from shell
# profile output), use only the last one — the explicit $LASTEXITCODE.
if ($output -is [array]) {
$exitCode = $output[-1]
} else {
$exitCode = $output
}
return $exitCode -eq 0
}
function Wait-WslDockerReady {
param(
[string]$TargetDistro,
[int]$TimeoutSeconds = 180
)
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if (Test-WslDockerReady $TargetDistro) {
return $true
}
Write-Output "Waiting for Docker Desktop to become reachable inside WSL distro '$TargetDistro'."
Start-Sleep -Seconds 5
}
return $false
}
Write-SetupStage "checking-platform"
if (-not (Test-WslAvailable)) {
Write-SetupStage "installing-prerequisites"
Write-Output "WSL is not available. Starting the Windows-supported WSL installation flow."
& wsl.exe --install
if ($LASTEXITCODE -ne 0) {
Write-SetupStage "failed"
Write-Host "WSL installation failed (exit code $LASTEXITCODE). Run 'wsl --install' manually from an elevated PowerShell prompt."
exit 1
}
Write-SetupStage "refresh-required"
Write-Host "WSL installation started. Restart Windows or complete distro first-run setup, then retry Harbor setup."
exit 1
}
Write-SetupStage "checking-prerequisites"
$distros = Normalize-WslOutput ((& wsl.exe --list --verbose) -join "`n")
# Get running distro names via --list --running (locale-independent — the
# output is just names, no state column). Used below to prefer a running
# WSL2 distro over a stopped one without relying on the English word
# "Running" in the --list --verbose output.
# wsl.exe --list --running exits non-zero when no distros are running,
# potentially writing an error message to stdout (locale-dependent).
# Capture $LASTEXITCODE so we can discard the output when the command fails,
# avoiding false-positive distro name matches against the error text.
$runningRaw = (& wsl.exe --list --running) -join "`n"
if ($LASTEXITCODE -eq 0) {
$runningDistros = Normalize-WslOutput $runningRaw
} else {
$runningDistros = ""
}
if ([string]::IsNullOrWhiteSpace($Distro)) {
# Try each supported distro prefix in preference order,
# preferring a running WSL2 distro over a stopped one.
# Use .+ instead of \w+ for the state field because non-English
# Windows locales can produce multi-word state names (e.g. German
# "Wird ausgeführt" for "Running").
foreach ($prefix in $SupportedDistroPrefixes) {
if ($distros -match "(?m)^\s*\*?\s*($([regex]::Escape($prefix))[^\s]*)\s+.+\s+2\s*$") {
$candidate = $Matches[1]
# Check if this WSL2 distro is currently running. The --list
# --running output may include a "(Default)" suffix after the name.
if ($runningDistros -match "(?m)^\s*$([regex]::Escape($candidate))(\s|\s*$)") {
$Distro = $candidate
break
}
}
}
if ([string]::IsNullOrWhiteSpace($Distro)) {
foreach ($prefix in $SupportedDistroPrefixes) {
if ($distros -match "(?m)^\s*\*?\s*($([regex]::Escape($prefix))[^\s]*)\s+.+\s+2\s*$") {
$Distro = $Matches[1]
break
}
}
}
}
if ([string]::IsNullOrWhiteSpace($Distro)) {
# Check if there is a WSL1 distro that matches — give a specific upgrade message.
$wsl1Match = $null
foreach ($prefix in $SupportedDistroPrefixes) {
if ($distros -match "(?m)^\s*\*?\s*($([regex]::Escape($prefix))[^\s]*)\s+.+\s+1\s*$") {
$wsl1Match = $Matches[1]
break
}
}
if ($wsl1Match) {
Write-SetupStage "blocked"
Write-Host "Found WSL1 distro '$wsl1Match' but Harbor requires WSL2. Upgrade it with: wsl --set-version $wsl1Match 2"
exit 1
}
Write-SetupStage "installing-prerequisites"
$supportedNames = $SupportedDistroPrefixes -join ", "
Write-Output "No supported WSL2 distro found (checked: $supportedNames). Installing Ubuntu."
& wsl.exe --install -d Ubuntu
if ($LASTEXITCODE -ne 0) {
Write-SetupStage "failed"
Write-Host "Ubuntu WSL installation failed (exit code $LASTEXITCODE). Run 'wsl --install -d Ubuntu' manually from an elevated PowerShell prompt."
exit 1
}
Write-SetupStage "refresh-required"
Write-Host "Ubuntu WSL installation started. Complete first-run account setup, then retry Harbor setup."
exit 1
}
# Verify the selected distro is actually WSL2.
if ($distros -match "(?m)^\s*\*?\s*$([regex]::Escape($Distro))\s+.+\s+1\s*$") {
Write-SetupStage "blocked"
Write-Host "Selected distro '$Distro' is running under WSL1. Harbor requires WSL2. Upgrade it with: wsl --set-version $Distro 2"
exit 1
} elseif ($distros -notmatch "(?m)^\s*\*?\s*$([regex]::Escape($Distro))\s+.+\s+2\s*$") {
Write-SetupStage "blocked"
Write-Host "Selected distro '$Distro' is not a WSL2 distro or is not installed. Set HARBOR_WSL_DISTRO to a valid WSL2 distro name."
exit 1
}
try {
# Use Start-Job with a timeout to prevent hanging on broken/unresponsive
# WSL distros (e.g., filesystem corruption, hung systemd init, interactive
# first-run setup prompts). Without a timeout, this blocks the entire
# install for up to 30 minutes (the Tauri run_logged timeout).
$healthJob = Start-Job -ScriptBlock {
param($d)
& wsl.exe -d $d -e bash -lic "uname -s && command -v bash && command -v curl" *> $null
$LASTEXITCODE
} -ArgumentList $Distro
$healthCompleted = Wait-Job $healthJob -Timeout 30
if ($null -eq $healthCompleted) {
Stop-Job $healthJob
Remove-Job $healthJob -Force
throw "WSL distro '$Distro' did not respond within 30 seconds"
}
$healthOutput = Receive-Job $healthJob
Remove-Job $healthJob -Force
if ($healthOutput -is [array]) { $healthOutput = $healthOutput[-1] }
if ($healthOutput -ne 0) {
throw "WSL distro '$Distro' health check failed (exit code $healthOutput)"
}
} catch {
Write-SetupStage "blocked"
Write-Host "WSL distro '$Distro' cannot run basic commands (bash, curl). The distro may need a first-run setup (username/password creation) or may be corrupted. Run 'wsl -d $Distro' in a terminal to check, then retry."
exit 1
}
Write-SetupStage "checking-prerequisites"
if (-not (Test-DockerDesktopInstalled)) {
Write-SetupStage "installing-prerequisites"
try {
Install-DockerDesktop
Start-DockerDesktop
} catch {
Write-SetupStage "failed"
# Emitted after the marker so the app surfaces it as the error detail.
Write-Output $_.Exception.Message
exit 1
}
Write-SetupStage "refresh-required"
Write-Host "Docker Desktop installation completed. Open Docker Desktop, accept required first-run prompts, enable WSL integration for '$Distro', then retry Harbor setup."
exit 1
}
Start-DockerDesktop
if (-not (Wait-WslDockerReady $Distro)) {
Write-SetupStage "blocked"
Write-Host "Docker Desktop did not become reachable inside WSL distro '$Distro' within 3 minutes. Possible causes: (1) Docker Desktop requires accepting the EULA/subscription agreement on first launch, (2) WSL integration is not enabled for '$Distro' in Docker Desktop Settings > Resources > WSL Integration, (3) Docker Desktop failed to start. Open Docker Desktop manually, complete any first-run prompts, then retry."
exit 1
}
Write-SetupStage "installing-cli"
# install.sh emits its own HARBOR_SETUP_STAGE markers (checking-platform,
# installing-prerequisites, installing-cli, linking-cli, verifying-cli, ready)
# that flow through WSL stdout to the Tauri PTY reader. On success it sets
# "ready"; on failure it sets "blocked", "refresh-required", or "failed" and
# exits non-zero (which Invoke-Wsl converts to a throw).
# Do NOT re-run "harbor doctor" or re-emit "verifying-cli" here — that would
# cause the step indicator to regress from "ready" back to "verify" briefly
# and duplicate work that install.sh already completed.
Invoke-Wsl @("-d", $Distro, "-e", "bash", "-lic", "$(Get-HarborInstallCommand)")
Write-Output "Harbor CLI installed in WSL distro '$Distro'."