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
169 changes: 139 additions & 30 deletions frontend/public/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,81 @@ function Add-UserPath($Directory) {
}
Write-Info 'User PATH configured. Open a new terminal to use Wave.'
}
function Get-LegacyStdRoot {
$profileRoot = if (-not [string]::IsNullOrEmpty($env:HOME)) { $env:HOME } else { $env:USERPROFILE }
if ([string]::IsNullOrEmpty($profileRoot)) { throw 'HOME or USERPROFILE is required to locate the compiler standard library.' }
$profilePathRoot = [IO.Path]::GetPathRoot($profileRoot)
if (-not [IO.Path]::IsPathRooted($profileRoot) -or
([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT -and $profilePathRoot.Length -lt 3)) {
throw 'HOME or USERPROFILE must be an absolute directory path.'
}
return [IO.Path]::GetFullPath((Join-Path $profileRoot '.wave/lib/wave/std'))
}
function Resolve-InstallerPath($Path, [int]$Depth = 0) {
if ($Depth -gt 32) { throw 'Too many links while resolving installation paths.' }
if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT -and
($Path.StartsWith('\\?\') -or $Path.StartsWith('\\.\'))) {
throw 'Use ordinary filesystem paths instead of Windows device paths for installation.'
}
$full = [IO.Path]::GetFullPath($Path)
$root = [IO.Path]::GetPathRoot($full)
$resolved = $root
$parts = $full.Substring($root.Length).Split([char[]]'\/', [StringSplitOptions]::RemoveEmptyEntries)
foreach ($part in $parts) {
$candidate = Join-Path $resolved $part
$item = Get-Item -LiteralPath $candidate -Force -ErrorAction SilentlyContinue
if ($null -ne $item -and ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
$targets = @($item.Target)
if ($targets.Count -ne 1 -or [string]::IsNullOrEmpty($targets[0])) {
throw "Cannot safely resolve installation link: $candidate"
}
$target = $targets[0]
if (-not [IO.Path]::IsPathRooted($target)) { $target = Join-Path (Split-Path -Parent $candidate) $target }
$resolved = Resolve-InstallerPath $target ($Depth + 1)
} else {
$resolved = $candidate
}
}
$trimmed = $resolved.TrimEnd([char[]]'\/')
$resolvedRoot = [IO.Path]::GetPathRoot($resolved)
if ($trimmed -eq $resolvedRoot.TrimEnd([char[]]'\/')) { return $resolvedRoot }
return $trimmed
}
function Assert-SeparateInstallationPaths($InstallDirectory, $StdDirectory) {
$compilerPath = Resolve-InstallerPath $InstallDirectory
$stdPath = Resolve-InstallerPath $StdDirectory
$separator = [IO.Path]::DirectorySeparatorChar
$comparison = [StringComparison]::OrdinalIgnoreCase
if ($compilerPath.Equals($stdPath, $comparison) -or
$compilerPath.StartsWith($stdPath.TrimEnd([char[]]'\/') + $separator, $comparison) -or
$stdPath.StartsWith($compilerPath.TrimEnd([char[]]'\/') + $separator, $comparison)) {
throw 'The compiler installation and legacy std directory must not overlap.'
}
}
function Assert-ManagedStdDirectory($Directory) {
$item = Get-Item -LiteralPath $Directory -Force -ErrorAction SilentlyContinue
if ($null -eq $item) { return }
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'The legacy std directory must not be a link or junction.' }
if (-not $item.PSIsContainer -or -not (Test-Path -LiteralPath (Join-Path $Directory 'manifest.json') -PathType Leaf)) {
throw "Refusing to replace a standard library not managed by Wave: $Directory"
}
}
function Invoke-InstalledTool($Executable, [string[]]$Arguments) {
$output = & $Executable @Arguments
if ($LASTEXITCODE -ne 0) { throw "Installed tool failed ($LASTEXITCODE): $Executable $($Arguments -join ' ')" }
return $output
}
function Test-Installation($Directory, $Target, [bool]$InstallVex, $Work) {
$wavec = Join-Path $Directory 'wavec.exe'
& $wavec --version
if ($LASTEXITCODE -ne 0) { throw 'wavec --version failed. Check the Visual C++ runtime.' }
$spec = & $wavec print target-spec --format=json
Invoke-InstalledTool $wavec @('--version') | Write-Host
$spec = Invoke-InstalledTool $wavec @('print', 'target-spec', '--format=json')
$actual = ($spec | ConvertFrom-Json).triple
if ($LASTEXITCODE -ne 0 -or "$actual".Trim() -ne $Target) { throw "Compiler host target differs from $Target." }
if ("$actual".Trim() -ne $Target) { throw "Compiler host target differs from $Target." }
$stdPath = Invoke-InstalledTool $wavec @('print', 'std-path')
$expectedStd = Resolve-InstallerPath (Get-LegacyStdRoot)
if (-not (Resolve-InstallerPath ("$stdPath".Trim())).Equals($expectedStd, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Compiler default std path differs from the installed standard library.'
}
$source = Join-Path $Work 'install-smoke.wave'
[IO.File]::WriteAllText($source, @'
import("std::mem::layout")::{size_of};
Expand All @@ -105,15 +173,11 @@ fun main() -> i32 {
'@)
Push-Location $Work
try {
& $wavec run $source --std-root (Join-Path $Directory 'std')
if ($LASTEXITCODE -ne 0) {
throw 'Bundled std compile/run check failed. Ensure the Windows SDK and MSVC/UCRT libraries are installed and available (for example, in a Visual Studio developer shell).'
}
Invoke-InstalledTool $wavec @('run', $source) | Write-Host
} catch {
throw "Default std compile/run check failed. Ensure the Windows SDK and MSVC/UCRT libraries are installed and available (for example, in a Visual Studio developer shell). $($_.Exception.Message)"
} finally { Pop-Location }
if ($InstallVex) {
& (Join-Path $Directory 'vex.exe') --version
if ($LASTEXITCODE -ne 0) { throw 'vex --version failed.' }
}
if ($InstallVex) { Invoke-InstalledTool (Join-Path $Directory 'vex.exe') @('--version') | Write-Host }
}
function Install-Wave {
if ($Help) {
Expand All @@ -123,6 +187,7 @@ Usage: .\install.ps1 [-Latest] [-WithVex | -WithoutVex] [-NoModifyPath]
Default: install Wave and install Vex when its latest release supports this platform.
-WithVex requires Vex; -WithoutVex installs Wave only.
WAVE_INSTALL_DIR overrides the dedicated installation directory (%LOCALAPPDATA%\Wave\bin).
The matching standard library also installs under HOME (or USERPROFILE) at .wave\lib\wave\std.
Older versions and Nightly: download manually from https://github.com/wavefnd/Wave/releases
'@
return
Expand All @@ -134,16 +199,21 @@ Older versions and Nightly: download manually from https://github.com/wavefnd/Wa
}
if ($WithVex -and $WithoutVex) { throw 'Conflicting Vex options.' }
$target = Get-NativeWindowsTarget
$installDir = if ($env:WAVE_INSTALL_DIR) { [IO.Path]::GetFullPath($env:WAVE_INSTALL_DIR) } else { Join-Path $env:LOCALAPPDATA 'Wave\bin' }
$installDir = if ($env:WAVE_INSTALL_DIR) { [IO.Path]::GetFullPath($env:WAVE_INSTALL_DIR) } else { [IO.Path]::GetFullPath((Join-Path $env:LOCALAPPDATA 'Wave\bin')) }
$stdDir = Get-LegacyStdRoot
if ($installDir -match '[;\r\n]') { throw 'Use an installation directory without semicolons or newlines.' }
if ($installDir.TrimEnd('\') -eq [IO.Path]::GetPathRoot($installDir).TrimEnd('\') -or
$installDir.TrimEnd('\') -ieq $HOME.TrimEnd('\')) { throw 'Use a dedicated installation directory.' }
if (Test-Path -LiteralPath $installDir) {
if ((Get-Item -LiteralPath $installDir).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'The installation directory must not be a link or junction.' }
$installPath = Resolve-InstallerPath $installDir
if ($installDir.TrimEnd([char[]]'\/') -eq [IO.Path]::GetPathRoot($installDir).TrimEnd([char[]]'\/') -or
$installPath.Equals((Resolve-InstallerPath $HOME), [StringComparison]::OrdinalIgnoreCase)) { throw 'Use a dedicated installation directory.' }
Assert-SeparateInstallationPaths $installDir $stdDir
$existingInstall = Get-Item -LiteralPath $installDir -Force -ErrorAction SilentlyContinue
if ($null -ne $existingInstall) {
if ($existingInstall.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'The installation directory must not be a link or junction.' }
if (-not (Test-Path -LiteralPath (Join-Path $installDir 'wavec.exe') -PathType Leaf)) {
throw "Refusing to replace a directory not managed by Wave: $installDir"
}
}
Assert-ManagedStdDirectory $stdDir
$wave = Get-PinnedWaveRelease
$waveName = "wave-$($wave.tag_name)-$target.zip"
$waveAsset = Get-ReleaseAsset $wave $waveName
Expand All @@ -159,16 +229,26 @@ Older versions and Nightly: download manually from https://github.com/wavefnd/Wa
}
Write-Info "Wave $($wave.tag_name) / $target"
Write-Info "Install directory: $installDir"
Write-Info "Compiler default standard library: $stdDir"
$parent = Split-Path -Parent $installDir
New-Item -ItemType Directory -Force -Path $parent | Out-Null
$stdParent = Split-Path -Parent $stdDir
$lockPath = "$installDir.install-lock"
# CreateNew prevents simultaneous installers from moving the same installation.
$lock = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
$stdLockPath = "$stdDir.install-lock"
$lock = $null; $stdLock = $null
$temp = Join-Path $parent ('.wave-install-' + [Guid]::NewGuid().ToString('N'))
$stdTemp = Join-Path $stdParent ('.wave-std-' + [Guid]::NewGuid().ToString('N'))
$stage = Join-Path $temp 'stage'; $backup = Join-Path $temp 'previous'
$activated = $false; $committed = $false; $keepBackup = $false
$stdStage = Join-Path $stdTemp 'stage'; $stdBackup = Join-Path $stdTemp 'previous'
$activated = $false; $stdActivated = $false; $committed = $false
$keepBackup = $false; $keepStdBackup = $false
try {
New-Item -ItemType Directory -Path $temp | Out-Null
New-Item -ItemType Directory -Force -Path $parent, $stdParent | Out-Null
# Different custom compiler directories still share the compiler's per-user std.
$lock = [IO.File]::Open($lockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
$stdLock = [IO.File]::Open($stdLockPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
Assert-SeparateInstallationPaths $installDir $stdDir
Assert-ManagedStdDirectory $stdDir
New-Item -ItemType Directory -Path $temp, $stdTemp | Out-Null
Write-Info '[1/4] Downloading and verifying packages'
$waveZip = Join-Path $temp 'wave.zip'
Save-Download "https://github.com/wavefnd/Wave/releases/download/$($wave.tag_name)/$waveName" $waveZip
Expand All @@ -187,6 +267,8 @@ Older versions and Nightly: download manually from https://github.com/wavefnd/Wa
}
New-Item -ItemType Directory -Path $stage | Out-Null
Get-ChildItem -LiteralPath $package -Force | Copy-Item -Destination $stage -Recurse -Force
# Stage beside the legacy root, so activation and restore stay on its volume.
Copy-Item -LiteralPath (Join-Path $stage 'std') -Destination $stdStage -Recurse
if ($vexAsset) {
$vexRoot = Join-Path $temp 'vex'
Expand-Archive -LiteralPath $vexZip -DestinationPath $vexRoot
Expand All @@ -201,9 +283,12 @@ Older versions and Nightly: download manually from https://github.com/wavefnd/Wa
}
Write-Info '[3/4] Activating installation'
if (Test-Path -LiteralPath $installDir) { Move-Item -LiteralPath $installDir -Destination $backup }
Move-Item -LiteralPath $stage -Destination $installDir
if (Test-Path -LiteralPath $stdDir) { Move-Item -LiteralPath $stdDir -Destination $stdBackup }
$stdActivated = $true
Move-Item -LiteralPath $stdStage -Destination $stdDir
$activated = $true
Write-Info '[4/4] Checking compiler, bundled std and runtime'
Move-Item -LiteralPath $stage -Destination $installDir
Write-Info '[4/4] Checking compiler, default std and runtime'
Test-Installation $installDir $target ($null -ne $vexAsset) $temp
$committed = $true
if (-not $NoModifyPath) {
Expand All @@ -213,20 +298,44 @@ Older versions and Nightly: download manually from https://github.com/wavefnd/Wa
Write-Info "Installed Wave $($wave.tag_name)."
if ($vexAsset) { Write-Info "Installed Vex $($vex.tag_name)." }
} finally {
$restoreErrors = @()
try {
if (-not $committed) {
if ($activated) { Remove-Item -LiteralPath $installDir -Recurse -Force }
if (Test-Path -LiteralPath $backup) {
try { Move-Item -LiteralPath $backup -Destination $installDir }
catch { $keepBackup = $true; throw "Restore failed. Previous installation retained at $backup. $($_.Exception.Message)" }
# Attempt both restores even when one destination is inaccessible.
try {
if ($activated -and (Test-Path -LiteralPath $installDir)) { Remove-Item -LiteralPath $installDir -Recurse -Force }
if (Test-Path -LiteralPath $backup) { Move-Item -LiteralPath $backup -Destination $installDir }
} catch {
$keepBackup = Test-Path -LiteralPath $backup
$restoreErrors += "Previous compiler retained at $backup. $($_.Exception.Message)"
}
try {
if ($stdActivated -and (Test-Path -LiteralPath $stdDir)) { Remove-Item -LiteralPath $stdDir -Recurse -Force }
if (Test-Path -LiteralPath $stdBackup) { Move-Item -LiteralPath $stdBackup -Destination $stdDir }
} catch {
$keepStdBackup = Test-Path -LiteralPath $stdBackup
$restoreErrors += "Previous standard library retained at $stdBackup. $($_.Exception.Message)"
}
}
if (-not $keepBackup -and (Test-Path -LiteralPath $temp)) { Remove-Item -LiteralPath $temp -Recurse -Force }
if (-not $keepStdBackup -and (Test-Path -LiteralPath $stdTemp)) { Remove-Item -LiteralPath $stdTemp -Recurse -Force }
if ($restoreErrors.Count) { throw "Restore failed. $($restoreErrors -join ' ')" }
} finally {
$lock.Dispose()
Remove-Item -LiteralPath $lockPath -Force
# A failed CreateNew never gives this invocation ownership of that lock.
try {
if ($null -ne $stdLock) {
$stdLock.Dispose()
Remove-Item -LiteralPath $stdLockPath -Force
}
} finally {
if ($null -ne $lock) {
$lock.Dispose()
Remove-Item -LiteralPath $lockPath -Force
}
}
}
}

}
# Dot-sourcing exposes helpers without installing anything.
if ($MyInvocation.InvocationName -ne '.') {
Expand Down
Loading
Loading