mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(install): normalize Windows temp short paths (#108050)
* Normalize Windows installer temp paths * fix(install): canonicalize Windows temp root Co-authored-by: luyifan <al3060388206@gmail.com> * fix(install): resolve Windows temp aliases with Get-Item --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
83d17e34dd
commit
f12b4af021
+85
-5
@@ -104,6 +104,84 @@ function Complete-Install {
|
||||
throw "OpenClaw installation failed with exit code $($script:InstallExitCode)."
|
||||
}
|
||||
|
||||
function Resolve-InstallerTempDirectory {
|
||||
param([scriptblock]$LongPathResolver)
|
||||
|
||||
$candidates = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($candidate in @($env:TEMP, $env:TMP, [System.IO.Path]::GetTempPath())) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($candidate)) {
|
||||
$candidates.Add($candidate)
|
||||
}
|
||||
}
|
||||
|
||||
$localAppData = [Environment]::GetFolderPath("LocalApplicationData")
|
||||
if (-not [string]::IsNullOrWhiteSpace($localAppData)) {
|
||||
$candidates.Add((Join-Path $localAppData "Temp"))
|
||||
}
|
||||
|
||||
$userHome = [Environment]::GetFolderPath("UserProfile")
|
||||
if (-not [string]::IsNullOrWhiteSpace($userHome)) {
|
||||
$candidates.Add((Join-Path $userHome "AppData\Local\Temp"))
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
$pathToResolve = $candidate
|
||||
$candidateHasShortAlias = $candidate -match '(?i)~\d'
|
||||
$requiresCanonicalization = (
|
||||
$candidateHasShortAlias -or
|
||||
$candidate.StartsWith('\\?\', [System.StringComparison]::OrdinalIgnoreCase)
|
||||
)
|
||||
if ($pathToResolve.StartsWith('\\?\UNC\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$pathToResolve = "\\" + $pathToResolve.Substring(8)
|
||||
} elseif ($pathToResolve.StartsWith('\\?\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$pathToResolve = $pathToResolve.Substring(4)
|
||||
}
|
||||
try {
|
||||
if ($LongPathResolver) {
|
||||
$resolvedCandidate = & $LongPathResolver $pathToResolve
|
||||
} else {
|
||||
# Windows PowerShell 5.1: FSO Folder.Path echoes 8.3 aliases; Get-Item.FullName expands them.
|
||||
$resolvedCandidate = (Get-Item -LiteralPath $pathToResolve -ErrorAction Stop).FullName
|
||||
}
|
||||
} catch {
|
||||
if ($requiresCanonicalization) {
|
||||
continue
|
||||
}
|
||||
$resolvedCandidate = $pathToResolve
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($resolvedCandidate)) {
|
||||
continue
|
||||
}
|
||||
if ($candidateHasShortAlias -and $resolvedCandidate -match '(?i)~\d') {
|
||||
continue
|
||||
}
|
||||
if ($resolvedCandidate.StartsWith('\\?\UNC\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$resolvedCandidate = "\\" + $resolvedCandidate.Substring(8)
|
||||
} elseif ($resolvedCandidate.StartsWith('\\?\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$resolvedCandidate = $resolvedCandidate.Substring(4)
|
||||
}
|
||||
if (
|
||||
Test-Path -LiteralPath $resolvedCandidate -PathType Container
|
||||
) {
|
||||
return $resolvedCandidate
|
||||
}
|
||||
# Try the next existing root; archive and extraction children do not exist yet.
|
||||
}
|
||||
|
||||
throw "Could not find a usable Windows temporary directory."
|
||||
}
|
||||
|
||||
function Initialize-InstallerTempDirectory {
|
||||
param([scriptblock]$LongPathResolver)
|
||||
|
||||
$tempDirectory = Resolve-InstallerTempDirectory -LongPathResolver $LongPathResolver
|
||||
$script:InstallerTempDirectory = $tempDirectory
|
||||
$env:TEMP = $tempDirectory
|
||||
$env:TMP = $tempDirectory
|
||||
}
|
||||
|
||||
Initialize-InstallerTempDirectory
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " OpenClaw Installer" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
@@ -408,7 +486,7 @@ function Install-PortableNode {
|
||||
$download = Resolve-PortableNodeDownload
|
||||
$portableRoot = Get-PortableNodeRoot
|
||||
$portableParent = Split-Path -Parent $portableRoot
|
||||
$tmpZip = Join-Path $env:TEMP $download.Name
|
||||
$tmpZip = Join-Path $script:InstallerTempDirectory $download.Name
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $portableParent | Out-Null
|
||||
if (Test-Path $portableRoot) {
|
||||
@@ -717,8 +795,9 @@ function Install-PortableGit {
|
||||
$download = Resolve-PortableGitDownload
|
||||
$portableRoot = Get-PortableGitRoot
|
||||
$portableParent = Split-Path -Parent $portableRoot
|
||||
$tmpZip = Join-Path $env:TEMP $download.Name
|
||||
$tmpExtract = Join-Path $env:TEMP ("openclaw-portable-git-" + [guid]::NewGuid().ToString("N"))
|
||||
$tempName = "openclaw-portable-git-" + [guid]::NewGuid().ToString("N")
|
||||
$tmpZip = Join-Path $script:InstallerTempDirectory ($tempName + ".zip")
|
||||
$tmpExtract = Join-Path $script:InstallerTempDirectory $tempName
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $portableParent | Out-Null
|
||||
if (Test-Path $portableRoot) {
|
||||
@@ -734,6 +813,7 @@ function Install-PortableGit {
|
||||
$downloadTimeouts = Get-WebRequestTimeoutParameters -CommandName "Invoke-WebRequest" -LegacyTimeoutSec 600
|
||||
Invoke-WebRequest -Uri $download.Url -OutFile $tmpZip @downloadTimeouts
|
||||
Expand-Archive -Path $tmpZip -DestinationPath $tmpExtract -Force
|
||||
New-Item -ItemType Directory -Force -Path $portableRoot | Out-Null
|
||||
Move-Item -Path (Join-Path $tmpExtract "*") -Destination $portableRoot -Force
|
||||
} finally {
|
||||
if (Test-Path $tmpZip) {
|
||||
@@ -858,8 +938,8 @@ function Get-WindowsCommandSafeDirectory {
|
||||
if (-not [string]::IsNullOrWhiteSpace($userHome) -and (Test-Path $userHome)) {
|
||||
return $userHome
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($env:TEMP) -and (Test-Path $env:TEMP)) {
|
||||
return $env:TEMP
|
||||
if (Test-Path -LiteralPath $script:InstallerTempDirectory -PathType Container) {
|
||||
return $script:InstallerTempDirectory
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -126,6 +126,82 @@ describe("install.ps1 failure handling", () => {
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "canonical-temp-root",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"$originalTemp = $env:TEMP",
|
||||
"$originalTmp = $env:TMP",
|
||||
'$sandbox = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-install-temp-test-" + [guid]::NewGuid().ToString("N"))',
|
||||
'$longTemp = Join-Path $sandbox "Long Temp"',
|
||||
"try {",
|
||||
" New-Item -ItemType Directory -Force -Path $longTemp | Out-Null",
|
||||
" $env:TEMP = $longTemp",
|
||||
" $env:TMP = $longTemp",
|
||||
" $resolved = Resolve-InstallerTempDirectory",
|
||||
" $expected = (Get-Item -LiteralPath $longTemp -ErrorAction Stop).FullName",
|
||||
' if ($resolved -ne $expected) { throw "default=$resolved expected=$expected" }',
|
||||
" $env:TEMP = '\\\\?\\' + $longTemp",
|
||||
" $env:TMP = $env:TEMP",
|
||||
' $resolved = Resolve-InstallerTempDirectory -LongPathResolver { param($candidate) if ($candidate -ne $longTemp) { throw "prefix not stripped: $candidate" }; return (Get-Item -LiteralPath $candidate -ErrorAction Stop).FullName }',
|
||||
' if ($resolved -ne $expected) { throw "extended=$resolved expected=$expected" }',
|
||||
" # Windows PowerShell 5.1 proof: FSO Folder.Path echoes 8.3; Get-Item.FullName expands it.",
|
||||
" $env:TEMP = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp'",
|
||||
" $env:TMP = $longTemp",
|
||||
" $resolved = Resolve-InstallerTempDirectory -LongPathResolver { param($candidate) if ($candidate -match '~') { return $longTemp }; return (Get-Item -LiteralPath $candidate -ErrorAction Stop).FullName }",
|
||||
' if ($resolved -ne $longTemp) { throw "short=$resolved" }',
|
||||
" $env:TEMP = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Missing'",
|
||||
" $resolved = Resolve-InstallerTempDirectory -LongPathResolver { param($candidate) if ($candidate -match '~') { throw 'unresolvable short alias' }; return (Get-Item -LiteralPath $candidate -ErrorAction Stop).FullName }",
|
||||
' if ($resolved -ne $longTemp) { throw "fallback=$resolved" }',
|
||||
" $env:TEMP = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp'",
|
||||
" $resolved = Resolve-InstallerTempDirectory -LongPathResolver { param($candidate) return $candidate }",
|
||||
' if ($resolved -ne $longTemp) { throw "unchanged-short=$resolved" }',
|
||||
" $env:TEMP = 'C:\\Users\\RUNNER~1\\AppData\\Local\\Temp'",
|
||||
" Initialize-InstallerTempDirectory -LongPathResolver { param($candidate) if ($candidate -match '~') { return $longTemp }; return (Get-Item -LiteralPath $candidate -ErrorAction Stop).FullName }",
|
||||
' if ($script:InstallerTempDirectory -ne $longTemp) { throw "canonical=$script:InstallerTempDirectory" }',
|
||||
' if ($env:TEMP -ne $longTemp) { throw "TEMP=$env:TEMP" }',
|
||||
' if ($env:TMP -ne $longTemp) { throw "TMP=$env:TMP" }',
|
||||
"} finally {",
|
||||
" $env:TEMP = $originalTemp",
|
||||
" $env:TMP = $originalTmp",
|
||||
" if (Test-Path -LiteralPath $sandbox) { Remove-Item -LiteralPath $sandbox -Recurse -Force }",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "portable-git-layout",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
'$sandbox = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-portable-git-test-" + [guid]::NewGuid().ToString("N"))',
|
||||
'$portableRoot = Join-Path $sandbox "portable-git"',
|
||||
"try {",
|
||||
" New-Item -ItemType Directory -Force -Path $sandbox | Out-Null",
|
||||
" $script:InstallerTempDirectory = $sandbox",
|
||||
" function Get-PortableGitRoot { return $portableRoot }",
|
||||
" function Resolve-PortableGitDownload { return @{ Tag = 'test'; Name = 'MinGit.zip'; Url = 'https://example.test/MinGit.zip' } }",
|
||||
" function Ensure-PortableGitOnUserPath { }",
|
||||
" function Use-PortableGitIfPresent { return (Test-Path -LiteralPath (Join-Path $portableRoot 'cmd/git.exe')) }",
|
||||
" function Invoke-WebRequest { param($Uri, $OutFile) New-Item -ItemType File -Force -Path $OutFile | Out-Null }",
|
||||
" function Expand-Archive {",
|
||||
" param($Path, $DestinationPath, [switch]$Force)",
|
||||
" New-Item -ItemType Directory -Force -Path (Join-Path $DestinationPath 'cmd') | Out-Null",
|
||||
" New-Item -ItemType Directory -Force -Path (Join-Path $DestinationPath 'etc') | Out-Null",
|
||||
" New-Item -ItemType File -Force -Path (Join-Path $DestinationPath 'cmd/git.exe') | Out-Null",
|
||||
" New-Item -ItemType File -Force -Path (Join-Path $DestinationPath 'etc/gitconfig') | Out-Null",
|
||||
" }",
|
||||
" Install-PortableGit",
|
||||
" if (-not (Test-Path -LiteralPath (Join-Path $portableRoot 'cmd/git.exe'))) { throw 'missing cmd/git.exe' }",
|
||||
" if (-not (Test-Path -LiteralPath (Join-Path $portableRoot 'etc/gitconfig'))) { throw 'missing etc/gitconfig' }",
|
||||
" if (@(Get-ChildItem -LiteralPath $sandbox -Filter 'openclaw-portable-git-*').Count -ne 0) { throw 'temporary Git files remain' }",
|
||||
"} finally {",
|
||||
" if (Test-Path -LiteralPath $sandbox) { Remove-Item -LiteralPath $sandbox -Recurse -Force }",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "sqlite-versions",
|
||||
source: [
|
||||
@@ -515,6 +591,14 @@ describe("install.ps1 failure handling", () => {
|
||||
expectBatchedPowerShellCase("sqlite-versions");
|
||||
});
|
||||
|
||||
runIfPowerShell("normalizes and exports one installer temp root", () => {
|
||||
expectBatchedPowerShellCase("canonical-temp-root");
|
||||
});
|
||||
|
||||
runIfPowerShell("installs portable Git from multiple archive roots without collisions", () => {
|
||||
expectBatchedPowerShellCase("portable-git-layout");
|
||||
});
|
||||
|
||||
runIfPowerShell("upgrades and validates Node installed by Windows package managers", () => {
|
||||
expectBatchedPowerShellCase("winget-node-delayed-path");
|
||||
expectBatchedPowerShellCase("chocolatey-node-upgrade");
|
||||
@@ -597,6 +681,30 @@ describe("install.ps1 failure handling", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("selects one canonical temp root for installer and child process paths", () => {
|
||||
const resolveBody = extractFunctionBody(source, "Resolve-InstallerTempDirectory");
|
||||
const initializeBody = extractFunctionBody(source, "Initialize-InstallerTempDirectory");
|
||||
const portableNodeBody = extractFunctionBody(source, "Install-PortableNode");
|
||||
const portableGitBody = extractFunctionBody(source, "Install-PortableGit");
|
||||
const commandSafeBody = extractFunctionBody(source, "Get-WindowsCommandSafeDirectory");
|
||||
|
||||
expect(resolveBody).toContain("Get-Item -LiteralPath $pathToResolve -ErrorAction Stop");
|
||||
expect(resolveBody).toContain(".FullName");
|
||||
expect(resolveBody).toContain("FSO Folder.Path echoes 8.3 aliases");
|
||||
expect(resolveBody).not.toContain("Scripting.FileSystemObject");
|
||||
expect(resolveBody).toContain("$resolvedCandidate.Substring(8)");
|
||||
expect(resolveBody).toContain("$resolvedCandidate.Substring(4)");
|
||||
expect(resolveBody).toContain("Test-Path -LiteralPath $resolvedCandidate -PathType Container");
|
||||
expect(initializeBody).toContain("$script:InstallerTempDirectory = $tempDirectory");
|
||||
expect(initializeBody).toContain("$env:TEMP = $tempDirectory");
|
||||
expect(initializeBody).toContain("$env:TMP = $tempDirectory");
|
||||
expect(portableNodeBody).toContain("Join-Path $script:InstallerTempDirectory");
|
||||
expect(portableGitBody).toContain("Join-Path $script:InstallerTempDirectory");
|
||||
expect(commandSafeBody).toContain("return $script:InstallerTempDirectory");
|
||||
expect(source.match(/^Initialize-InstallerTempDirectory$/gm)).toHaveLength(1);
|
||||
expect(source).not.toContain("Get-InstallerTempDirectory");
|
||||
});
|
||||
|
||||
it("rejects OpenClaw GitHub source targets for npm installs", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
const sourceTargetBody = extractFunctionBody(source, "Test-OpenClawSourcePackageInstallSpec");
|
||||
@@ -735,6 +843,16 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(portableGitBody).toContain("@downloadTimeouts");
|
||||
expect(portableGitDownloadBody).toContain("'^MinGit-.*-arm64\\.zip$'");
|
||||
expect(portableGitDownloadBody).toContain("'^MinGit-.*-64-bit\\.zip$'");
|
||||
expect(portableGitBody).toContain(
|
||||
'$tempName = "openclaw-portable-git-" + [guid]::NewGuid().ToString("N")',
|
||||
);
|
||||
expect(portableGitBody).toContain(
|
||||
'Join-Path $script:InstallerTempDirectory ($tempName + ".zip")',
|
||||
);
|
||||
expect(portableGitBody).toContain("Join-Path $script:InstallerTempDirectory $tempName");
|
||||
expect(portableGitBody).toContain(
|
||||
"New-Item -ItemType Directory -Force -Path $portableRoot | Out-Null",
|
||||
);
|
||||
});
|
||||
|
||||
runIfPowerShell("selects native ARM64 MinGit when the release publishes it", () => {
|
||||
|
||||
Reference in New Issue
Block a user