diff --git a/Launcher/LocalBuild.ps1 b/Launcher/LocalBuild.ps1 index b4538be..dbfc8db 100644 --- a/Launcher/LocalBuild.ps1 +++ b/Launcher/LocalBuild.ps1 @@ -103,7 +103,7 @@ function Write-MkwBuildStep([string]$StepId, [string]$Message) { function Reset-LocalDirectory([string]$Path) { $full = [IO.Path]::GetFullPath($Path) $root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\' - $installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\' + $installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).TrimEnd('\') + '\' # The caller-supplied output destinations are legitimate reset targets by # definition, wherever the caller placed them: a fresh install's operation # scratch lives beside the installation directory rather than inside it. @@ -150,8 +150,16 @@ if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) { throw '-BaseOutputDirectory is valid only with -Profile both.' } +$realWorkspace = $Workspace.TrimEnd('\') +$Workspace = Get-MkwBuildSafePath $realWorkspace 'workspace' 'runtime\CMakeLists.txt' +# A selected package inside the install may also name the same Code.pul through the real path. +# Give it the workspace spelling before comparing it with the staged copy. +if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory) -and + $RetroRewindPackageDirectory.StartsWith($realWorkspace + '\', [StringComparison]::OrdinalIgnoreCase)) { + $RetroRewindPackageDirectory = $Workspace + $RetroRewindPackageDirectory.Substring($realWorkspace.Length) +} $translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe' -$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit +$toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe' $cmake = Join-Path $toolchain 'CMake\bin\cmake.exe' $ninja = Join-Path $toolchain 'Ninja\ninja.exe' $toolchainBin = Join-Path $toolchain 'llvm-mingw\bin' diff --git a/Launcher/NativeBuildFlags.ps1 b/Launcher/NativeBuildFlags.ps1 index b288b2e..7d88f4b 100644 --- a/Launcher/NativeBuildFlags.ps1 +++ b/Launcher/NativeBuildFlags.ps1 @@ -40,41 +40,55 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) { ) -join ';') } -function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) { - if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' } - $full = [IO.Path]::GetFullPath($ToolchainRoot) +function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile) { + <# + The Windows native build passes workspace paths through CMake, Ninja response files and + clang, which do not all interpret quotes the same way. Keep those paths plain even when the + user's install directory contains an apostrophe, ampersand or other punctuation. + #> + if ([string]::IsNullOrWhiteSpace($Path)) { throw "A $Kind path is required." } + $full = [IO.Path]::GetFullPath($Path) # A drive root keeps its separator: "C:" is relative to the current directory on that drive. if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') } - if ($full -notmatch '[()&^%!]') { return $full } + if ($full -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full } + + Assert-File (Join-Path $full $MarkerFile) "$Kind marker" $sha = [Security.Cryptography.SHA256]::Create() try { $bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant())) } finally { $sha.Dispose() } - $linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '') + $linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '') $failures = @() foreach ($base in @($env:ProgramData, $env:PUBLIC)) { - if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue } + if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue } $link = Join-Path (Join-Path $base 'WiiCompiled') $linkName try { [IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null - # The name already identifies the target, so an existing junction that still resolves is - # this one; only a broken leftover is replaced. Directory.Delete removes the reparse - # point itself, where Remove-Item -Recurse would delete the toolchain it points at. - if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) { - if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) } + $existing = Get-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue + if ($null -ne $existing) { + # Never trust a directory just because it has the marker: it could point at a + # different installation. Nor may we remove a directory we did not create. + if ($existing.LinkType -ne 'Junction' -or + @($existing.Target).Count -ne 1 -or + -not [string]::Equals([IO.Path]::GetFullPath(@($existing.Target)[0]), + $full, [StringComparison]::OrdinalIgnoreCase)) { + throw "An existing path is not the expected junction: $link" + } + } else { New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null } - Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse" + Assert-File (Join-Path $link $MarkerFile) "$Kind junction marker" + Write-Host "MKWCBUILD: Building the $Kind through $link, because $full contains characters the native build cannot quote reliably" return $link } catch { $failures += "$link ($($_.Exception.Message))" } } - throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " + - 'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') + - '. Install to a path without those characters.') + throw ("The $Kind path $full cannot be passed safely to the native build, and no junction " + + 'to it could be created: ' + ($failures -join '; ') + '. Install to a path of plain ' + + 'letters, digits and spaces, or make a safe junction location available.') } function Get-MkwProjectPins([string]$ProjectFile) {