Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PublishWindowsPdb>false</PublishWindowsPdb>

<TargetArchitecture Condition="'$(TargetArchitecture)' == '' and ('$(Platform)' == 'arm64' or '$(Platform)' == 'x64' or '$(Platform)' == 'x86')">$(Platform.ToLowerInvariant())</TargetArchitecture>
<TargetArchitecture Condition="'$(TargetArchitecture)' == '' and '$(OS)' == 'Windows_NT' and $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) == 'Arm64'">arm64</TargetArchitecture>
<DefaultAppHostRuntimeIdentifier Condition="'$(TargetArchitecture)'!=''">win-$(TargetArchitecture)</DefaultAppHostRuntimeIdentifier>
</PropertyGroup>

Expand Down
3 changes: 2 additions & 1 deletion Restore.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@echo off
powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0eng\common\Build.ps1""" -NativeToolsOnMachine -restore %*"
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0eng\build.cmd.ps1" -RestoreOnly %*
exit /b %ErrorLevel%
2 changes: 1 addition & 1 deletion build.cmd
Original file line number Diff line number Diff line change
@@ -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%
47 changes: 47 additions & 0 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full solution builds successfully with .\build.cmd -platform x64, and ScratchProject correctly produces win-x64.

Building an individual project fails:

.\build.cmd -test -platform x64 -projects .\src\System.Windows.Forms.Primitives\tests\UnitTests\System.Windows.Forms.Primitives.Tests.csproj
Image

System.Windows.Forms fails with CS8034 because System.Windows.Forms.PrivateSourceGenerators is built to
artifacts\bin\...\x64\Debug\nets…dard2.0\
as an x64 assembly, which the ARM64 compiler cannot load.

p:TargetArchitecture=x64 without -platform works as a workaround.

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.
Expand Down
20 changes: 20 additions & 0 deletions eng/Tools.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project>
<!-- Bypass Arcade's directory-only cache checks and x86-only runtime isolation.
The repository installer uses the SDK muxer architecture, not the OS or target. -->
<Target Name="InstallWinFormsRuntimes"
BeforeTargets="InstallDotNetCore"
Condition="'$(OS)' == 'Windows_NT' and '$(DotNetBuildSourceOnly)' != 'true' and '$(SkipRuntimesInstall)' != 'true'">
<PropertyGroup>
<SkipRuntimesInstall>true</SkipRuntimesInstall>
<_WinFormsRuntimeRoot>$(DotNetRoot)</_WinFormsRuntimeRoot>
<_WinFormsRuntimeRoot Condition="'$(DOTNET_GLOBAL_INSTALL_DIR)' != ''">$(DOTNET_GLOBAL_INSTALL_DIR)</_WinFormsRuntimeRoot>
<_WinFormsRuntimeRootArgument>$(_WinFormsRuntimeRoot)</_WinFormsRuntimeRootArgument>
<_WinFormsRuntimeRootArgument Condition="$(_WinFormsRuntimeRoot.EndsWith('\'))">$(_WinFormsRuntimeRoot)\</_WinFormsRuntimeRootArgument>
<_WinFormsRuntimeSourceOptions Condition="'$(DotNetRuntimeSourceFeed)' != ''">-RuntimeSourceFeed "$(DotNetRuntimeSourceFeed)"</_WinFormsRuntimeSourceOptions>
<_WinFormsRuntimeSourceOptions Condition="'$(DotNetRuntimeSourceFeed)' != '' and '$(DotNetRuntimeSourceFeedKey)' != ''">$(_WinFormsRuntimeSourceOptions) -RuntimeSourceFeedKey "$(DotNetRuntimeSourceFeedKey)"</_WinFormsRuntimeSourceOptions>
<_WinFormsTargetArchitectureOptions Condition="'$(TargetArchitecture)' != ''">-TargetArchitecture "$(TargetArchitecture)"</_WinFormsTargetArchitectureOptions>
</PropertyGroup>

<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File &quot;$(RepositoryEngineeringDir)install-runtimes.ps1&quot; -SdkRoot &quot;$(_WinFormsRuntimeRootArgument)&quot; -RuntimeVersion &quot;$(MicrosoftNETCoreAppRefPackageVersion)&quot; $(_WinFormsTargetArchitectureOptions) $(_WinFormsRuntimeSourceOptions)" />
</Target>
</Project>
131 changes: 131 additions & 0 deletions eng/build.cmd.ps1
Original file line number Diff line number Diff line change
@@ -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 '^-(?<Name>[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*(?<Name>[^=;]+)=(?<Value>(?:"[^"]*"|[^;"])*)')) {
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
}
32 changes: 32 additions & 0 deletions eng/configure-toolset.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading