From 684664c5a08aeba28cde7c5d3c24e6e799556024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Tue, 19 May 2026 15:12:12 -0700 Subject: [PATCH 1/3] Update build scripts for ARM64 hostfxr handling - Switch build.cmd to use build.cmd.ps1 for improved argument parsing and ARM64 detection. - Add logic to remove mismatched hostfxr.dll versions on ARM64, ensuring correct architecture resolution and self-healing builds. - Pin CMake version to 3.21.0 in global.json for consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 479cf8e8-6235-4ad2-8f8d-7d4bb931f24c --- build.cmd | 2 +- eng/build.cmd.ps1 | 177 ++++++++++++++++++++++++++++++++++++++++++++++ global.json | 2 +- 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 eng/build.cmd.ps1 diff --git a/build.cmd b/build.cmd index 9852a061a26..c25a3fbebe1 100755 --- a/build.cmd +++ b/build.cmd @@ -1,3 +1,3 @@ @echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -NativeToolsOnMachine -restore -build -bl %*" +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0eng\build.cmd.ps1" %* exit /b %ErrorLevel% diff --git a/eng/build.cmd.ps1 b/eng/build.cmd.ps1 new file mode 100644 index 00000000000..52ad8b3760b --- /dev/null +++ b/eng/build.cmd.ps1 @@ -0,0 +1,177 @@ +[CmdletBinding(PositionalBinding = $false)] +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $BuildArgs +) + +# Machine type constants from the PE COFF header (IMAGE_FILE_HEADER.Machine). +$ImageFileMachineArm64 = 0xAA64 + +<# +.SYNOPSIS +Reads the PE Machine field of a native DLL/EXE, identifying the architecture it was built for. +#> +function Get-PEMachineType([string] $filePath) { + $stream = [System.IO.File]::OpenRead($filePath) + try { + $reader = [System.IO.BinaryReader]::new($stream) + $stream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) | Out-Null + $peHeaderOffset = $reader.ReadInt32() + $stream.Seek($peHeaderOffset + 4, [System.IO.SeekOrigin]::Begin) | Out-Null + return $reader.ReadUInt16() + } + finally { + $stream.Dispose() + } +} + +<# +.SYNOPSIS +Works around an Arcade limitation (only x86 secondary runtimes are redirected to an +architecture-specific subfolder; x64 is always installed into the shared .dotnet root - +see eng/common/dotnet-install.ps1). On an ARM64 host, installing the "dotnet/x64" runtime +listed in global.json's tools.runtimes therefore drops an x64 hostfxr.dll next to the +native ARM64 one under .dotnet/host/fxr. The dotnet muxer picks the highest-versioned +hostfxr it finds without regard to architecture, so once the x64 copy is present with a +higher version, every subsequent "dotnet" invocation fails to load hostfxr on this ARM64 +machine. Removing the mismatched-architecture hostfxr folder(s) restores resolution to the +native one; the corresponding shared runtime (still on disk) is left alone so dotnet-install +continues to treat that runtime as already installed and won't recreate the problem. +#> +function Remove-MismatchedHostFxr([string] $dotnetRoot) { + $fxrDir = Join-Path $dotnetRoot 'host\fxr' + if (!(Test-Path $fxrDir)) { + return + } + + $versionDirs = Get-ChildItem -Path $fxrDir -Directory -ErrorAction SilentlyContinue + $nativeDirs = @() + $mismatchedDirs = @() + + foreach ($versionDir in $versionDirs) { + $hostfxrPath = Join-Path $versionDir.FullName 'hostfxr.dll' + if (!(Test-Path $hostfxrPath)) { + continue + } + + if ((Get-PEMachineType $hostfxrPath) -eq $ImageFileMachineArm64) { + $nativeDirs += $versionDir + } + else { + $mismatchedDirs += $versionDir + } + } + + # Only clean up if a native ARM64 hostfxr remains available; never remove the last copy. + if ($nativeDirs.Count -gt 0) { + foreach ($mismatchedDir in $mismatchedDirs) { + Write-Host "Removing mismatched-architecture hostfxr: $($mismatchedDir.FullName)" + Remove-Item -Path $mismatchedDir.FullName -Recurse -Force + } + } +} + +$forwardArgs = [System.Collections.Generic.List[string]]::new() +$useNativeTools = $true +$isArm64Platform = $false +$hasTargetArchitecture = $false +$hasRestore = $false +$hasBuild = $false +$hasBinaryLog = $false + +for ($i = 0; $i -lt $BuildArgs.Length; $i++) { + $arg = $BuildArgs[$i] + + if ($arg.StartsWith('/p:TargetArchitecture=', [StringComparison]::OrdinalIgnoreCase) -or + $arg.StartsWith('-p:TargetArchitecture=', [StringComparison]::OrdinalIgnoreCase)) { + $hasTargetArchitecture = $true + } + + if ($arg.Equals('-restore', [StringComparison]::OrdinalIgnoreCase)) { + $hasRestore = $true + } + + if ($arg.Equals('-build', [StringComparison]::OrdinalIgnoreCase)) { + $hasBuild = $true + } + + if ($arg.Equals('-bl', [StringComparison]::OrdinalIgnoreCase)) { + $hasBinaryLog = $true + } + + if ($arg.Equals('-platform', [StringComparison]::OrdinalIgnoreCase) -and + $i + 1 -lt $BuildArgs.Length -and + $BuildArgs[$i + 1].Equals('arm64', [StringComparison]::OrdinalIgnoreCase)) { + $isArm64Platform = $true + $i++ + continue + } + + if ($arg.Equals('/p:Platform=arm64', [StringComparison]::OrdinalIgnoreCase) -or + $arg.Equals('-p:Platform=arm64', [StringComparison]::OrdinalIgnoreCase)) { + $isArm64Platform = $true + continue + } + + $forwardArgs.Add($arg) +} + +if ($isArm64Platform) { + $useNativeTools = $false + + if (!$hasTargetArchitecture) { + $forwardArgs.Add('/p:TargetArchitecture=arm64') + } +} + +$buildScript = Join-Path $PSScriptRoot 'common\build.ps1' +$baseArgs = @() + +if ($useNativeTools) { + $baseArgs += '-NativeToolsOnMachine' +} + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') +$dotnetRoot = if ($env:DOTNET_GLOBAL_INSTALL_DIR) { $env:DOTNET_GLOBAL_INSTALL_DIR } else { Join-Path $repoRoot '.dotnet' } + +$defaultArgs = @() +if (!$hasRestore) { $defaultArgs += '-restore' } +if (!$hasBuild) { $defaultArgs += '-build' } +if (!$hasBinaryLog) { $defaultArgs += '-bl' } + +$processArgs = @( + '-ExecutionPolicy' + 'ByPass' + '-NoProfile' + '-File' + $buildScript +) + $baseArgs + $defaultArgs + $forwardArgs + +if ($isArm64Platform) { + # Clean up any leftover clobbered hostfxr from a previous run before we start. + Remove-MismatchedHostFxr $dotnetRoot +} + +& powershell @processArgs +$exitCode = $LASTEXITCODE + +if ($isArm64Platform -and $exitCode -ne 0) { + # A restore step may have just installed a non-native "dotnet/x64" runtime from + # global.json, clobbering hostfxr resolution (see Remove-MismatchedHostFxr above). + # Clean up and retry once so a single invocation self-heals. + $fxrDir = Join-Path $dotnetRoot 'host\fxr' + $hadMismatch = (Test-Path $fxrDir) -and + (Get-ChildItem -Path $fxrDir -Directory -ErrorAction SilentlyContinue | Where-Object { + $hostfxrPath = Join-Path $_.FullName 'hostfxr.dll' + (Test-Path $hostfxrPath) -and ((Get-PEMachineType $hostfxrPath) -ne $ImageFileMachineArm64) + }) + + if ($hadMismatch) { + Remove-MismatchedHostFxr $dotnetRoot + Write-Host 'Retrying build after removing mismatched-architecture hostfxr...' + & powershell @processArgs + $exitCode = $LASTEXITCODE + } +} + +exit $exitCode diff --git a/global.json b/global.json index 6bcdd3f22fa..698f5cd4b75 100644 --- a/global.json +++ b/global.json @@ -31,6 +31,6 @@ "Microsoft.NET.Sdk.IL": "12.0.0-alpha.1.26519.114" }, "native-tools": { - "cmake": "latest" + "cmake": "3.21.0" } } From 2c3c719196cb33ea3f8e1452509049601a2f99c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Wed, 7 Oct 2026 23:41:32 -0700 Subject: [PATCH 2/3] Isolate SDK and runtime architectures for Windows builds Separate OS, SDK execution, and target architectures to support native and cross-target builds on ARM64 and x64. Preserve compatible emulated SDKs, repair mixed runtime installations safely, and align restore and IDE launcher roots. Add regression checks to PR CI and document the architecture model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Build.props | 2 + Restore.cmd | 3 +- docs/building.md | 38 +++ eng/Tools.props | 20 ++ eng/build.cmd.ps1 | 233 ++++++------------ eng/configure-toolset.ps1 | 26 ++ eng/dotnet-architecture.ps1 | 191 ++++++++++++++ eng/install-runtimes.ps1 | 52 ++++ eng/pipelines/build-PR.yml | 5 + eng/tests/BuildScript.Tests.ps1 | 93 +++++++ eng/tests/DotNetArchitecture.Tests.ps1 | 194 +++++++++++++++ global.json | 8 +- .../ScratchProject/ScratchProject.csproj | 3 +- start-code.cmd | 11 +- start-vs.cmd | 12 +- 15 files changed, 721 insertions(+), 170 deletions(-) create mode 100644 eng/Tools.props create mode 100644 eng/dotnet-architecture.ps1 create mode 100644 eng/install-runtimes.ps1 create mode 100644 eng/tests/BuildScript.Tests.ps1 create mode 100644 eng/tests/DotNetArchitecture.Tests.ps1 diff --git a/Directory.Build.props b/Directory.Build.props index 6ea9e7f0bd2..8808e13491d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,8 @@ True false + $(Platform) + arm64 win-$(TargetArchitecture) diff --git a/Restore.cmd b/Restore.cmd index f615deaac21..d0bffa68b00 100644 --- a/Restore.cmd +++ b/Restore.cmd @@ -1,2 +1,3 @@ @echo off -powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -NativeToolsOnMachine -restore %*" \ No newline at end of file +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0eng\build.cmd.ps1" -RestoreOnly %* +exit /b %ErrorLevel% \ No newline at end of file diff --git a/docs/building.md b/docs/building.md index 25f26541301..754617222b1 100644 --- a/docs/building.md +++ b/docs/building.md @@ -13,6 +13,44 @@ Follow the prerequisites listed at [Developer Guide](developer-guide.md). Note that this does **not** build using your machine-wide installed version of the dotnet sdk. It builds using the repo-local .NET SDK specified in the global.json in the repository root. +### Windows x64 and ARM64 builds + +Run `.\build.cmd` or `.\Restore.cmd` normally on either machine architecture. A fresh SDK +installation uses the OS architecture, even from an emulated shell. Existing installations +retain the architecture of their actual `dotnet.exe`; an x64 SDK running under emulation on +ARM64 is not replaced or stripped of its x64 runtimes. +If a checkout containing an ARM64 SDK is reused on x64, restore preserves that SDK and +bootstraps an executable x64 SDK in its `x64` subdirectory instead. + +The target is independent of the SDK architecture. Builds default applications and tests to +the OS architecture. Use `.\build.cmd -platform arm64`, `-platform x64`, or `-platform x86` +to select a target explicitly. Source generators and analyzers retain the solution's +Any CPU mappings. Cross-compiling does not make ARM64 executables runnable on an x64 machine. + +Restore installs the SDK architecture's runtime alongside the SDK. Other architectures go +into the active installation's `arm64`, `x64`, or `x86` subdirectories. The runtime cache checks +the architecture and presence of the host, resolver, and runtime, not just a version directory. +Incomplete entries are preserved and reinstalled rather than silently skipped. + +For a separate SDK installation, set `DOTNET_GLOBAL_INSTALL_DIR` before running the build or +restore scripts. `global.json` prefers the SDK muxer's own installation before falling back +to `.dotnet`, so an external x64 SDK does not load ARM64 SDK binaries (or vice versa). +`start-vs.cmd` and `start-code.cmd` also honor this root and expose architecture-specific +runtime roots for launched applications. + +Older mixed-architecture installations are repaired relative to their **SDK muxer**, not the +OS. Incompatible version directories are preserved under the installation's `legacy-runtimes` +directory. Repair checks both `hostfxr.dll` and `coreclr.dll`, and refuses to move any files +if that would leave the muxer without a compatible resolver. + +The changes use repository-owned scripts, `eng\configure-toolset.ps1`, and `eng\Tools.props`; +the synchronized `eng\common` files are unchanged. PR build jobs run these regression checks: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\eng\tests\BuildScript.Tests.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\eng\tests\DotNetArchitecture.Tests.ps1 +``` + ### Building from Visual Studio 1. .NET 6.0 and above branches need VisualStudio 2022 to build. diff --git a/eng/Tools.props b/eng/Tools.props new file mode 100644 index 00000000000..c14d8d15ce0 --- /dev/null +++ b/eng/Tools.props @@ -0,0 +1,20 @@ + + + + + true + <_WinFormsRuntimeRoot>$(DotNetRoot) + <_WinFormsRuntimeRoot Condition="'$(DOTNET_GLOBAL_INSTALL_DIR)' != ''">$(DOTNET_GLOBAL_INSTALL_DIR) + <_WinFormsRuntimeRootArgument>$(_WinFormsRuntimeRoot) + <_WinFormsRuntimeRootArgument Condition="$(_WinFormsRuntimeRoot.EndsWith('\'))">$(_WinFormsRuntimeRoot)\ + <_WinFormsRuntimeSourceOptions Condition="'$(DotNetRuntimeSourceFeed)' != ''">-RuntimeSourceFeed "$(DotNetRuntimeSourceFeed)" + <_WinFormsRuntimeSourceOptions Condition="'$(DotNetRuntimeSourceFeed)' != '' and '$(DotNetRuntimeSourceFeedKey)' != ''">$(_WinFormsRuntimeSourceOptions) -RuntimeSourceFeedKey "$(DotNetRuntimeSourceFeedKey)" + <_WinFormsTargetArchitectureOptions Condition="'$(TargetArchitecture)' != ''">-TargetArchitecture "$(TargetArchitecture)" + + + + + diff --git a/eng/build.cmd.ps1 b/eng/build.cmd.ps1 index 52ad8b3760b..0783027eedb 100644 --- a/eng/build.cmd.ps1 +++ b/eng/build.cmd.ps1 @@ -1,177 +1,86 @@ -[CmdletBinding(PositionalBinding = $false)] -param( - [Parameter(ValueFromRemainingArguments = $true)] - [string[]] $BuildArgs -) +$ErrorActionPreference = 'Stop' +$BuildArgs = @($args) + +try { + . "$PSScriptRoot\dotnet-architecture.ps1" + $hostArchitecture = Get-WindowsBuildArchitecture + + $forwardArgs = [System.Collections.Generic.List[string]]::new() + $targetArchitecture = $null + $hasTargetArchitecture = $false + $hasRestore = $false + $hasBuild = $false + $hasBinaryLog = $false + $restoreOnly = $false + + for ($i = 0; $i -lt $BuildArgs.Length; $i++) { + $arg = $BuildArgs[$i] + if ($arg -eq '-RestoreOnly') { + $restoreOnly = $true + continue + } -# Machine type constants from the PE COFF header (IMAGE_FILE_HEADER.Machine). -$ImageFileMachineArm64 = 0xAA64 + if ($arg -match '^[-/]p:TargetArchitecture=') { $hasTargetArchitecture = $true } + if ($arg -match '^-(restore|r)$') { $hasRestore = $true } + if ($arg -match '^-(build|b)$') { $hasBuild = $true } + if ($arg -match '^-(binaryLog|bl|binaryLogName|bln|excludeCIBinarylog|nobl)$') { $hasBinaryLog = $true } -<# -.SYNOPSIS -Reads the PE Machine field of a native DLL/EXE, identifying the architecture it was built for. -#> -function Get-PEMachineType([string] $filePath) { - $stream = [System.IO.File]::OpenRead($filePath) - try { - $reader = [System.IO.BinaryReader]::new($stream) - $stream.Seek(0x3C, [System.IO.SeekOrigin]::Begin) | Out-Null - $peHeaderOffset = $reader.ReadInt32() - $stream.Seek($peHeaderOffset + 4, [System.IO.SeekOrigin]::Begin) | Out-Null - return $reader.ReadUInt16() - } - finally { - $stream.Dispose() - } -} + if ($arg -eq '-platform' -and $i + 1 -lt $BuildArgs.Length) { + $targetArchitecture = $BuildArgs[$i + 1] + } + elseif ($arg -match '^[-/]p:Platform=(.+)$') { + $targetArchitecture = $Matches[1] + } -<# -.SYNOPSIS -Works around an Arcade limitation (only x86 secondary runtimes are redirected to an -architecture-specific subfolder; x64 is always installed into the shared .dotnet root - -see eng/common/dotnet-install.ps1). On an ARM64 host, installing the "dotnet/x64" runtime -listed in global.json's tools.runtimes therefore drops an x64 hostfxr.dll next to the -native ARM64 one under .dotnet/host/fxr. The dotnet muxer picks the highest-versioned -hostfxr it finds without regard to architecture, so once the x64 copy is present with a -higher version, every subsequent "dotnet" invocation fails to load hostfxr on this ARM64 -machine. Removing the mismatched-architecture hostfxr folder(s) restores resolution to the -native one; the corresponding shared runtime (still on disk) is left alone so dotnet-install -continues to treat that runtime as already installed and won't recreate the problem. -#> -function Remove-MismatchedHostFxr([string] $dotnetRoot) { - $fxrDir = Join-Path $dotnetRoot 'host\fxr' - if (!(Test-Path $fxrDir)) { - return + $forwardArgs.Add($arg) } - $versionDirs = Get-ChildItem -Path $fxrDir -Directory -ErrorAction SilentlyContinue - $nativeDirs = @() - $mismatchedDirs = @() - - foreach ($versionDir in $versionDirs) { - $hostfxrPath = Join-Path $versionDir.FullName 'hostfxr.dll' - if (!(Test-Path $hostfxrPath)) { - continue - } - - if ((Get-PEMachineType $hostfxrPath) -eq $ImageFileMachineArm64) { - $nativeDirs += $versionDir + if (!$hasTargetArchitecture) { + if ($targetArchitecture -match '^(arm64|x64|x86)$') { + $forwardArgs.Add("/p:TargetArchitecture=$($targetArchitecture.ToLowerInvariant())") } else { - $mismatchedDirs += $versionDir + $forwardArgs.Add("/p:TargetArchitecture=$hostArchitecture") } } - # Only clean up if a native ARM64 hostfxr remains available; never remove the last copy. - if ($nativeDirs.Count -gt 0) { - foreach ($mismatchedDir in $mismatchedDirs) { - Write-Host "Removing mismatched-architecture hostfxr: $($mismatchedDir.FullName)" - Remove-Item -Path $mismatchedDir.FullName -Recurse -Force + if (!$hasRestore) { $forwardArgs.Add('-restore') } + if (!$restoreOnly -and !$hasBuild) { $forwardArgs.Add('-build') } + if (!$restoreOnly -and !$hasBinaryLog) { $forwardArgs.Add('-bl') } + + # -File cannot bind Boolean parameters in Windows PowerShell. Encode a command + # with quoted values and literal Boolean arguments instead of losing their types. + $buildScript = "$PSScriptRoot\common\build.ps1".Replace("'", "''") + $command = "`$ProgressPreference = 'SilentlyContinue'; & '$buildScript' -NativeToolsOnMachine" + for ($i = 0; $i -lt $forwardArgs.Count; $i++) { + $arg = $forwardArgs[$i] + if ($arg -match '^-(nodeReuse|warnAsError|msbuildMultiThreaded|mt)(:(.+))?$') { + $parameter = $Matches[1] + $value = if ($Matches[3]) { $Matches[3] } else { + $i++ + if ($i -ge $forwardArgs.Count) { throw "Missing Boolean value for -$parameter." } + $forwardArgs[$i] + } + switch ($value.TrimStart('$').ToLowerInvariant()) { + { $_ -in @('true', '1') } { $command += " -$parameter `$true" } + { $_ -in @('false', '0') } { $command += " -$parameter `$false" } + default { throw "Invalid Boolean value for -${parameter}: $value" } + } + } + elseif ($arg -match '^-[A-Za-z]+$') { + $command += " $arg" + } + else { + $command += " '$($arg.Replace("'", "''"))'" } } -} - -$forwardArgs = [System.Collections.Generic.List[string]]::new() -$useNativeTools = $true -$isArm64Platform = $false -$hasTargetArchitecture = $false -$hasRestore = $false -$hasBuild = $false -$hasBinaryLog = $false - -for ($i = 0; $i -lt $BuildArgs.Length; $i++) { - $arg = $BuildArgs[$i] - - if ($arg.StartsWith('/p:TargetArchitecture=', [StringComparison]::OrdinalIgnoreCase) -or - $arg.StartsWith('-p:TargetArchitecture=', [StringComparison]::OrdinalIgnoreCase)) { - $hasTargetArchitecture = $true - } - - if ($arg.Equals('-restore', [StringComparison]::OrdinalIgnoreCase)) { - $hasRestore = $true - } - - if ($arg.Equals('-build', [StringComparison]::OrdinalIgnoreCase)) { - $hasBuild = $true - } - - if ($arg.Equals('-bl', [StringComparison]::OrdinalIgnoreCase)) { - $hasBinaryLog = $true - } - - if ($arg.Equals('-platform', [StringComparison]::OrdinalIgnoreCase) -and - $i + 1 -lt $BuildArgs.Length -and - $BuildArgs[$i + 1].Equals('arm64', [StringComparison]::OrdinalIgnoreCase)) { - $isArm64Platform = $true - $i++ - continue - } - - if ($arg.Equals('/p:Platform=arm64', [StringComparison]::OrdinalIgnoreCase) -or - $arg.Equals('-p:Platform=arm64', [StringComparison]::OrdinalIgnoreCase)) { - $isArm64Platform = $true - continue - } - - $forwardArgs.Add($arg) -} - -if ($isArm64Platform) { - $useNativeTools = $false - - if (!$hasTargetArchitecture) { - $forwardArgs.Add('/p:TargetArchitecture=arm64') - } -} - -$buildScript = Join-Path $PSScriptRoot 'common\build.ps1' -$baseArgs = @() - -if ($useNativeTools) { - $baseArgs += '-NativeToolsOnMachine' -} - -$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..') -$dotnetRoot = if ($env:DOTNET_GLOBAL_INSTALL_DIR) { $env:DOTNET_GLOBAL_INSTALL_DIR } else { Join-Path $repoRoot '.dotnet' } - -$defaultArgs = @() -if (!$hasRestore) { $defaultArgs += '-restore' } -if (!$hasBuild) { $defaultArgs += '-build' } -if (!$hasBinaryLog) { $defaultArgs += '-bl' } -$processArgs = @( - '-ExecutionPolicy' - 'ByPass' - '-NoProfile' - '-File' - $buildScript -) + $baseArgs + $defaultArgs + $forwardArgs - -if ($isArm64Platform) { - # Clean up any leftover clobbered hostfxr from a previous run before we start. - Remove-MismatchedHostFxr $dotnetRoot + $command += ' 6>&1; exit $LASTEXITCODE' + $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) + & powershell -ExecutionPolicy ByPass -NoProfile -OutputFormat Text -EncodedCommand $encodedCommand + exit $LASTEXITCODE } - -& powershell @processArgs -$exitCode = $LASTEXITCODE - -if ($isArm64Platform -and $exitCode -ne 0) { - # A restore step may have just installed a non-native "dotnet/x64" runtime from - # global.json, clobbering hostfxr resolution (see Remove-MismatchedHostFxr above). - # Clean up and retry once so a single invocation self-heals. - $fxrDir = Join-Path $dotnetRoot 'host\fxr' - $hadMismatch = (Test-Path $fxrDir) -and - (Get-ChildItem -Path $fxrDir -Directory -ErrorAction SilentlyContinue | Where-Object { - $hostfxrPath = Join-Path $_.FullName 'hostfxr.dll' - (Test-Path $hostfxrPath) -and ((Get-PEMachineType $hostfxrPath) -ne $ImageFileMachineArm64) - }) - - if ($hadMismatch) { - Remove-MismatchedHostFxr $dotnetRoot - Write-Host 'Retrying build after removing mismatched-architecture hostfxr...' - & powershell @processArgs - $exitCode = $LASTEXITCODE - } +catch { + Write-Error $_ + exit 1 } - -exit $exitCode diff --git a/eng/configure-toolset.ps1 b/eng/configure-toolset.ps1 index a6048f7e634..65e19d85cdc 100644 --- a/eng/configure-toolset.ps1 +++ b/eng/configure-toolset.ps1 @@ -3,3 +3,29 @@ $script:DoNotDisplayNativeToolsInstallationWarnings = $true # Add CMake to path. $env:PATH = "$PSScriptRoot\..\.tools\bin;$env:PATH" + +if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { + . "$PSScriptRoot\dotnet-architecture.ps1" + $_WinFormsDotNetRoot = if ($env:DOTNET_GLOBAL_INSTALL_DIR) { $env:DOTNET_GLOBAL_INSTALL_DIR } else { Join-Path $RepoRoot '.dotnet' } + $_WinFormsSdkRoot = Get-DotNetSdkDirectory $_WinFormsDotNetRoot + if ($_WinFormsSdkRoot -ne $_WinFormsDotNetRoot) { + Write-Host "Using compatible SDK installation '$_WinFormsSdkRoot'; preserving '$_WinFormsDotNetRoot'." + $env:DOTNET_GLOBAL_INSTALL_DIR = $_WinFormsSdkRoot + $_WinFormsDotNetRoot = $_WinFormsSdkRoot + } + + if (Test-Path -LiteralPath (Join-Path $_WinFormsDotNetRoot "sdk\$($GlobalJson.tools.dotnet)")) { + Repair-DotNetInstallation $_WinFormsDotNetRoot + } + + # Keep an existing SDK's execution architecture, including x64 under emulation. + # For a fresh installation, select the OS architecture explicitly. + function InstallDotNetSdk([string] $dotnetRoot, [string] $version, [string] $architecture = '', [switch] $noPath) { + if (!$architecture) { + $architecture = Get-DotNetInstallationArchitecture $dotnetRoot + } + + InstallDotNet $dotnetRoot $version $architecture '' $false $runtimeSourceFeed $runtimeSourceFeedKey -noPath:$noPath + Repair-DotNetInstallation $dotnetRoot + } +} diff --git a/eng/dotnet-architecture.ps1 b/eng/dotnet-architecture.ps1 new file mode 100644 index 00000000000..ca6d5334fe7 --- /dev/null +++ b/eng/dotnet-architecture.ps1 @@ -0,0 +1,191 @@ +param([string] $ResolveSdkRoot, [switch] $ResolveRuntimeRoots) + +function Get-DotNetExecutableArchitecture([string] $path) { + $stream = [System.IO.File]::OpenRead($path) + try { + $reader = [System.IO.BinaryReader]::new($stream) + if ($reader.ReadUInt16() -ne 0x5A4D) { + throw "Invalid PE file: $path" + } + + $stream.Position = 0x3C + $stream.Position = $reader.ReadInt32() + if ($reader.ReadUInt32() -ne 0x00004550) { + throw "Invalid PE header: $path" + } + + switch ($reader.ReadUInt16()) { + 0xAA64 { return 'arm64' } + 0x8664 { return 'x64' } + 0x014C { return 'x86' } + default { throw "Unsupported .NET executable architecture: $path" } + } + } + finally { + $stream.Dispose() + } +} + +function Get-WindowsBuildArchitecture { + $architecture = $env:PROCESSOR_ARCHITECTURE + if ($env:PROCESSOR_ARCHITEW6432) { + $architecture = $env:PROCESSOR_ARCHITEW6432 + } + + switch ($architecture) { + 'ARM64' { return 'arm64' } + 'AMD64' { return 'x64' } + 'x86' { return 'x86' } + default { throw "Unsupported Windows build architecture: $architecture" } + } +} + +function Get-DotNetInstallationArchitecture([string] $dotnetRoot) { + $muxer = Join-Path $dotnetRoot 'dotnet.exe' + if (Test-Path -LiteralPath $muxer) { + return Get-DotNetExecutableArchitecture $muxer + } + + return Get-WindowsBuildArchitecture +} + +function Get-DotNetSdkDirectory([string] $dotnetRoot) { + $osArchitecture = Get-WindowsBuildArchitecture + $sdkArchitecture = Get-DotNetInstallationArchitecture $dotnetRoot + if ($osArchitecture -eq 'arm64' -or $sdkArchitecture -eq $osArchitecture -or $sdkArchitecture -eq 'x86') { + return $dotnetRoot + } + + # An ARM64 SDK cannot execute on x64. Keep it intact and bootstrap the native + # SDK separately, also covering a checkout reused on a different machine. + $nativeRoot = Join-Path $dotnetRoot $osArchitecture + if ((Get-DotNetInstallationArchitecture $nativeRoot) -ne $osArchitecture) { + throw "The SDK in '$nativeRoot' cannot execute on $osArchitecture. Choose a different DOTNET_GLOBAL_INSTALL_DIR." + } + + return $nativeRoot +} + +function Get-DotNetRuntimeDirectory([string] $dotnetRoot, [string] $sdkArchitecture, [string] $runtimeArchitecture) { + if ($runtimeArchitecture -notin @('x64', 'arm64', 'x86')) { + throw "Unsupported .NET runtime architecture: $runtimeArchitecture" + } + + if ($runtimeArchitecture -eq $sdkArchitecture) { + return $dotnetRoot + } + + return Join-Path $dotnetRoot $runtimeArchitecture +} + +function Move-DotNetRuntimeVersion([string] $dotnetRoot, [string] $component, [System.IO.DirectoryInfo] $versionDir) { + $backupDir = Join-Path $dotnetRoot "legacy-runtimes\$component" + New-Item -ItemType Directory -Path $backupDir -Force | Out-Null + $destination = Join-Path $backupDir "$($versionDir.Name)-$([Guid]::NewGuid().ToString('N'))" + Write-Host "Preserving incompatible or incomplete .NET files from '$($versionDir.FullName)' in '$destination'." + Move-Item -LiteralPath $versionDir.FullName -Destination $destination +} + +function Repair-DotNetInstallation([string] $dotnetRoot) { + $muxer = Join-Path $dotnetRoot 'dotnet.exe' + if (!(Test-Path -LiteralPath $muxer)) { + return + } + + $architecture = Get-DotNetExecutableArchitecture $muxer + $foreignVersions = @() + $compatibleResolvers = 0 + $foreignResolvers = 0 + + foreach ($component in @('host\fxr', 'shared\Microsoft.NETCore.App')) { + $componentDir = Join-Path $dotnetRoot $component + if (!(Test-Path -LiteralPath $componentDir)) { + continue + } + + $binaryName = if ($component -eq 'host\fxr') { 'hostfxr.dll' } else { 'coreclr.dll' } + foreach ($versionDir in Get-ChildItem -LiteralPath $componentDir -Directory) { + $binaryPath = Join-Path $versionDir.FullName $binaryName + if (!(Test-Path -LiteralPath $binaryPath)) { + continue + } + + if ((Get-DotNetExecutableArchitecture $binaryPath) -eq $architecture) { + if ($component -eq 'host\fxr') { $compatibleResolvers++ } + } + else { + if ($component -eq 'host\fxr') { $foreignResolvers++ } + $foreignVersions += @{ Component = $component; Directory = $versionDir } + } + } + } + + # Preflight the whole installation before moving anything, including shared runtimes. + if ($foreignResolvers -gt 0 -and $compatibleResolvers -eq 0) { + throw "No hostfxr compatible with the $architecture SDK in '$dotnetRoot'. No files were moved. Restore into a new DOTNET_GLOBAL_INSTALL_DIR or reinstall this SDK." + } + + foreach ($version in $foreignVersions) { + Move-DotNetRuntimeVersion $dotnetRoot $version.Component $version.Directory + } +} + +function Install-WinFormsDotNetRuntime( + [string] $dotnetRoot, + [string] $sdkArchitecture, + [string] $runtimeArchitecture, + [string] $version, + [string] $runtimeSourceFeed, + [string] $runtimeSourceFeedKey) { + + $installDir = Get-DotNetRuntimeDirectory $dotnetRoot $sdkArchitecture $runtimeArchitecture + $muxer = Join-Path $installDir 'dotnet.exe' + if ((Test-Path -LiteralPath $muxer) -and (Get-DotNetExecutableArchitecture $muxer) -ne $runtimeArchitecture) { + throw "The .NET host in '$installDir' is not $runtimeArchitecture. No files were moved." + } + + $runtimeDir = Join-Path $installDir "shared\Microsoft.NETCore.App\$version" + $coreclr = Join-Path $runtimeDir 'coreclr.dll' + $resolver = Join-Path $installDir "host\fxr\$version\hostfxr.dll" + $complete = (Test-Path -LiteralPath $muxer) -and + (Test-Path -LiteralPath $coreclr) -and + (Test-Path -LiteralPath $resolver) + if ($complete) { + $complete = (Get-DotNetExecutableArchitecture $coreclr) -eq $runtimeArchitecture -and + (Get-DotNetExecutableArchitecture $resolver) -eq $runtimeArchitecture + } + + if (!$complete) { + # Arcade's cache check only tests the directory's existence. Move an incomplete + # entry aside so the shared installer downloads it rather than skipping it. + if (Test-Path -LiteralPath $runtimeDir) { + Move-DotNetRuntimeVersion $installDir 'shared\Microsoft.NETCore.App' (Get-Item -LiteralPath $runtimeDir) + } + + InstallDotNet $installDir $version $runtimeArchitecture 'dotnet' $true $runtimeSourceFeed $runtimeSourceFeedKey -noPath + foreach ($binary in @($muxer, $coreclr, $resolver)) { + if (!(Test-Path -LiteralPath $binary) -or (Get-DotNetExecutableArchitecture $binary) -ne $runtimeArchitecture) { + throw "The $runtimeArchitecture runtime $version installation is incomplete or incompatible: $binary" + } + } + } + else { + Write-Host "Runtime dotnet/$runtimeArchitecture $version already installed in '$installDir'." + } + + Repair-DotNetInstallation $installDir +} + +if ($ResolveSdkRoot) { + $sdkRoot = Get-DotNetSdkDirectory $ResolveSdkRoot + if ($ResolveRuntimeRoots) { + $sdkArchitecture = Get-DotNetInstallationArchitecture $sdkRoot + foreach ($architecture in @('x86', 'x64', 'arm64')) { + $runtimeRoot = Get-DotNetRuntimeDirectory $sdkRoot $sdkArchitecture $architecture + "DOTNET_ROOT_$($architecture.ToUpperInvariant())=$runtimeRoot" + } + } + else { + $sdkRoot + } +} diff --git a/eng/install-runtimes.ps1 b/eng/install-runtimes.ps1 new file mode 100644 index 00000000000..26d96506aff --- /dev/null +++ b/eng/install-runtimes.ps1 @@ -0,0 +1,52 @@ +[CmdletBinding(PositionalBinding = $false)] +param( + [Parameter(Mandatory = $true)] + [string] $SdkRoot, + [Parameter(Mandatory = $true)] + [string] $RuntimeVersion, + [string] $TargetArchitecture, + [string] $RuntimeSourceFeed, + [string] $RuntimeSourceFeedKey +) + +$ErrorActionPreference = 'Stop' + +try { + . "$PSScriptRoot\common\tools.ps1" + . "$PSScriptRoot\dotnet-architecture.ps1" + + $sdkArchitecture = Get-DotNetExecutableArchitecture (Join-Path $SdkRoot 'dotnet.exe') + $requests = @() + foreach ($runtime in $GlobalJson.tools.runtimes.PSObject.Properties) { + $parts = $runtime.Name.Split('/') + if ($parts[0] -ne 'dotnet' -or $parts.Length -gt 2) { + throw "Unsupported WinForms runtime specification: $($runtime.Name)" + } + + $architecture = if ($parts.Length -eq 2) { $parts[1].ToLowerInvariant() } else { $sdkArchitecture } + foreach ($value in $runtime.Value) { + $version = if ($value -eq '$(MicrosoftNETCoreAppRefPackageVersion)') { $RuntimeVersion } else { $value } + if ($version -notmatch '^\d+\.\d+\.\d+([+-][0-9A-Za-z.-]+)?$') { + throw "Unresolved or invalid runtime version: $value" + } + + $requests += @{ Architecture = $architecture; Version = $version } + if ($parts.Length -eq 1 -and $TargetArchitecture) { + $requests += @{ Architecture = $TargetArchitecture.ToLowerInvariant(); Version = $version } + } + } + } + + $installed = @{} + foreach ($request in $requests) { + $key = "$($request.Architecture)/$($request.Version)" + if (!$installed.ContainsKey($key)) { + Install-WinFormsDotNetRuntime $SdkRoot $sdkArchitecture $request.Architecture $request.Version $RuntimeSourceFeed $RuntimeSourceFeedKey + $installed[$key] = $true + } + } +} +catch { + Write-Error $_ + exit 1 +} diff --git a/eng/pipelines/build-PR.yml b/eng/pipelines/build-PR.yml index 76805cc952d..4cb410031f3 100644 --- a/eng/pipelines/build-PR.yml +++ b/eng/pipelines/build-PR.yml @@ -64,6 +64,11 @@ jobs: - script: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen.exe queue pause displayName: Pause NGEN x64 + - powershell: | + & .\eng\tests\BuildScript.Tests.ps1 + & .\eng\tests\DotNetArchitecture.Tests.ps1 + displayName: Verify SDK and runtime architecture isolation + # Build and rename binlog # The /p:Coverage argument is passed here since some build properties change to accommodate running with # coverage. This is part of the workarounds for https://github.com/tonerdo/coverlet/issues/362 and diff --git a/eng/tests/BuildScript.Tests.ps1 b/eng/tests/BuildScript.Tests.ps1 new file mode 100644 index 00000000000..8a9c2e5c0af --- /dev/null +++ b/eng/tests/BuildScript.Tests.ps1 @@ -0,0 +1,93 @@ +$ErrorActionPreference = 'Stop' + +function Assert-True([bool] $condition, [string] $message) { + if (!$condition) { + throw $message + } +} + +function Invoke-TestBuild([string[]] $arguments, [int] $expectedExitCode = 0) { + & "$testRoot\eng\build.cmd.ps1" @arguments + Assert-True ($LASTEXITCODE -eq $expectedExitCode) "Unexpected exit code for: $arguments" + if ($expectedExitCode -eq 0) { + return @(Get-Content -LiteralPath $env:WINFORMS_TEST_ARGUMENTS -Raw | ConvertFrom-Json) + } +} + +$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) "WinForms-build-tests-$([Guid]::NewGuid().ToString('N'))" +$savedEnvironment = @{} +foreach ($name in @('PROCESSOR_ARCHITECTURE', 'PROCESSOR_ARCHITEW6432', 'DOTNET_GLOBAL_INSTALL_DIR', 'WINFORMS_TEST_ARGUMENTS', 'WINFORMS_TEST_EXIT_CODE')) { + $savedEnvironment[$name] = [Environment]::GetEnvironmentVariable($name) +} + +try { + New-Item -ItemType Directory -Path "$testRoot\eng\common" -Force | Out-Null + Copy-Item -LiteralPath "$PSScriptRoot\..\build.cmd.ps1" -Destination "$testRoot\eng\build.cmd.ps1" + Copy-Item -LiteralPath "$PSScriptRoot\..\dotnet-architecture.ps1" -Destination "$testRoot\eng\dotnet-architecture.ps1" + @' +ConvertTo-Json -InputObject @($args) -Compress | Set-Content -LiteralPath $env:WINFORMS_TEST_ARGUMENTS +exit ([int] $env:WINFORMS_TEST_EXIT_CODE) +'@ | Set-Content -LiteralPath "$testRoot\eng\common\build.ps1" + + $env:WINFORMS_TEST_ARGUMENTS = "$testRoot\arguments.json" + $env:WINFORMS_TEST_EXIT_CODE = '0' + $env:DOTNET_GLOBAL_INSTALL_DIR = "$testRoot\SDK with spaces" + $env:PROCESSOR_ARCHITECTURE = 'ARM64' + $env:PROCESSOR_ARCHITEW6432 = $null + + $actual = Invoke-TestBuild @() + Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'ARM64 host did not default to ARM64.' + Assert-True ($actual -contains '-restore' -and $actual -contains '-build' -and $actual -contains '-bl') 'Default build actions missing.' + + $actual = Invoke-TestBuild @('-platform', 'ARM64') + Assert-True ($actual -contains '-platform' -and $actual -contains 'ARM64') 'ARM64 solution platform was discarded.' + Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'ARM64 platform did not set target architecture.' + + foreach ($platform in @('x64', 'x86')) { + $actual = Invoke-TestBuild @('-platform', $platform) + Assert-True ($actual -contains "/p:TargetArchitecture=$platform") "Explicit $platform target was ignored." + } + + $actual = Invoke-TestBuild @('/p:Platform=arm64', '/p:TargetArchitecture=x64') + Assert-True ($actual -contains '/p:Platform=arm64') 'MSBuild platform was discarded.' + Assert-True ($actual -notcontains '/p:TargetArchitecture=arm64') 'Explicit target architecture was overwritten.' + + $actual = Invoke-TestBuild @('-r', '-b', '-nobl', '-projects', 'project with spaces.csproj') + Assert-True ($actual -notcontains '-restore' -and $actual -notcontains '-build' -and $actual -notcontains '-bl') 'Action aliases were duplicated.' + Assert-True ($actual -contains 'project with spaces.csproj') 'Argument containing spaces was split.' + + $actual = Invoke-TestBuild @('-nodeReuse', 'false', '-warnAsError:$true', '-mt', '1', '/p:Example=O''Brien;$notCode') + Assert-True ($actual -contains $false -and $actual -contains $true) 'Boolean arguments lost their types.' + Assert-True ($actual -contains '/p:Example=O''Brien;$notCode') 'Quoted property value was evaluated as code.' + + $actual = Invoke-TestBuild @('-RestoreOnly') + Assert-True ($actual -contains '-restore' -and $actual -notcontains '-build' -and $actual -notcontains '-bl') 'Restore-only requested a build.' + + $env:PROCESSOR_ARCHITECTURE = 'AMD64' + $env:PROCESSOR_ARCHITEW6432 = 'ARM64' + $actual = Invoke-TestBuild @() + Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'Emulated shell did not detect the ARM64 OS.' + + $env:PROCESSOR_ARCHITEW6432 = $null + $actual = Invoke-TestBuild @() + Assert-True ($actual -notcontains '/p:TargetArchitecture=arm64') 'x64 host was treated as ARM64.' + Assert-True ($actual -contains '/p:TargetArchitecture=x64') 'x64 host did not default to x64.' + $actual = Invoke-TestBuild @('-platform', 'arm64') + Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'Cross-build target was ignored.' + + # A failed compile must propagate its exit code without retrying or masking it. + $env:WINFORMS_TEST_EXIT_CODE = '7' + Invoke-TestBuild @() 7 + $env:WINFORMS_TEST_EXIT_CODE = '0' + $null = Invoke-TestBuild @() + + Write-Host 'Build script regression checks passed.' +} +finally { + foreach ($name in $savedEnvironment.Keys) { + [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name]) + } + if (Test-Path -LiteralPath $testRoot) { + Remove-Item -LiteralPath $testRoot -Recurse -Force + } +} diff --git a/eng/tests/DotNetArchitecture.Tests.ps1 b/eng/tests/DotNetArchitecture.Tests.ps1 new file mode 100644 index 00000000000..39d23473f82 --- /dev/null +++ b/eng/tests/DotNetArchitecture.Tests.ps1 @@ -0,0 +1,194 @@ +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot\..\dotnet-architecture.ps1" + +function Assert-True([bool] $condition, [string] $message) { + if (!$condition) { throw $message } +} + +function Assert-Throws([scriptblock] $action, [string] $messagePattern) { + $errorMessage = $null + try { & $action } + catch { $errorMessage = $_.Exception.Message } + Assert-True ($errorMessage -like $messagePattern) "Expected '$messagePattern', received '$errorMessage'." +} + +function Write-TestPE([string] $path, [string] $architecture) { + $machine = switch ($architecture) { + 'arm64' { [UInt16] 0xAA64 } + 'x64' { [UInt16] 0x8664 } + 'x86' { [UInt16] 0x014C } + default { throw "Invalid fixture architecture: $architecture" } + } + New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null + $bytes = [byte[]]::new(128) + $bytes[0] = 0x4D + $bytes[1] = 0x5A + $bytes[0x3C] = 0x40 + $bytes[0x40] = 0x50 + $bytes[0x41] = 0x45 + [BitConverter]::GetBytes($machine).CopyTo($bytes, 0x44) + [IO.File]::WriteAllBytes($path, $bytes) +} + +# The real installer exits early for an existing runtime directory. Require its caller +# to distinguish complete caches from partial or wrong-architecture entries first. +function InstallDotNet($root, $version, $architecture, $runtime, $skipNonVersionedFiles, $runtimeSourceFeed, $runtimeSourceFeedKey, [switch] $noPath) { + $installCalls.Add(@{ Root = $root; Architecture = $architecture; Runtime = $runtime; Version = $version }) + Assert-True (!(Test-Path "$root\shared\Microsoft.NETCore.App\$version")) 'Installer would skip a cached runtime directory.' + Write-TestPE "$root\dotnet.exe" $architecture + Write-TestPE "$root\host\fxr\$version\hostfxr.dll" $architecture + Write-TestPE "$root\shared\Microsoft.NETCore.App\$version\coreclr.dll" $architecture + if (!$runtime) { + New-Item -ItemType Directory -Path "$root\sdk\$version" -Force | Out-Null + } +} + +$testRoot = Join-Path ([IO.Path]::GetTempPath()) "WinForms-architecture-tests-$([Guid]::NewGuid().ToString('N'))" +$savedEnvironment = @{} +foreach ($name in @('PROCESSOR_ARCHITECTURE', 'PROCESSOR_ARCHITEW6432', 'DOTNET_GLOBAL_INSTALL_DIR', 'PATH')) { + $savedEnvironment[$name] = [Environment]::GetEnvironmentVariable($name) +} + +try { + foreach ($os in @('AMD64', 'ARM64')) { + $env:PROCESSOR_ARCHITECTURE = $os + $env:PROCESSOR_ARCHITEW6432 = $null + $expected = if ($os -eq 'ARM64') { 'arm64' } else { 'x64' } + Assert-True ((Get-DotNetInstallationArchitecture "$testRoot\fresh-$os") -eq $expected) 'Fresh SDK did not select the OS architecture.' + + foreach ($sdk in @('x64', 'arm64')) { + $root = "$testRoot\$os-$sdk" + Write-TestPE "$root\dotnet.exe" $sdk + Assert-True ((Get-DotNetInstallationArchitecture $root) -eq $sdk) 'Existing SDK architecture was replaced by the OS architecture.' + $expectedSdkRoot = if ($os -eq 'AMD64' -and $sdk -eq 'arm64') { "$root\x64" } else { $root } + Assert-True ((Get-DotNetSdkDirectory $root) -eq $expectedSdkRoot) 'SDK selection did not preserve a compatible host or isolate an incompatible host.' + Assert-True (Test-Path "$root\dotnet.exe") 'SDK selection removed the old muxer.' + $launcherRoots = @(& "$PSScriptRoot\..\dotnet-architecture.ps1" -ResolveSdkRoot $root -ResolveRuntimeRoots) + $launcherSdk = Get-DotNetInstallationArchitecture $expectedSdkRoot + foreach ($target in @('x64', 'arm64', 'x86')) { + $expectedRoot = if ($sdk -eq $target) { $root } else { "$root\$target" } + Assert-True ((Get-DotNetRuntimeDirectory $root $sdk $target) -eq $expectedRoot) 'Runtime routing mixed SDK and target architectures.' + $expectedLauncherRoot = Get-DotNetRuntimeDirectory $expectedSdkRoot $launcherSdk $target + Assert-True ($launcherRoots -contains "DOTNET_ROOT_$($target.ToUpperInvariant())=$expectedLauncherRoot") 'IDE runtime roots depended on whether restore had already installed the target runtime.' + } + } + } + + $env:PROCESSOR_ARCHITECTURE = 'AMD64' + $env:PROCESSOR_ARCHITEW6432 = 'ARM64' + Assert-True ((Get-WindowsBuildArchitecture) -eq 'arm64') 'An emulated shell hid the ARM64 OS architecture.' + + foreach ($sdk in @('x64', 'arm64')) { + $other = if ($sdk -eq 'x64') { 'arm64' } else { 'x64' } + $root = "$testRoot\repair-$sdk" + Write-TestPE "$root\dotnet.exe" $sdk + Write-TestPE "$root\host\fxr\1.0.0\hostfxr.dll" $sdk + Write-TestPE "$root\shared\Microsoft.NETCore.App\1.0.0\coreclr.dll" $sdk + Write-TestPE "$root\host\fxr\2.0.0\hostfxr.dll" $other + Write-TestPE "$root\shared\Microsoft.NETCore.App\2.0.0\coreclr.dll" $other + Repair-DotNetInstallation $root + Assert-True ((Test-Path "$root\host\fxr\1.0.0\hostfxr.dll") -and (Test-Path "$root\shared\Microsoft.NETCore.App\1.0.0\coreclr.dll")) 'Compatible files were moved.' + Assert-True (!(Test-Path "$root\host\fxr\2.0.0") -and !(Test-Path "$root\shared\Microsoft.NETCore.App\2.0.0")) 'Foreign files remain in the live installation.' + Assert-True (@(Get-ChildItem "$root\legacy-runtimes" -Recurse -File).Count -eq 2) 'Foreign files were not preserved.' + Repair-DotNetInstallation $root + Assert-True (@(Get-ChildItem "$root\legacy-runtimes" -Recurse -File).Count -eq 2) 'Repair is not idempotent.' + + $unsafeRoot = "$testRoot\no-compatible-resolver-$sdk" + Write-TestPE "$unsafeRoot\dotnet.exe" $sdk + Write-TestPE "$unsafeRoot\host\fxr\1.0.0\hostfxr.dll" $other + Write-TestPE "$unsafeRoot\shared\Microsoft.NETCore.App\1.0.0\coreclr.dll" $other + Assert-Throws { Repair-DotNetInstallation $unsafeRoot } 'No hostfxr compatible*' + Assert-True ((Test-Path "$unsafeRoot\host\fxr\1.0.0\hostfxr.dll") -and (Test-Path "$unsafeRoot\shared\Microsoft.NETCore.App\1.0.0\coreclr.dll")) 'Failed preflight moved files.' + + foreach ($target in @('x64', 'arm64', 'x86')) { + $runtimeRoot = "$testRoot\runtime-$sdk-$target" + Write-TestPE "$runtimeRoot\dotnet.exe" $sdk + Write-TestPE "$runtimeRoot\host\fxr\1.0.0\hostfxr.dll" $sdk + $installCalls = [System.Collections.Generic.List[object]]::new() + Install-WinFormsDotNetRuntime $runtimeRoot $sdk $target '2.0.0' '' '' + $expectedRoot = Get-DotNetRuntimeDirectory $runtimeRoot $sdk $target + Assert-True ($installCalls.Count -eq 1 -and $installCalls[0].Root -eq $expectedRoot -and $installCalls[0].Architecture -eq $target) 'Runtime installer received the wrong directory or architecture.' + Assert-True ((Get-DotNetExecutableArchitecture "$runtimeRoot\dotnet.exe") -eq $sdk) 'Cross-target runtime replaced the SDK muxer.' + Install-WinFormsDotNetRuntime $runtimeRoot $sdk $target '2.0.0' '' '' + Assert-True ($installCalls.Count -eq 1) 'Complete runtime cache was not reused.' + + Write-TestPE "$expectedRoot\shared\Microsoft.NETCore.App\3.0.0\coreclr.dll" $other + Install-WinFormsDotNetRuntime $runtimeRoot $sdk $target '3.0.0' '' '' + Assert-True ((Get-DotNetExecutableArchitecture "$expectedRoot\shared\Microsoft.NETCore.App\3.0.0\coreclr.dll") -eq $target) 'Partial or foreign runtime cache was not repaired.' + } + } + + # Exercise the repository's Arcade extension, including native fresh bootstrap and + # preserving an x64 SDK while the OS (and requested target) is ARM64. + $RepoRoot = $testRoot + $GlobalJson = @{ tools = @{ dotnet = '4.0.0' } } + $runtimeSourceFeed = '' + $runtimeSourceFeedKey = '' + foreach ($os in @('AMD64', 'ARM64')) { + $env:PROCESSOR_ARCHITECTURE = $os + $env:PROCESSOR_ARCHITEW6432 = $null + $env:DOTNET_GLOBAL_INSTALL_DIR = "$testRoot\bootstrap-$os" + $installCalls = [System.Collections.Generic.List[object]]::new() + . "$PSScriptRoot\..\configure-toolset.ps1" + InstallDotNetSdk $env:DOTNET_GLOBAL_INSTALL_DIR '4.0.0' + $expected = if ($os -eq 'ARM64') { 'arm64' } else { 'x64' } + Assert-True ($installCalls[0].Architecture -eq $expected) 'Fresh SDK bootstrap used the shell instead of the OS architecture.' + } + + $env:PROCESSOR_ARCHITECTURE = 'ARM64' + $env:DOTNET_GLOBAL_INSTALL_DIR = "$testRoot\emulated-sdk" + Write-TestPE "$env:DOTNET_GLOBAL_INSTALL_DIR\dotnet.exe" 'x64' + Write-TestPE "$env:DOTNET_GLOBAL_INSTALL_DIR\host\fxr\1.0.0\hostfxr.dll" 'x64' + $installCalls = [System.Collections.Generic.List[object]]::new() + . "$PSScriptRoot\..\configure-toolset.ps1" + InstallDotNetSdk $env:DOTNET_GLOBAL_INSTALL_DIR '4.0.0' + Assert-True ($installCalls[0].Architecture -eq 'x64') 'SDK upgrade replaced an emulated x64 installation.' + + $env:PROCESSOR_ARCHITECTURE = 'AMD64' + $originalRoot = "$testRoot\arm64-sdk-on-x64" + $env:DOTNET_GLOBAL_INSTALL_DIR = $originalRoot + Write-TestPE "$originalRoot\dotnet.exe" 'arm64' + Write-TestPE "$originalRoot\host\fxr\1.0.0\hostfxr.dll" 'arm64' + $installCalls = [System.Collections.Generic.List[object]]::new() + . "$PSScriptRoot\..\configure-toolset.ps1" + Assert-True ($env:DOTNET_GLOBAL_INSTALL_DIR -eq "$originalRoot\x64") 'An unexecutable SDK was not isolated before bootstrap.' + InstallDotNetSdk $env:DOTNET_GLOBAL_INSTALL_DIR '4.0.0' + Assert-True ($installCalls[0].Architecture -eq 'x64' -and (Get-DotNetExecutableArchitecture "$originalRoot\dotnet.exe") -eq 'arm64') 'Native bootstrap replaced the preserved ARM64 SDK.' + + $installerRepo = "$testRoot\installer" + New-Item -ItemType Directory -Path "$installerRepo\eng\common" -Force | Out-Null + Copy-Item "$PSScriptRoot\..\install-runtimes.ps1" "$installerRepo\eng\install-runtimes.ps1" + Copy-Item "$PSScriptRoot\..\dotnet-architecture.ps1" "$installerRepo\eng\dotnet-architecture.ps1" + '$GlobalJson = Get-Content "$PSScriptRoot\..\..\global.json" -Raw | ConvertFrom-Json' | + Set-Content "$installerRepo\eng\common\tools.ps1" + '{"tools":{"runtimes":{"dotnet":["$(MicrosoftNETCoreAppRefPackageVersion)"],"dotnet/x86":["$(MicrosoftNETCoreAppRefPackageVersion)"]}}}' | + Set-Content "$installerRepo\global.json" + foreach ($sdk in @('x64', 'arm64')) { + $sdkRoot = "$installerRepo\sdk-$sdk" + Write-TestPE "$sdkRoot\dotnet.exe" $sdk + Write-TestPE "$sdkRoot\host\fxr\1.0.0\hostfxr.dll" $sdk + $installCalls = [System.Collections.Generic.List[object]]::new() + $target = if ($sdk -eq 'x64') { 'arm64' } else { 'x64' } + & "$installerRepo\eng\install-runtimes.ps1" -SdkRoot $sdkRoot -RuntimeVersion '5.0.0' -TargetArchitecture $target + Assert-True ($installCalls.Count -eq 3) 'Runtime manifest did not install SDK, cross-target, and x86 runtimes.' + foreach ($architecture in @($sdk, $target, 'x86')) { + $expectedRoot = Get-DotNetRuntimeDirectory $sdkRoot $sdk $architecture + Assert-True (@($installCalls | Where-Object { $_.Root -eq $expectedRoot -and $_.Architecture -eq $architecture -and $_.Version -eq '5.0.0' }).Count -eq 1) 'Manifest installation used the wrong root, version, or architecture.' + } + & "$installerRepo\eng\install-runtimes.ps1" -SdkRoot $sdkRoot -RuntimeVersion '5.0.0' -TargetArchitecture $sdk + Assert-True ($installCalls.Count -eq 3) 'Duplicate manifest requests bypassed the complete runtime cache.' + } + + Assert-Throws { Get-DotNetRuntimeDirectory $testRoot 'x64' 'invalid' } 'Unsupported .NET runtime architecture*' + [IO.File]::WriteAllBytes("$testRoot\invalid.exe", [byte[]]::new(128)) + Assert-Throws { Get-DotNetExecutableArchitecture "$testRoot\invalid.exe" } 'Invalid PE file*' + Write-Host 'Bidirectional SDK and runtime architecture checks passed.' +} +finally { + foreach ($name in $savedEnvironment.Keys) { + [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name]) + } + if (Test-Path -LiteralPath $testRoot) { + Remove-Item -LiteralPath $testRoot -Recurse -Force + } +} diff --git a/global.json b/global.json index 698f5cd4b75..5bff8e11369 100644 --- a/global.json +++ b/global.json @@ -4,15 +4,15 @@ "allowPrerelease": true, "rollForward": "latestFeature", "paths": [ - ".dotnet", - "$host$" + "$host$", + ".dotnet" ], "errorMessage": "The required .NET SDK wasn't found. Please run ./eng/common/dotnet.cmd/sh to install it." }, "tools": { "dotnet": "12.0.100-alpha.1.26480.102", "runtimes": { - "dotnet/x64": [ + "dotnet": [ "$(MicrosoftNETCoreAppRefPackageVersion)" ], "dotnet/x86": [ @@ -31,6 +31,6 @@ "Microsoft.NET.Sdk.IL": "12.0.0-alpha.1.26519.114" }, "native-tools": { - "cmake": "3.21.0" + "cmake": "latest" } } diff --git a/src/test/integration/ScratchProject/ScratchProject.csproj b/src/test/integration/ScratchProject/ScratchProject.csproj index 7273829aade..56cc7438b77 100644 --- a/src/test/integration/ScratchProject/ScratchProject.csproj +++ b/src/test/integration/ScratchProject/ScratchProject.csproj @@ -19,7 +19,8 @@ false false - win-x64 + win-$(TargetArchitecture) + win-x64 diff --git a/start-code.cmd b/start-code.cmd index 08af10160fb..f7d042aec99 100644 --- a/start-code.cmd +++ b/start-code.cmd @@ -13,7 +13,13 @@ IF ["%vscode%"] == [""] ( :: This tells .NET Core to use the same dotnet.exe that build scripts use SET DOTNET_ROOT=%~dp0.dotnet -SET DOTNET_ROOT(x86)=%~dp0.dotnet\x86 +IF DEFINED DOTNET_GLOBAL_INSTALL_DIR SET "DOTNET_ROOT=%DOTNET_GLOBAL_INSTALL_DIR%" +SET "__DotNetSdkRoot=" +FOR /f "delims=" %%r IN ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%"') DO SET "__DotNetSdkRoot=%%r" +IF NOT DEFINED __DotNetSdkRoot EXIT /b 1 +SET "DOTNET_ROOT=%__DotNetSdkRoot%" +SET "DOTNET_GLOBAL_INSTALL_DIR=%DOTNET_ROOT%" +SET "__DotNetSdkRoot=" :: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use SET PATH=%DOTNET_ROOT%;%PATH% @@ -23,4 +29,7 @@ IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" ( exit /b 1 ) +FOR /f "delims=" %%r IN ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%" -ResolveRuntimeRoots') DO SET "%%r" +SET "DOTNET_ROOT(x86)=%DOTNET_ROOT_X86%" + "%vscode%" "." \ No newline at end of file diff --git a/start-vs.cmd b/start-vs.cmd index 0c92b4692fc..07d4eb7fea3 100644 --- a/start-vs.cmd +++ b/start-vs.cmd @@ -5,18 +5,28 @@ setlocal enabledelayedexpansion :: This tells .NET Core to use the same dotnet.exe that build scripts use set DOTNET_ROOT=%~dp0.dotnet -set DOTNET_ROOT(x86)=%~dp0.dotnet\x86 +if defined DOTNET_GLOBAL_INSTALL_DIR set "DOTNET_ROOT=%DOTNET_GLOBAL_INSTALL_DIR%" +set "__DotNetSdkRoot=" +for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%"') do set "__DotNetSdkRoot=%%r" +if not defined __DotNetSdkRoot exit /b 1 +set "DOTNET_ROOT=%__DotNetSdkRoot%" +set "DOTNET_GLOBAL_INSTALL_DIR=%DOTNET_ROOT%" +set "__DotNetSdkRoot=" :: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use set PATH=%DOTNET_ROOT%;%PATH% call restore.cmd +if errorlevel 1 exit /b %ErrorLevel% if not exist "%DOTNET_ROOT%\dotnet.exe" ( echo [ERROR] .NET Core has not yet been installed. Run `%~dp0restore.cmd` to install tools exit /b 1 ) +for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%" -ResolveRuntimeRoots') do set "%%r" +set "DOTNET_ROOT(x86)=%DOTNET_ROOT_X86%" + :: Prefer the VS in the developer command prompt if we're in one, followed by whatever shows up in the current search path. set "DEVENV=%DevEnvDir%devenv.exe" From 2362ebf5f5f54407fdb207f7256cf5b300d966fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Mon, 26 Oct 2026 18:00:41 -0700 Subject: [PATCH 3/3] Fix architecture routing and runtime recovery in build tooling Address ARM64 build review findings with architecture-specific apphost roots, reliable argument forwarding, runtime payload validation, and safe resolver repair. Add regression coverage for fresh installs and incomplete caches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60ae4767-5c06-4b45-ab0f-996e7d30e05a --- Directory.Build.props | 2 +- docs/building.md | 17 +++- eng/build.cmd.ps1 | 85 +++++++++++++---- eng/configure-toolset.ps1 | 6 ++ eng/dotnet-architecture.ps1 | 65 ++++++++----- eng/tests/BuildScript.Tests.ps1 | 82 +++++++++++----- eng/tests/DotNetArchitecture.Tests.ps1 | 124 +++++++++++++++++++++++-- start-code.cmd | 4 +- start-vs.cmd | 4 +- 9 files changed, 304 insertions(+), 85 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8808e13491d..5babeeda2a4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ True false - $(Platform) + $(Platform.ToLowerInvariant()) arm64 win-$(TargetArchitecture) diff --git a/docs/building.md b/docs/building.md index 754617222b1..1c38b2c7a19 100644 --- a/docs/building.md +++ b/docs/building.md @@ -29,18 +29,27 @@ Any CPU mappings. Cross-compiling does not make ARM64 executables runnable on an Restore installs the SDK architecture's runtime alongside the SDK. Other architectures go into the active installation's `arm64`, `x64`, or `x86` subdirectories. The runtime cache checks -the architecture and presence of the host, resolver, and runtime, not just a version directory. +the architecture of `dotnet.exe`, `hostfxr.dll`, `hostpolicy.dll`, and `coreclr.dll`, and the +presence of `System.Private.CoreLib.dll`, not just a version directory. Incomplete entries are preserved and reinstalled rather than silently skipped. +This separation matters even for a fresh checkout. Previously, the runtime manifest explicitly +requested x64, and the shared installer only isolated x86. An ARM64 SDK could therefore receive +a newer x64 `hostfxr.dll` in its own installation. The ARM64 host cannot load that resolver. +The default runtime request now follows the SDK architecture; explicit cross-target runtimes +are installed separately. + For a separate SDK installation, set `DOTNET_GLOBAL_INSTALL_DIR` before running the build or restore scripts. `global.json` prefers the SDK muxer's own installation before falling back to `.dotnet`, so an external x64 SDK does not load ARM64 SDK binaries (or vice versa). -`start-vs.cmd` and `start-code.cmd` also honor this root and expose architecture-specific -runtime roots for launched applications. +Command-line builds, `start-vs.cmd`, and `start-code.cmd` honor this root and expose +architecture-specific runtime roots for apphost executables, including tests and their child +processes. These roots select the runtime independently of the SDK used to build the executable. Older mixed-architecture installations are repaired relative to their **SDK muxer**, not the OS. Incompatible version directories are preserved under the installation's `legacy-runtimes` -directory. Repair checks both `hostfxr.dll` and `coreclr.dll`, and refuses to move any files +directory. Repair checks both `hostfxr.dll` and `coreclr.dll`, including version directories +missing those files, and refuses to move any files if that would leave the muxer without a compatible resolver. The changes use repository-owned scripts, `eng\configure-toolset.ps1`, and `eng\Tools.props`; diff --git a/eng/build.cmd.ps1 b/eng/build.cmd.ps1 index 0783027eedb..9d8a594ecf7 100644 --- a/eng/build.cmd.ps1 +++ b/eng/build.cmd.ps1 @@ -4,9 +4,32 @@ $BuildArgs = @($args) try { . "$PSScriptRoot\dotnet-architecture.ps1" $hostArchitecture = Get-WindowsBuildArchitecture + $buildScriptPath = "$PSScriptRoot\common\build.ps1" + $buildParameters = (Get-Command $buildScriptPath).Parameters.Values + + function Get-ArcadeParameter([string] $argument) { + if ($argument -notmatch '^-(?[A-Za-z]+)(:.*)?$') { + return + } + + $name = $Matches.Name + $parameters = @($buildParameters | Where-Object { $_.Name -eq $name -or $_.Aliases -contains $name }) + if ($parameters.Count -eq 0) { + $parameters = @($buildParameters | Where-Object { + $names = @($_.Name) + @($_.Aliases) + @($names | Where-Object { $_.StartsWith($name, [StringComparison]::OrdinalIgnoreCase) }).Count -gt 0 + }) + } + + if ($parameters.Count -eq 1) { + return $parameters[0] + } + } $forwardArgs = [System.Collections.Generic.List[string]]::new() - $targetArchitecture = $null + $platform = $null + $propertyPlatform = $null + $hasPropertyPlatform = $false $hasTargetArchitecture = $false $hasRestore = $false $hasBuild = $false @@ -20,22 +43,35 @@ try { continue } - if ($arg -match '^[-/]p:TargetArchitecture=') { $hasTargetArchitecture = $true } - if ($arg -match '^-(restore|r)$') { $hasRestore = $true } - if ($arg -match '^-(build|b)$') { $hasBuild = $true } - if ($arg -match '^-(binaryLog|bl|binaryLogName|bln|excludeCIBinarylog|nobl)$') { $hasBinaryLog = $true } - - if ($arg -eq '-platform' -and $i + 1 -lt $BuildArgs.Length) { - $targetArchitecture = $BuildArgs[$i + 1] + if ($arg -match '^[-/](p|property):(.+)$') { + $propertyList = $Matches[2] + foreach ($property in [regex]::Matches($propertyList, '(?:^|;)\s*(?[^=;]+)=(?(?:"[^"]*"|[^;"])*)')) { + switch ($property.Groups['Name'].Value.Trim()) { + 'TargetArchitecture' { $hasTargetArchitecture = $true } + 'Platform' { + $propertyPlatform = $property.Groups['Value'].Value.Trim().Trim('"') + $hasPropertyPlatform = $true + } + } + } } - elseif ($arg -match '^[-/]p:Platform=(.+)$') { - $targetArchitecture = $Matches[1] + + $parameter = Get-ArcadeParameter $arg + if ($parameter.Name -eq 'restore') { $hasRestore = $true } + if ($parameter.Name -eq 'build') { $hasBuild = $true } + if ($parameter.Name -in @('binaryLog', 'binaryLogName', 'excludeCIBinarylog')) { $hasBinaryLog = $true } + if ($parameter.Name -eq 'platform') { + $parts = $arg.Split(':', 2) + if ($parts.Length -eq 2) { $platform = $parts[1] } + elseif ($i + 1 -lt $BuildArgs.Length) { $platform = $BuildArgs[$i + 1] } } $forwardArgs.Add($arg) } if (!$hasTargetArchitecture) { + # Arcade emits -platform before its remaining MSBuild properties. + $targetArchitecture = if ($hasPropertyPlatform) { $propertyPlatform } else { $platform } if ($targetArchitecture -match '^(arm64|x64|x86)$') { $forwardArgs.Add("/p:TargetArchitecture=$($targetArchitecture.ToLowerInvariant())") } @@ -50,21 +86,30 @@ try { # -File cannot bind Boolean parameters in Windows PowerShell. Encode a command # with quoted values and literal Boolean arguments instead of losing their types. - $buildScript = "$PSScriptRoot\common\build.ps1".Replace("'", "''") - $command = "`$ProgressPreference = 'SilentlyContinue'; & '$buildScript' -NativeToolsOnMachine" + $buildScript = $buildScriptPath.Replace("'", "''") + $command = "`$ErrorActionPreference = 'Stop'; `$ProgressPreference = 'SilentlyContinue'; & '$buildScript' -NativeToolsOnMachine" for ($i = 0; $i -lt $forwardArgs.Count; $i++) { $arg = $forwardArgs[$i] - if ($arg -match '^-(nodeReuse|warnAsError|msbuildMultiThreaded|mt)(:(.+))?$') { - $parameter = $Matches[1] - $value = if ($Matches[3]) { $Matches[3] } else { + $parameter = Get-ArcadeParameter $arg + $parts = $arg.Split(':', 2) + if ($parameter.ParameterType -eq [bool] -or + ($parameter.ParameterType -eq [switch] -and $parts.Length -eq 2)) { + $value = if ($parts.Length -eq 2) { $parts[1] } else { $i++ - if ($i -ge $forwardArgs.Count) { throw "Missing Boolean value for -$parameter." } + if ($i -ge $forwardArgs.Count) { throw "Missing Boolean value for -$($parameter.Name)." } $forwardArgs[$i] } + $separator = if ($parameter.ParameterType -eq [switch]) { ':' } else { ' ' } switch ($value.TrimStart('$').ToLowerInvariant()) { - { $_ -in @('true', '1') } { $command += " -$parameter `$true" } - { $_ -in @('false', '0') } { $command += " -$parameter `$false" } - default { throw "Invalid Boolean value for -${parameter}: $value" } + { $_ -in @('true', '1') } { $command += " -$($parameter.Name)$separator`$true" } + { $_ -in @('false', '0') } { $command += " -$($parameter.Name)$separator`$false" } + default { throw "Invalid Boolean value for -$($parameter.Name): $value" } + } + } + elseif ($parameter) { + $command += " -$($parameter.Name)" + if ($parts.Length -eq 2) { + $command += " '$($parts[1].Replace("'", "''"))'" } } elseif ($arg -match '^-[A-Za-z]+$') { @@ -81,6 +126,6 @@ try { exit $LASTEXITCODE } catch { - Write-Error $_ + Write-Error $_ -ErrorAction Continue exit 1 } diff --git a/eng/configure-toolset.ps1 b/eng/configure-toolset.ps1 index 65e19d85cdc..a9fc68f15ce 100644 --- a/eng/configure-toolset.ps1 +++ b/eng/configure-toolset.ps1 @@ -14,6 +14,12 @@ if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { $_WinFormsDotNetRoot = $_WinFormsSdkRoot } + $_WinFormsSdkArchitecture = Get-DotNetInstallationArchitecture $_WinFormsDotNetRoot + foreach ($_WinFormsArchitecture in @('x86', 'x64', 'arm64')) { + $_WinFormsRuntimeRoot = Get-DotNetRuntimeDirectory $_WinFormsDotNetRoot $_WinFormsSdkArchitecture $_WinFormsArchitecture + [Environment]::SetEnvironmentVariable("DOTNET_ROOT_$($_WinFormsArchitecture.ToUpperInvariant())", $_WinFormsRuntimeRoot) + } + if (Test-Path -LiteralPath (Join-Path $_WinFormsDotNetRoot "sdk\$($GlobalJson.tools.dotnet)")) { Repair-DotNetInstallation $_WinFormsDotNetRoot } diff --git a/eng/dotnet-architecture.ps1 b/eng/dotnet-architecture.ps1 index ca6d5334fe7..63f6089ac77 100644 --- a/eng/dotnet-architecture.ps1 +++ b/eng/dotnet-architecture.ps1 @@ -50,6 +50,7 @@ function Get-DotNetInstallationArchitecture([string] $dotnetRoot) { } function Get-DotNetSdkDirectory([string] $dotnetRoot) { + $dotnetRoot = [IO.Path]::GetFullPath($dotnetRoot) $osArchitecture = Get-WindowsBuildArchitecture $sdkArchitecture = Get-DotNetInstallationArchitecture $dotnetRoot if ($osArchitecture -eq 'arm64' -or $sdkArchitecture -eq $osArchitecture -or $sdkArchitecture -eq 'x86') { @@ -93,9 +94,9 @@ function Repair-DotNetInstallation([string] $dotnetRoot) { } $architecture = Get-DotNetExecutableArchitecture $muxer - $foreignVersions = @() + $incompatibleVersions = @() $compatibleResolvers = 0 - $foreignResolvers = 0 + $incompatibleResolvers = 0 foreach ($component in @('host\fxr', 'shared\Microsoft.NETCore.App')) { $componentDir = Join-Path $dotnetRoot $component @@ -106,30 +107,46 @@ function Repair-DotNetInstallation([string] $dotnetRoot) { $binaryName = if ($component -eq 'host\fxr') { 'hostfxr.dll' } else { 'coreclr.dll' } foreach ($versionDir in Get-ChildItem -LiteralPath $componentDir -Directory) { $binaryPath = Join-Path $versionDir.FullName $binaryName - if (!(Test-Path -LiteralPath $binaryPath)) { - continue - } - - if ((Get-DotNetExecutableArchitecture $binaryPath) -eq $architecture) { + if ((Test-Path -LiteralPath $binaryPath -PathType Leaf) -and + (Get-DotNetExecutableArchitecture $binaryPath) -eq $architecture) { if ($component -eq 'host\fxr') { $compatibleResolvers++ } } else { - if ($component -eq 'host\fxr') { $foreignResolvers++ } - $foreignVersions += @{ Component = $component; Directory = $versionDir } + if ($component -eq 'host\fxr') { $incompatibleResolvers++ } + $incompatibleVersions += @{ Component = $component; Directory = $versionDir } } } } # Preflight the whole installation before moving anything, including shared runtimes. - if ($foreignResolvers -gt 0 -and $compatibleResolvers -eq 0) { + if ($incompatibleResolvers -gt 0 -and $compatibleResolvers -eq 0) { throw "No hostfxr compatible with the $architecture SDK in '$dotnetRoot'. No files were moved. Restore into a new DOTNET_GLOBAL_INSTALL_DIR or reinstall this SDK." } - foreach ($version in $foreignVersions) { + foreach ($version in $incompatibleVersions) { Move-DotNetRuntimeVersion $dotnetRoot $version.Component $version.Directory } } +function Get-DotNetRuntimeInstallationProblem([string] $dotnetRoot, [string] $version, [string] $architecture) { + $runtimeDir = Join-Path $dotnetRoot "shared\Microsoft.NETCore.App\$version" + foreach ($binary in @( + (Join-Path $dotnetRoot 'dotnet.exe'), + (Join-Path $dotnetRoot "host\fxr\$version\hostfxr.dll"), + (Join-Path $runtimeDir 'coreclr.dll'), + (Join-Path $runtimeDir 'hostpolicy.dll'))) { + if (!(Test-Path -LiteralPath $binary -PathType Leaf) -or + (Get-DotNetExecutableArchitecture $binary) -ne $architecture) { + return $binary + } + } + + $coreLib = Join-Path $runtimeDir 'System.Private.CoreLib.dll' + if (!(Test-Path -LiteralPath $coreLib -PathType Leaf)) { + return $coreLib + } +} + function Install-WinFormsDotNetRuntime( [string] $dotnetRoot, [string] $sdkArchitecture, @@ -145,28 +162,26 @@ function Install-WinFormsDotNetRuntime( } $runtimeDir = Join-Path $installDir "shared\Microsoft.NETCore.App\$version" - $coreclr = Join-Path $runtimeDir 'coreclr.dll' - $resolver = Join-Path $installDir "host\fxr\$version\hostfxr.dll" - $complete = (Test-Path -LiteralPath $muxer) -and - (Test-Path -LiteralPath $coreclr) -and - (Test-Path -LiteralPath $resolver) - if ($complete) { - $complete = (Get-DotNetExecutableArchitecture $coreclr) -eq $runtimeArchitecture -and + $resolverDir = Join-Path $installDir "host\fxr\$version" + $resolver = Join-Path $resolverDir 'hostfxr.dll' + if (Get-DotNetRuntimeInstallationProblem $installDir $version $runtimeArchitecture) { + $compatibleResolver = (Test-Path -LiteralPath $resolver -PathType Leaf) -and (Get-DotNetExecutableArchitecture $resolver) -eq $runtimeArchitecture - } - - if (!$complete) { # Arcade's cache check only tests the directory's existence. Move an incomplete # entry aside so the shared installer downloads it rather than skipping it. if (Test-Path -LiteralPath $runtimeDir) { Move-DotNetRuntimeVersion $installDir 'shared\Microsoft.NETCore.App' (Get-Item -LiteralPath $runtimeDir) } + # The extractor also skips existing versioned directories, even if empty. + if (!$compatibleResolver -and (Test-Path -LiteralPath $resolverDir)) { + Move-DotNetRuntimeVersion $installDir 'host\fxr' (Get-Item -LiteralPath $resolverDir) + } + InstallDotNet $installDir $version $runtimeArchitecture 'dotnet' $true $runtimeSourceFeed $runtimeSourceFeedKey -noPath - foreach ($binary in @($muxer, $coreclr, $resolver)) { - if (!(Test-Path -LiteralPath $binary) -or (Get-DotNetExecutableArchitecture $binary) -ne $runtimeArchitecture) { - throw "The $runtimeArchitecture runtime $version installation is incomplete or incompatible: $binary" - } + $problem = Get-DotNetRuntimeInstallationProblem $installDir $version $runtimeArchitecture + if ($problem) { + throw "The $runtimeArchitecture runtime $version installation is incomplete or incompatible: $problem. Previous files were preserved under '$installDir\legacy-runtimes'." } } else { diff --git a/eng/tests/BuildScript.Tests.ps1 b/eng/tests/BuildScript.Tests.ps1 index 8a9c2e5c0af..9aeffcfe1a7 100644 --- a/eng/tests/BuildScript.Tests.ps1 +++ b/eng/tests/BuildScript.Tests.ps1 @@ -7,10 +7,11 @@ function Assert-True([bool] $condition, [string] $message) { } function Invoke-TestBuild([string[]] $arguments, [int] $expectedExitCode = 0) { - & "$testRoot\eng\build.cmd.ps1" @arguments - Assert-True ($LASTEXITCODE -eq $expectedExitCode) "Unexpected exit code for: $arguments" + $ErrorActionPreference = 'Continue' + $output = @(& "$testRoot\eng\build.cmd.ps1" @arguments 2>&1) + Assert-True ($LASTEXITCODE -eq $expectedExitCode) "Unexpected exit code $LASTEXITCODE for: $arguments`n$output" if ($expectedExitCode -eq 0) { - return @(Get-Content -LiteralPath $env:WINFORMS_TEST_ARGUMENTS -Raw | ConvertFrom-Json) + return Get-Content -LiteralPath $env:WINFORMS_TEST_ARGUMENTS -Raw | ConvertFrom-Json } } @@ -24,10 +25,21 @@ try { New-Item -ItemType Directory -Path "$testRoot\eng\common" -Force | Out-Null Copy-Item -LiteralPath "$PSScriptRoot\..\build.cmd.ps1" -Destination "$testRoot\eng\build.cmd.ps1" Copy-Item -LiteralPath "$PSScriptRoot\..\dotnet-architecture.ps1" -Destination "$testRoot\eng\dotnet-architecture.ps1" - @' -ConvertTo-Json -InputObject @($args) -Compress | Set-Content -LiteralPath $env:WINFORMS_TEST_ARGUMENTS + $tokens = $null + $parseErrors = $null + $arcadeScript = [System.Management.Automation.Language.Parser]::ParseFile( + "$PSScriptRoot\..\common\build.ps1", [ref] $tokens, [ref] $parseErrors) + Assert-True ($parseErrors.Count -eq 0) 'Could not read Arcade build parameters.' + $fixture = '[CmdletBinding(PositionalBinding=$false)]' + "`n" + $arcadeScript.ParamBlock.Extent.Text + @' + +$bound = @{} +foreach ($parameter in $PSBoundParameters.GetEnumerator()) { + $bound[$parameter.Key] = if ($parameter.Value -is [switch]) { [bool] $parameter.Value } else { $parameter.Value } +} +ConvertTo-Json -InputObject $bound -Compress | Set-Content -LiteralPath $env:WINFORMS_TEST_ARGUMENTS exit ([int] $env:WINFORMS_TEST_EXIT_CODE) -'@ | Set-Content -LiteralPath "$testRoot\eng\common\build.ps1" +'@ + $fixture | Set-Content -LiteralPath "$testRoot\eng\common\build.ps1" $env:WINFORMS_TEST_ARGUMENTS = "$testRoot\arguments.json" $env:WINFORMS_TEST_EXIT_CODE = '0' @@ -36,44 +48,70 @@ exit ([int] $env:WINFORMS_TEST_EXIT_CODE) $env:PROCESSOR_ARCHITEW6432 = $null $actual = Invoke-TestBuild @() - Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'ARM64 host did not default to ARM64.' - Assert-True ($actual -contains '-restore' -and $actual -contains '-build' -and $actual -contains '-bl') 'Default build actions missing.' + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64') "ARM64 host did not default to ARM64: $($actual | ConvertTo-Json -Compress)" + Assert-True ($actual.restore -and $actual.build -and $actual.binaryLog) 'Default build actions missing.' $actual = Invoke-TestBuild @('-platform', 'ARM64') - Assert-True ($actual -contains '-platform' -and $actual -contains 'ARM64') 'ARM64 solution platform was discarded.' - Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'ARM64 platform did not set target architecture.' + Assert-True ($actual.platform -eq 'ARM64') 'ARM64 solution platform was discarded.' + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64') 'ARM64 platform did not set target architecture.' foreach ($platform in @('x64', 'x86')) { $actual = Invoke-TestBuild @('-platform', $platform) - Assert-True ($actual -contains "/p:TargetArchitecture=$platform") "Explicit $platform target was ignored." + Assert-True ($actual.properties -contains "/p:TargetArchitecture=$platform") "Explicit $platform target was ignored." } $actual = Invoke-TestBuild @('/p:Platform=arm64', '/p:TargetArchitecture=x64') - Assert-True ($actual -contains '/p:Platform=arm64') 'MSBuild platform was discarded.' - Assert-True ($actual -notcontains '/p:TargetArchitecture=arm64') 'Explicit target architecture was overwritten.' + Assert-True ($actual.properties -contains '/p:Platform=arm64') 'MSBuild platform was discarded.' + Assert-True ($actual.properties -notcontains '/p:TargetArchitecture=arm64') 'Explicit target architecture was overwritten.' $actual = Invoke-TestBuild @('-r', '-b', '-nobl', '-projects', 'project with spaces.csproj') - Assert-True ($actual -notcontains '-restore' -and $actual -notcontains '-build' -and $actual -notcontains '-bl') 'Action aliases were duplicated.' - Assert-True ($actual -contains 'project with spaces.csproj') 'Argument containing spaces was split.' + Assert-True ($actual.restore -and $actual.build -and $actual.excludeCIBinarylog -and !$actual.binaryLog) 'Action aliases were duplicated.' + Assert-True ($actual.projects -eq 'project with spaces.csproj') 'Argument containing spaces was split.' $actual = Invoke-TestBuild @('-nodeReuse', 'false', '-warnAsError:$true', '-mt', '1', '/p:Example=O''Brien;$notCode') - Assert-True ($actual -contains $false -and $actual -contains $true) 'Boolean arguments lost their types.' - Assert-True ($actual -contains '/p:Example=O''Brien;$notCode') 'Quoted property value was evaluated as code.' + Assert-True ($actual.nodeReuse -eq $false -and $actual.warnAsError -eq $true -and $actual.msbuildMultiThreaded -eq $true) 'Boolean arguments lost their types.' + Assert-True ($actual.properties -contains '/p:Example=O''Brien;$notCode') 'Quoted property value was evaluated as code.' $actual = Invoke-TestBuild @('-RestoreOnly') - Assert-True ($actual -contains '-restore' -and $actual -notcontains '-build' -and $actual -notcontains '-bl') 'Restore-only requested a build.' + Assert-True ($actual.restore -and !$actual.build -and !$actual.binaryLog) 'Restore-only requested a build.' $env:PROCESSOR_ARCHITECTURE = 'AMD64' $env:PROCESSOR_ARCHITEW6432 = 'ARM64' $actual = Invoke-TestBuild @() - Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'Emulated shell did not detect the ARM64 OS.' + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64') 'Emulated shell did not detect the ARM64 OS.' $env:PROCESSOR_ARCHITEW6432 = $null $actual = Invoke-TestBuild @() - Assert-True ($actual -notcontains '/p:TargetArchitecture=arm64') 'x64 host was treated as ARM64.' - Assert-True ($actual -contains '/p:TargetArchitecture=x64') 'x64 host did not default to x64.' + Assert-True ($actual.properties -notcontains '/p:TargetArchitecture=arm64') 'x64 host was treated as ARM64.' + Assert-True ($actual.properties -contains '/p:TargetArchitecture=x64') 'x64 host did not default to x64.' $actual = Invoke-TestBuild @('-platform', 'arm64') - Assert-True ($actual -contains '/p:TargetArchitecture=arm64') 'Cross-build target was ignored.' + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64') 'Cross-build target was ignored.' + + foreach ($property in @( + '/property:TargetArchitecture=arm64', + '/PROPERTY:Configuration=Debug;TargetArchitecture=arm64', + '/p:TargetArchitecture=arm64;Platform=x64')) { + $actual = Invoke-TestBuild @($property) + Assert-True ($actual.properties -contains $property -and $actual.properties.Count -eq 1) 'Explicit target architecture was overridden.' + } + + foreach ($property in @('/property:Platform=ARM64', '/p:Configuration=Debug;Platform=arm64')) { + $actual = Invoke-TestBuild @($property, '-platform', 'x64') + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64') 'MSBuild Platform did not override the Arcade platform.' + } + + $actual = Invoke-TestBuild @('-platform', 'x86', '/p:Platform=x64', '/property:Platform=ARM64') + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64') 'Last MSBuild Platform did not win.' + $actual = Invoke-TestBuild @('/p:Example="text;TargetArchitecture=arm64"') + Assert-True ($actual.properties -contains '/p:TargetArchitecture=x64') 'Quoted property contents were treated as architecture arguments.' + + $actual = Invoke-TestBuild @('-plat', 'arm64', '-rest', '-nobl') + Assert-True ($actual.properties -contains '/p:TargetArchitecture=arm64' -and $actual.restore) 'Unambiguous Arcade abbreviations were not honored.' + $actual = Invoke-TestBuild @('-plat:ARM64', '-nodeR:false', '-restore:$false') + Assert-True ($actual.platform -eq 'ARM64' -and $actual.nodeReuse -eq $false -and $actual.restore -eq $false) 'Inline parameter values were not preserved.' + + Invoke-TestBuild @('-configuration', 'Debug', '-c', 'Release') 1 + Invoke-TestBuild @('-restore', '-rest') 1 # A failed compile must propagate its exit code without retrying or masking it. $env:WINFORMS_TEST_EXIT_CODE = '7' diff --git a/eng/tests/DotNetArchitecture.Tests.ps1 b/eng/tests/DotNetArchitecture.Tests.ps1 index 39d23473f82..b701942ea97 100644 --- a/eng/tests/DotNetArchitecture.Tests.ps1 +++ b/eng/tests/DotNetArchitecture.Tests.ps1 @@ -35,17 +35,35 @@ function Write-TestPE([string] $path, [string] $architecture) { function InstallDotNet($root, $version, $architecture, $runtime, $skipNonVersionedFiles, $runtimeSourceFeed, $runtimeSourceFeedKey, [switch] $noPath) { $installCalls.Add(@{ Root = $root; Architecture = $architecture; Runtime = $runtime; Version = $version }) Assert-True (!(Test-Path "$root\shared\Microsoft.NETCore.App\$version")) 'Installer would skip a cached runtime directory.' - Write-TestPE "$root\dotnet.exe" $architecture - Write-TestPE "$root\host\fxr\$version\hostfxr.dll" $architecture - Write-TestPE "$root\shared\Microsoft.NETCore.App\$version\coreclr.dll" $architecture + if (!(Test-Path "$root\dotnet.exe")) { + Write-TestPE "$root\dotnet.exe" $architecture + } + + # Like the real extractor, do not overwrite an existing versioned resolver folder. + if (!(Test-Path "$root\host\fxr\$version") -and $omitRuntimeFile -ne 'hostfxr.dll') { + Write-TestPE "$root\host\fxr\$version\hostfxr.dll" $architecture + } + + foreach ($binary in @('coreclr.dll', 'hostpolicy.dll')) { + if ($omitRuntimeFile -ne $binary) { + $binaryArchitecture = if ($wrongRuntimeFile -eq $binary) { 'x86' } else { $architecture } + Write-TestPE "$root\shared\Microsoft.NETCore.App\$version\$binary" $binaryArchitecture + } + } + + if ($omitRuntimeFile -ne 'System.Private.CoreLib.dll') { + Set-Content -LiteralPath "$root\shared\Microsoft.NETCore.App\$version\System.Private.CoreLib.dll" -Value 'managed fixture' + } if (!$runtime) { New-Item -ItemType Directory -Path "$root\sdk\$version" -Force | Out-Null } } $testRoot = Join-Path ([IO.Path]::GetTempPath()) "WinForms-architecture-tests-$([Guid]::NewGuid().ToString('N'))" +$omitRuntimeFile = $null +$wrongRuntimeFile = $null $savedEnvironment = @{} -foreach ($name in @('PROCESSOR_ARCHITECTURE', 'PROCESSOR_ARCHITEW6432', 'DOTNET_GLOBAL_INSTALL_DIR', 'PATH')) { +foreach ($name in @('PROCESSOR_ARCHITECTURE', 'PROCESSOR_ARCHITEW6432', 'DOTNET_GLOBAL_INSTALL_DIR', 'DOTNET_ROOT', 'DOTNET_ROOT_X86', 'DOTNET_ROOT_X64', 'DOTNET_ROOT_ARM64', 'PATH')) { $savedEnvironment[$name] = [Environment]::GetEnvironmentVariable($name) } @@ -93,6 +111,13 @@ try { Repair-DotNetInstallation $root Assert-True (@(Get-ChildItem "$root\legacy-runtimes" -Recurse -File).Count -eq 2) 'Repair is not idempotent.' + New-Item -ItemType Directory -Path "$root\host\fxr\3.0.0", "$root\shared\Microsoft.NETCore.App\3.0.0" -Force | Out-Null + Repair-DotNetInstallation $root + Assert-True (!(Test-Path "$root\host\fxr\3.0.0") -and !(Test-Path "$root\shared\Microsoft.NETCore.App\3.0.0")) 'Incomplete version directories still block runtime discovery.' + Assert-True (@(Get-ChildItem "$root\legacy-runtimes\host\fxr" -Directory).Count -eq 2) 'Incomplete resolver folder was not preserved.' + Repair-DotNetInstallation $root + Assert-True (@(Get-ChildItem "$root\legacy-runtimes\host\fxr" -Directory).Count -eq 2) 'Incomplete resolver repair is not idempotent.' + $unsafeRoot = "$testRoot\no-compatible-resolver-$sdk" Write-TestPE "$unsafeRoot\dotnet.exe" $sdk Write-TestPE "$unsafeRoot\host\fxr\1.0.0\hostfxr.dll" $other @@ -100,6 +125,13 @@ try { Assert-Throws { Repair-DotNetInstallation $unsafeRoot } 'No hostfxr compatible*' Assert-True ((Test-Path "$unsafeRoot\host\fxr\1.0.0\hostfxr.dll") -and (Test-Path "$unsafeRoot\shared\Microsoft.NETCore.App\1.0.0\coreclr.dll")) 'Failed preflight moved files.' + $missingRoot = "$testRoot\missing-resolver-$sdk" + Write-TestPE "$missingRoot\dotnet.exe" $sdk + New-Item -ItemType Directory -Path "$missingRoot\host\fxr\3.0.0" -Force | Out-Null + Write-TestPE "$missingRoot\shared\Microsoft.NETCore.App\3.0.0\coreclr.dll" $other + Assert-Throws { Repair-DotNetInstallation $missingRoot } 'No hostfxr compatible*' + Assert-True ((Test-Path "$missingRoot\host\fxr\3.0.0") -and (Test-Path "$missingRoot\shared\Microsoft.NETCore.App\3.0.0\coreclr.dll")) 'Missing-resolver preflight moved files.' + foreach ($target in @('x64', 'arm64', 'x86')) { $runtimeRoot = "$testRoot\runtime-$sdk-$target" Write-TestPE "$runtimeRoot\dotnet.exe" $sdk @@ -116,7 +148,46 @@ try { Install-WinFormsDotNetRuntime $runtimeRoot $sdk $target '3.0.0' '' '' Assert-True ((Get-DotNetExecutableArchitecture "$expectedRoot\shared\Microsoft.NETCore.App\3.0.0\coreclr.dll") -eq $target) 'Partial or foreign runtime cache was not repaired.' } + + foreach ($case in @( + @{ Name = 'missing-hostpolicy'; File = 'shared\Microsoft.NETCore.App\2.0.0\hostpolicy.dll' }, + @{ Name = 'missing-corelib'; File = 'shared\Microsoft.NETCore.App\2.0.0\System.Private.CoreLib.dll' }, + @{ Name = 'foreign-hostpolicy'; File = 'shared\Microsoft.NETCore.App\2.0.0\hostpolicy.dll'; Architecture = $other }, + @{ Name = 'missing-hostfxr'; File = 'host\fxr\2.0.0\hostfxr.dll' }, + @{ Name = 'foreign-hostfxr'; File = 'host\fxr\2.0.0\hostfxr.dll'; Architecture = $other })) { + $cacheRoot = "$testRoot\$sdk-$($case.Name)" + $installCalls = [System.Collections.Generic.List[object]]::new() + Install-WinFormsDotNetRuntime $cacheRoot $sdk $sdk '2.0.0' '' '' + $file = Join-Path $cacheRoot $case.File + if ($case.Architecture) { Write-TestPE $file $case.Architecture } + else { Remove-Item -LiteralPath $file } + + Install-WinFormsDotNetRuntime $cacheRoot $sdk $sdk '2.0.0' '' '' + Assert-True ($installCalls.Count -eq 2 -and (Test-Path -LiteralPath $file)) "Incomplete cache was reused: $($case.Name)" + if ($case.Architecture) { + Assert-True ((Get-DotNetExecutableArchitecture $file) -eq $sdk) "Foreign payload was not replaced: $($case.Name)" + } + + $resolverBackups = "$cacheRoot\legacy-runtimes\host\fxr" + Assert-True ((Test-Path $resolverBackups) -eq $case.File.StartsWith('host\')) 'A healthy resolver was moved, or an incomplete resolver was not preserved.' + Install-WinFormsDotNetRuntime $cacheRoot $sdk $sdk '2.0.0' '' '' + Assert-True ($installCalls.Count -eq 2) 'Repaired runtime was not reused.' + } + } + + foreach ($omittedFile in @('hostfxr.dll', 'System.Private.CoreLib.dll')) { + $omitRuntimeFile = $omittedFile + $installCalls = [System.Collections.Generic.List[object]]::new() + Assert-Throws { + Install-WinFormsDotNetRuntime "$testRoot\incomplete-install-$omittedFile" 'x64' 'x64' '2.0.0' '' '' + } "*incomplete or incompatible: *$omittedFile*legacy-runtimes*" } + $omitRuntimeFile = $null + $wrongRuntimeFile = 'hostpolicy.dll' + Assert-Throws { + Install-WinFormsDotNetRuntime "$testRoot\foreign-install" 'x64' 'x64' '2.0.0' '' '' + } '*incomplete or incompatible: *hostpolicy.dll*' + $wrongRuntimeFile = $null # Exercise the repository's Arcade extension, including native fresh bootstrap and # preserving an x64 SDK while the OS (and requested target) is ARM64. @@ -128,11 +199,20 @@ try { $env:PROCESSOR_ARCHITECTURE = $os $env:PROCESSOR_ARCHITEW6432 = $null $env:DOTNET_GLOBAL_INSTALL_DIR = "$testRoot\bootstrap-$os" + $env:DOTNET_ROOT_ARM64 = 'stale runtime root' $installCalls = [System.Collections.Generic.List[object]]::new() + $genericRoot = $env:DOTNET_ROOT . "$PSScriptRoot\..\configure-toolset.ps1" + $rootsBeforeInstall = @($env:DOTNET_ROOT_X86, $env:DOTNET_ROOT_X64, $env:DOTNET_ROOT_ARM64) InstallDotNetSdk $env:DOTNET_GLOBAL_INSTALL_DIR '4.0.0' $expected = if ($os -eq 'ARM64') { 'arm64' } else { 'x64' } Assert-True ($installCalls[0].Architecture -eq $expected) 'Fresh SDK bootstrap used the shell instead of the OS architecture.' + Assert-True ($env:DOTNET_ROOT -eq $genericRoot) 'Toolset initialization retargeted the generic runtime root.' + $index = 0 + foreach ($architecture in @('x86', 'x64', 'arm64')) { + $runtimeRoot = Get-DotNetRuntimeDirectory $env:DOTNET_GLOBAL_INSTALL_DIR $expected $architecture + Assert-True ($rootsBeforeInstall[$index++] -eq $runtimeRoot) 'Pre-install runtime roots do not match the bootstrapped SDK.' + } } $env:PROCESSOR_ARCHITECTURE = 'ARM64' @@ -143,6 +223,7 @@ try { . "$PSScriptRoot\..\configure-toolset.ps1" InstallDotNetSdk $env:DOTNET_GLOBAL_INSTALL_DIR '4.0.0' Assert-True ($installCalls[0].Architecture -eq 'x64') 'SDK upgrade replaced an emulated x64 installation.' + Assert-True ($env:DOTNET_ROOT_X64 -eq $env:DOTNET_GLOBAL_INSTALL_DIR -and $env:DOTNET_ROOT_ARM64 -eq "$env:DOTNET_GLOBAL_INSTALL_DIR\arm64") 'Emulated SDK did not export independent apphost runtime roots.' $env:PROCESSOR_ARCHITECTURE = 'AMD64' $originalRoot = "$testRoot\arm64-sdk-on-x64" @@ -161,14 +242,22 @@ try { Copy-Item "$PSScriptRoot\..\dotnet-architecture.ps1" "$installerRepo\eng\dotnet-architecture.ps1" '$GlobalJson = Get-Content "$PSScriptRoot\..\..\global.json" -Raw | ConvertFrom-Json' | Set-Content "$installerRepo\eng\common\tools.ps1" - '{"tools":{"runtimes":{"dotnet":["$(MicrosoftNETCoreAppRefPackageVersion)"],"dotnet/x86":["$(MicrosoftNETCoreAppRefPackageVersion)"]}}}' | - Set-Content "$installerRepo\global.json" + Copy-Item "$PSScriptRoot\..\..\global.json" "$installerRepo\global.json" foreach ($sdk in @('x64', 'arm64')) { - $sdkRoot = "$installerRepo\sdk-$sdk" - Write-TestPE "$sdkRoot\dotnet.exe" $sdk - Write-TestPE "$sdkRoot\host\fxr\1.0.0\hostfxr.dll" $sdk + $env:PROCESSOR_ARCHITECTURE = if ($sdk -eq 'arm64') { 'ARM64' } else { 'AMD64' } + $sdkRoot = "$installerRepo\sdk $sdk" + $env:DOTNET_GLOBAL_INSTALL_DIR = $sdkRoot $installCalls = [System.Collections.Generic.List[object]]::new() + . "$PSScriptRoot\..\configure-toolset.ps1" + InstallDotNetSdk $sdkRoot '4.0.0' + $installCalls.Clear() + & "$installerRepo\eng\install-runtimes.ps1" -SdkRoot $sdkRoot -RuntimeVersion '5.0.0' -TargetArchitecture $sdk + Assert-True ($installCalls.Count -eq 2) 'Fresh native restore did not install SDK-native and x86 runtimes.' $target = if ($sdk -eq 'x64') { 'arm64' } else { 'x64' } + Assert-True (!(Test-Path "$sdkRoot\$target")) 'Fresh native restore installed an unrequested foreign runtime.' + Assert-True ((Get-DotNetExecutableArchitecture "$sdkRoot\dotnet.exe") -eq $sdk -and + (Get-DotNetExecutableArchitecture "$sdkRoot\host\fxr\5.0.0\hostfxr.dll") -eq $sdk) 'Fresh restore mixed SDK and resolver architectures.' + & "$installerRepo\eng\install-runtimes.ps1" -SdkRoot $sdkRoot -RuntimeVersion '5.0.0' -TargetArchitecture $target Assert-True ($installCalls.Count -eq 3) 'Runtime manifest did not install SDK, cross-target, and x86 runtimes.' foreach ($architecture in @($sdk, $target, 'x86')) { @@ -179,6 +268,23 @@ try { Assert-True ($installCalls.Count -eq 3) 'Duplicate manifest requests bypassed the complete runtime cache.' } + $repoRoot = [IO.Path]::GetFullPath("$PSScriptRoot\..\..\") + foreach ($launcher in @('start-vs.cmd', 'start-code.cmd')) { + $commands = [regex]::Matches((Get-Content (Join-Path $repoRoot $launcher) -Raw), "'(?powershell [^']+)'") + Assert-True ($commands.Count -eq 2) "Could not find SDK/runtime queries in $launcher." + foreach ($match in $commands) { + $command = $match.Groups['Command'].Value.Replace('%~dp0', $repoRoot).Replace('%DOTNET_ROOT%', "$sdkRoot\") + $resolved = @(& $env:ComSpec /d /c $command) + Assert-True ($LASTEXITCODE -eq 0) "Trailing-backslash SDK query failed in $launcher." + if ($command -like '*-ResolveRuntimeRoots') { + Assert-True ($resolved -contains "DOTNET_ROOT_ARM64=$sdkRoot") "Runtime query corrupted a trailing-backslash root in $launcher." + } + else { + Assert-True ($resolved.Count -eq 1 -and $resolved[0] -eq $sdkRoot) "SDK query corrupted a trailing-backslash root in $launcher." + } + } + } + Assert-Throws { Get-DotNetRuntimeDirectory $testRoot 'x64' 'invalid' } 'Unsupported .NET runtime architecture*' [IO.File]::WriteAllBytes("$testRoot\invalid.exe", [byte[]]::new(128)) Assert-Throws { Get-DotNetExecutableArchitecture "$testRoot\invalid.exe" } 'Invalid PE file*' diff --git a/start-code.cmd b/start-code.cmd index f7d042aec99..3f5e4232e3c 100644 --- a/start-code.cmd +++ b/start-code.cmd @@ -15,7 +15,7 @@ IF ["%vscode%"] == [""] ( SET DOTNET_ROOT=%~dp0.dotnet IF DEFINED DOTNET_GLOBAL_INSTALL_DIR SET "DOTNET_ROOT=%DOTNET_GLOBAL_INSTALL_DIR%" SET "__DotNetSdkRoot=" -FOR /f "delims=" %%r IN ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%"') DO SET "__DotNetSdkRoot=%%r" +FOR /f "delims=" %%r IN ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%\."') DO SET "__DotNetSdkRoot=%%r" IF NOT DEFINED __DotNetSdkRoot EXIT /b 1 SET "DOTNET_ROOT=%__DotNetSdkRoot%" SET "DOTNET_GLOBAL_INSTALL_DIR=%DOTNET_ROOT%" @@ -29,7 +29,7 @@ IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" ( exit /b 1 ) -FOR /f "delims=" %%r IN ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%" -ResolveRuntimeRoots') DO SET "%%r" +FOR /f "delims=" %%r IN ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%\." -ResolveRuntimeRoots') DO SET "%%r" SET "DOTNET_ROOT(x86)=%DOTNET_ROOT_X86%" "%vscode%" "." \ No newline at end of file diff --git a/start-vs.cmd b/start-vs.cmd index 07d4eb7fea3..1df3004c00d 100644 --- a/start-vs.cmd +++ b/start-vs.cmd @@ -7,7 +7,7 @@ setlocal enabledelayedexpansion set DOTNET_ROOT=%~dp0.dotnet if defined DOTNET_GLOBAL_INSTALL_DIR set "DOTNET_ROOT=%DOTNET_GLOBAL_INSTALL_DIR%" set "__DotNetSdkRoot=" -for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%"') do set "__DotNetSdkRoot=%%r" +for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%\."') do set "__DotNetSdkRoot=%%r" if not defined __DotNetSdkRoot exit /b 1 set "DOTNET_ROOT=%__DotNetSdkRoot%" set "DOTNET_GLOBAL_INSTALL_DIR=%DOTNET_ROOT%" @@ -24,7 +24,7 @@ if not exist "%DOTNET_ROOT%\dotnet.exe" ( exit /b 1 ) -for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%" -ResolveRuntimeRoots') do set "%%r" +for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy ByPass -File "%~dp0eng\dotnet-architecture.ps1" -ResolveSdkRoot "%DOTNET_ROOT%\." -ResolveRuntimeRoots') do set "%%r" set "DOTNET_ROOT(x86)=%DOTNET_ROOT_X86%" :: Prefer the VS in the developer command prompt if we're in one, followed by whatever shows up in the current search path.