diff --git a/Directory.Build.props b/Directory.Build.props index 6ea9e7f0bd2..5babeeda2a4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,8 @@ True false + $(Platform.ToLowerInvariant()) + 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/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/docs/building.md b/docs/building.md index 25f26541301..1c38b2c7a19 100644 --- a/docs/building.md +++ b/docs/building.md @@ -13,6 +13,53 @@ 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 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). +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`, 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`; +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 new file mode 100644 index 00000000000..9d8a594ecf7 --- /dev/null +++ b/eng/build.cmd.ps1 @@ -0,0 +1,131 @@ +$ErrorActionPreference = 'Stop' +$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() + $platform = $null + $propertyPlatform = $null + $hasPropertyPlatform = $false + $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 + } + + 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 + } + } + } + } + + $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())") + } + else { + $forwardArgs.Add("/p:TargetArchitecture=$hostArchitecture") + } + } + + 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 = $buildScriptPath.Replace("'", "''") + $command = "`$ErrorActionPreference = 'Stop'; `$ProgressPreference = 'SilentlyContinue'; & '$buildScript' -NativeToolsOnMachine" + for ($i = 0; $i -lt $forwardArgs.Count; $i++) { + $arg = $forwardArgs[$i] + $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.Name)." } + $forwardArgs[$i] + } + $separator = if ($parameter.ParameterType -eq [switch]) { ':' } else { ' ' } + switch ($value.TrimStart('$').ToLowerInvariant()) { + { $_ -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]+$') { + $command += " $arg" + } + else { + $command += " '$($arg.Replace("'", "''"))'" + } + } + + $command += ' 6>&1; exit $LASTEXITCODE' + $encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) + & powershell -ExecutionPolicy ByPass -NoProfile -OutputFormat Text -EncodedCommand $encodedCommand + exit $LASTEXITCODE +} +catch { + Write-Error $_ -ErrorAction Continue + exit 1 +} diff --git a/eng/configure-toolset.ps1 b/eng/configure-toolset.ps1 index a6048f7e634..a9fc68f15ce 100644 --- a/eng/configure-toolset.ps1 +++ b/eng/configure-toolset.ps1 @@ -3,3 +3,35 @@ $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 + } + + $_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 + } + + # 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..63f6089ac77 --- /dev/null +++ b/eng/dotnet-architecture.ps1 @@ -0,0 +1,206 @@ +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) { + $dotnetRoot = [IO.Path]::GetFullPath($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 + $incompatibleVersions = @() + $compatibleResolvers = 0 + $incompatibleResolvers = 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 -PathType Leaf) -and + (Get-DotNetExecutableArchitecture $binaryPath) -eq $architecture) { + if ($component -eq 'host\fxr') { $compatibleResolvers++ } + } + else { + if ($component -eq 'host\fxr') { $incompatibleResolvers++ } + $incompatibleVersions += @{ Component = $component; Directory = $versionDir } + } + } + } + + # Preflight the whole installation before moving anything, including shared runtimes. + 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 $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, + [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" + $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 + # 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 + $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 { + 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..9aeffcfe1a7 --- /dev/null +++ b/eng/tests/BuildScript.Tests.ps1 @@ -0,0 +1,131 @@ +$ErrorActionPreference = 'Stop' + +function Assert-True([bool] $condition, [string] $message) { + if (!$condition) { + throw $message + } +} + +function Invoke-TestBuild([string[]] $arguments, [int] $expectedExitCode = 0) { + $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 + } +} + +$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" + $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) +'@ + $fixture | 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.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.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.properties -contains "/p:TargetArchitecture=$platform") "Explicit $platform target was ignored." + } + + $actual = Invoke-TestBuild @('/p:Platform=arm64', '/p:TargetArchitecture=x64') + 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.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.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.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.properties -contains '/p:TargetArchitecture=arm64') 'Emulated shell did not detect the ARM64 OS.' + + $env:PROCESSOR_ARCHITEW6432 = $null + $actual = Invoke-TestBuild @() + 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.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' + 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..b701942ea97 --- /dev/null +++ b/eng/tests/DotNetArchitecture.Tests.ps1 @@ -0,0 +1,300 @@ +$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.' + 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', 'DOTNET_ROOT', 'DOTNET_ROOT_X86', 'DOTNET_ROOT_X64', 'DOTNET_ROOT_ARM64', '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.' + + 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 + 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.' + + $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 + 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.' + } + + 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. + $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" + $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' + $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.' + 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" + $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" + Copy-Item "$PSScriptRoot\..\..\global.json" "$installerRepo\global.json" + foreach ($sdk in @('x64', 'arm64')) { + $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')) { + $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.' + } + + $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*' + 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 6bcdd3f22fa..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": [ 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..3f5e4232e3c 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..1df3004c00d 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"