Skip to content

Commit 4fd2161

Browse files
committed
fix: harden windows installer bootstrap
1 parent 74121a8 commit 4fd2161

File tree

1 file changed

+201
-17
lines changed

1 file changed

+201
-17
lines changed

public/install.ps1

Lines changed: 201 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,157 @@ function Check-Git {
146146
}
147147
}
148148

149-
function Require-Git {
149+
function Add-ToProcessPath {
150+
param(
151+
[Parameter(Mandatory = $true)]
152+
[string]$PathEntry
153+
)
154+
155+
if ([string]::IsNullOrWhiteSpace($PathEntry)) {
156+
return
157+
}
158+
159+
$currentEntries = @($env:Path -split ";" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
160+
if ($currentEntries | Where-Object { $_ -ieq $PathEntry }) {
161+
return
162+
}
163+
164+
$env:Path = "$PathEntry;$env:Path"
165+
}
166+
167+
function Get-PortableGitRoot {
168+
$base = Join-Path $env:LOCALAPPDATA "OpenClaw\deps"
169+
return (Join-Path $base "portable-git")
170+
}
171+
172+
function Get-PortableGitCommandPath {
173+
$root = Get-PortableGitRoot
174+
foreach ($candidate in @(
175+
(Join-Path $root "mingw64\bin\git.exe"),
176+
(Join-Path $root "cmd\git.exe"),
177+
(Join-Path $root "bin\git.exe"),
178+
(Join-Path $root "git.exe")
179+
)) {
180+
if (Test-Path $candidate) {
181+
return $candidate
182+
}
183+
}
184+
return $null
185+
}
186+
187+
function Use-PortableGitIfPresent {
188+
$gitExe = Get-PortableGitCommandPath
189+
if (-not $gitExe) {
190+
return $false
191+
}
192+
193+
$portableRoot = Get-PortableGitRoot
194+
foreach ($pathEntry in @(
195+
(Join-Path $portableRoot "mingw64\bin"),
196+
(Join-Path $portableRoot "usr\bin"),
197+
(Split-Path -Parent $gitExe)
198+
)) {
199+
if (Test-Path $pathEntry) {
200+
Add-ToProcessPath $pathEntry
201+
}
202+
}
203+
if (Check-Git) {
204+
return $true
205+
}
206+
return $false
207+
}
208+
209+
function Resolve-PortableGitDownload {
210+
$releaseApi = "https://api.github.com/repos/git-for-windows/git/releases/latest"
211+
$headers = @{
212+
"User-Agent" = "openclaw-installer"
213+
"Accept" = "application/vnd.github+json"
214+
}
215+
$release = Invoke-RestMethod -Uri $releaseApi -Headers $headers
216+
if (-not $release -or -not $release.assets) {
217+
throw "Could not resolve latest git-for-windows release metadata."
218+
}
219+
220+
$asset = $release.assets |
221+
Where-Object { $_.name -match '^MinGit-.*-64-bit\.zip$' -and $_.name -notmatch 'busybox' } |
222+
Select-Object -First 1
223+
224+
if (-not $asset) {
225+
throw "Could not find a MinGit zip asset in the latest git-for-windows release."
226+
}
227+
228+
return @{
229+
Tag = $release.tag_name
230+
Name = $asset.name
231+
Url = $asset.browser_download_url
232+
}
233+
}
234+
235+
function Install-PortableGit {
236+
if (Use-PortableGitIfPresent) {
237+
$portableVersion = (& git --version 2>$null)
238+
if ($portableVersion) {
239+
Write-Host "[OK] User-local Git already available: $portableVersion" -ForegroundColor Green
240+
}
241+
return
242+
}
243+
244+
Write-Host "[*] Git not found; bootstrapping user-local portable Git..." -ForegroundColor Yellow
245+
246+
$download = Resolve-PortableGitDownload
247+
$portableRoot = Get-PortableGitRoot
248+
$portableParent = Split-Path -Parent $portableRoot
249+
$tmpZip = Join-Path $env:TEMP $download.Name
250+
$tmpExtract = Join-Path $env:TEMP ("openclaw-portable-git-" + [guid]::NewGuid().ToString("N"))
251+
252+
New-Item -ItemType Directory -Force -Path $portableParent | Out-Null
253+
if (Test-Path $portableRoot) {
254+
Remove-Item -Recurse -Force $portableRoot
255+
}
256+
if (Test-Path $tmpExtract) {
257+
Remove-Item -Recurse -Force $tmpExtract
258+
}
259+
New-Item -ItemType Directory -Force -Path $tmpExtract | Out-Null
260+
261+
try {
262+
Write-Host " Downloading $($download.Tag)..." -ForegroundColor Gray
263+
Invoke-WebRequest -Uri $download.Url -OutFile $tmpZip
264+
Expand-Archive -Path $tmpZip -DestinationPath $tmpExtract -Force
265+
Move-Item -Path (Join-Path $tmpExtract "*") -Destination $portableRoot -Force
266+
} finally {
267+
if (Test-Path $tmpZip) {
268+
Remove-Item -Force $tmpZip
269+
}
270+
if (Test-Path $tmpExtract) {
271+
Remove-Item -Recurse -Force $tmpExtract
272+
}
273+
}
274+
275+
if (-not (Use-PortableGitIfPresent)) {
276+
throw "Portable Git bootstrap completed, but git is still unavailable."
277+
}
278+
279+
$portableVersion = (& git --version 2>$null)
280+
Write-Host "[OK] User-local Git ready: $portableVersion" -ForegroundColor Green
281+
}
282+
283+
function Ensure-Git {
150284
if (Check-Git) { return }
285+
if (Use-PortableGitIfPresent) { return }
286+
try {
287+
Install-PortableGit
288+
if (Check-Git) {
289+
return
290+
}
291+
} catch {
292+
Write-Host "[!] Portable Git bootstrap failed: $($_.Exception.Message)" -ForegroundColor Yellow
293+
}
294+
151295
Write-Host ""
152296
Write-Host "Error: Git is required to install OpenClaw." -ForegroundColor Red
153-
Write-Host "Install Git for Windows:" -ForegroundColor Yellow
297+
Write-Host "Auto-bootstrap of user-local Git did not succeed." -ForegroundColor Yellow
298+
Write-Host "Install Git for Windows manually, then re-run this installer:" -ForegroundColor Yellow
154299
Write-Host " https://git-scm.com/download/win" -ForegroundColor Cyan
155-
Write-Host "Then re-run this installer." -ForegroundColor Yellow
156300
exit 1
157301
}
158302

@@ -184,6 +328,38 @@ function Invoke-OpenClawCommand {
184328
& $commandPath @Arguments
185329
}
186330

331+
function Resolve-CommandPath {
332+
param(
333+
[Parameter(Mandatory = $true)]
334+
[string[]]$Candidates
335+
)
336+
337+
foreach ($candidate in $Candidates) {
338+
$command = Get-Command $candidate -ErrorAction SilentlyContinue
339+
if ($command -and $command.Source) {
340+
return $command.Source
341+
}
342+
}
343+
344+
return $null
345+
}
346+
347+
function Get-NpmCommandPath {
348+
$path = Resolve-CommandPath -Candidates @("npm.cmd", "npm.exe", "npm")
349+
if (-not $path) {
350+
throw "npm not found on PATH."
351+
}
352+
return $path
353+
}
354+
355+
function Get-CorepackCommandPath {
356+
return (Resolve-CommandPath -Candidates @("corepack.cmd", "corepack.exe", "corepack"))
357+
}
358+
359+
function Get-PnpmCommandPath {
360+
return (Resolve-CommandPath -Candidates @("pnpm.cmd", "pnpm.exe", "pnpm"))
361+
}
362+
187363
function Get-NpmGlobalBinCandidates {
188364
param(
189365
[string]$NpmPrefix
@@ -208,7 +384,7 @@ function Ensure-OpenClawOnPath {
208384

209385
$npmPrefix = $null
210386
try {
211-
$npmPrefix = (npm config get prefix 2>$null).Trim()
387+
$npmPrefix = (& (Get-NpmCommandPath) config get prefix 2>$null).Trim()
212388
} catch {
213389
$npmPrefix = $null
214390
}
@@ -242,14 +418,15 @@ function Ensure-OpenClawOnPath {
242418
}
243419

244420
function Ensure-Pnpm {
245-
if (Get-Command pnpm -ErrorAction SilentlyContinue) {
421+
if (Get-PnpmCommandPath) {
246422
return
247423
}
248-
if (Get-Command corepack -ErrorAction SilentlyContinue) {
424+
$corepackCommand = Get-CorepackCommandPath
425+
if ($corepackCommand) {
249426
try {
250-
corepack enable | Out-Null
251-
corepack prepare pnpm@latest --activate | Out-Null
252-
if (Get-Command pnpm -ErrorAction SilentlyContinue) {
427+
& $corepackCommand enable | Out-Null
428+
& $corepackCommand prepare pnpm@latest --activate | Out-Null
429+
if (Get-PnpmCommandPath) {
253430
Write-Host "[OK] pnpm installed via corepack" -ForegroundColor Green
254431
return
255432
}
@@ -261,7 +438,7 @@ function Ensure-Pnpm {
261438
$prevScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL
262439
$env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe"
263440
try {
264-
npm install -g pnpm
441+
& (Get-NpmCommandPath) install -g pnpm
265442
} finally {
266443
$env:NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell
267444
}
@@ -273,7 +450,7 @@ function Install-OpenClaw {
273450
if ([string]::IsNullOrWhiteSpace($Tag)) {
274451
$Tag = "latest"
275452
}
276-
Require-Git
453+
Ensure-Git
277454

278455
# Use openclaw package for beta, openclaw for stable
279456
$packageName = "openclaw"
@@ -286,13 +463,15 @@ function Install-OpenClaw {
286463
$prevFund = $env:NPM_CONFIG_FUND
287464
$prevAudit = $env:NPM_CONFIG_AUDIT
288465
$prevScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL
466+
$prevNodeLlamaSkipDownload = $env:NODE_LLAMA_CPP_SKIP_DOWNLOAD
289467
$env:NPM_CONFIG_LOGLEVEL = "error"
290468
$env:NPM_CONFIG_UPDATE_NOTIFIER = "false"
291469
$env:NPM_CONFIG_FUND = "false"
292470
$env:NPM_CONFIG_AUDIT = "false"
293471
$env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe"
472+
$env:NODE_LLAMA_CPP_SKIP_DOWNLOAD = "1"
294473
try {
295-
$npmOutput = npm install -g "$packageName@$Tag" 2>&1
474+
$npmOutput = & (Get-NpmCommandPath) install -g "$packageName@$Tag" 2>&1
296475
if ($LASTEXITCODE -ne 0) {
297476
Write-Host "[!] npm install failed" -ForegroundColor Red
298477
if ($npmOutput -match "spawn git" -or $npmOutput -match "ENOENT.*git") {
@@ -312,6 +491,7 @@ function Install-OpenClaw {
312491
$env:NPM_CONFIG_FUND = $prevFund
313492
$env:NPM_CONFIG_AUDIT = $prevAudit
314493
$env:NPM_CONFIG_SCRIPT_SHELL = $prevScriptShell
494+
$env:NODE_LLAMA_CPP_SKIP_DOWNLOAD = $prevNodeLlamaSkipDownload
315495
}
316496
Write-Host "[OK] OpenClaw installed" -ForegroundColor Green
317497
}
@@ -322,7 +502,7 @@ function Install-OpenClawFromGit {
322502
[string]$RepoDir,
323503
[switch]$SkipUpdate
324504
)
325-
Require-Git
505+
Ensure-Git
326506
Ensure-Pnpm
327507

328508
$repoUrl = "https://github.com/openclaw/openclaw.git"
@@ -345,13 +525,17 @@ function Install-OpenClawFromGit {
345525
Remove-LegacySubmodule -RepoDir $RepoDir
346526

347527
$prevPnpmScriptShell = $env:NPM_CONFIG_SCRIPT_SHELL
528+
$pnpmCommand = Get-PnpmCommandPath
529+
if (-not $pnpmCommand) {
530+
throw "pnpm not found after installation."
531+
}
348532
$env:NPM_CONFIG_SCRIPT_SHELL = "cmd.exe"
349533
try {
350-
pnpm -C $RepoDir install
351-
if (-not (pnpm -C $RepoDir ui:build)) {
534+
& $pnpmCommand -C $RepoDir install
535+
if (-not (& $pnpmCommand -C $RepoDir ui:build)) {
352536
Write-Host "[!] UI build failed; continuing (CLI may still work)" -ForegroundColor Yellow
353537
}
354-
pnpm -C $RepoDir build
538+
& $pnpmCommand -C $RepoDir build
355539
} finally {
356540
$env:NPM_CONFIG_SCRIPT_SHELL = $prevPnpmScriptShell
357541
}
@@ -522,7 +706,7 @@ function Main {
522706
}
523707
if (-not $installedVersion) {
524708
try {
525-
$npmList = npm list -g --depth 0 --json 2>$null | ConvertFrom-Json
709+
$npmList = & (Get-NpmCommandPath) list -g --depth 0 --json 2>$null | ConvertFrom-Json
526710
if ($npmList -and $npmList.dependencies -and $npmList.dependencies.openclaw -and $npmList.dependencies.openclaw.version) {
527711
$installedVersion = $npmList.dependencies.openclaw.version
528712
}

0 commit comments

Comments
 (0)