diff --git a/azure-pipelines/.vsts-dotnet-build-jobs.yml b/azure-pipelines/.vsts-dotnet-build-jobs.yml
index 1b5f19a8d03..c8fffe6904b 100644
--- a/azure-pipelines/.vsts-dotnet-build-jobs.yml
+++ b/azure-pipelines/.vsts-dotnet-build-jobs.yml
@@ -210,9 +210,9 @@ jobs:
"xsd"
)
- if ("${{ parameters.enableOptProf }}" -eq "true") {
- $paths += "OptProf/$(BuildConfiguration)/Data"
- $paths += "VSSetup/$(BuildConfiguration)"
+ if ("${{ parameters.enableOptProf }}" -eq "true") {
+ $paths += "OptProf/$(BuildConfiguration)/Data"
+ $paths += "VSSetup/$(BuildConfiguration)"
}
$allpresent = $true
diff --git a/eng/MicrobuildTest.ps1 b/eng/MicrobuildTest.ps1
deleted file mode 100644
index d57b82644a8..00000000000
--- a/eng/MicrobuildTest.ps1
+++ /dev/null
@@ -1,421 +0,0 @@
-param (
- [Parameter(Mandatory = $false)]
- [string] $CPVSDrop
-)
-
-function CombineAndNormalize([string[]] $paths) {
- $combined = [System.IO.Path]::Combine($paths)
- return [System.IO.Path]::GetFullPath($combined)
-}
-
-function Log ($a) {
- Write-Host `n
- Write-Host $a.ToString()
-}
-
-function Test-AssemblyStrongNamed($assemblyPath) {
- $hasPublicKey = $true
-
- try {
- $hasPublicKey = [System.Reflection.Assembly]::ReflectionOnlyLoadFrom($assemblyPath).GetName().GetPublicKeyToken().Count -gt 0
- }
- catch {
- if (-Not $_.Exception.Message.Contains("It cannot be loaded from a new location within the same appdomain")) {
- throw
- }
- }
-
- return $hasPublicKey
-}
-
-class BuildInstance {
- static $languages = @("cs", "de", "en", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant")
-
- [string] $Root
-
- [string[]] $AssemblyNames = @(
- "Microsoft.Build.dll",
- "Microsoft.Build.Framework.dll",
- "Microsoft.Build.Tasks.Core.dll",
- "Microsoft.Build.Utilities.Core.dll"
- )
-
- [string[]] $SatelliteAssemblyNames = @(
- "Microsoft.Build.resources.dll",
- "Microsoft.Build.Tasks.Core.resources.dll",
- "Microsoft.Build.Utilities.Core.resources.dll",
- "MSBuild.resources.dll"
- )
-
- BuildInstance([String] $root) {
- $this.Root = $root
- }
-
- [string[]] BuildFiles() {
- return $this.ResolvedAssemblies() + $this.ResolvedSatelliteAssemblies()
- }
-
- [string[]] ResolvedAssemblies() {
- return $this.AssemblyNames | foreach{CombineAndNormalize($this.Root, $_)}
- }
-
- [string[]] ResolvedSatelliteAssemblies() {
- $satellites = @()
-
- foreach ($l in [BuildInstance]::languages) {
- foreach ($s in $this.SatelliteAssemblyNames) {
- $satellites += CombineAndNormalize(@($this.Root, $l, $s))
- }
- }
-
- return $satellites
- }
-
- [String] ToString() {
- return $this.Root + "`n`n" + (($this.BuildFiles() | foreach{"`t`t" + $_.ToString()}) -join "`n")
- }
-
- Check([Checker] $checker) {
- $checker.Check($this)
- }
-}
-
-class FullFrameworkBuildInstance : BuildInstance{
- FullFrameworkBuildInstance([String] $root) : base ($root) {
- ([BuildInstance]$this).AssemblyNames += @(
- "MSBuild.exe",
- "MSBuildTaskHost.exe",
- "Microsoft.Build.Conversion.Core.dll",
- "Microsoft.Build.Engine.dll"
- )
-
- ([BuildInstance]$this).SatelliteAssemblyNames += @(
- "Microsoft.Build.Conversion.Core.resources.dll",
- "Microsoft.Build.Engine.resources.dll",
- "MSBuildTaskHost.resources.dll"
- )
- }
-}
-
-class NetCoreBuildInstance : BuildInstance{
- NetCoreBuildInstance([String] $root) : base ($root) {
- ([BuildInstance]$this).AssemblyNames += "MSBuild.dll"
- }
-}
-
-class NugetPackage{
- [String] $Path
-
- NugetPackage([string] $Path) {
- $this.Path = $Path
- }
-
- Check([Checker] $checker) {
- $checker.Check($this)
- }
-
- [string] ToString() {
- return "NugetPackage: $($this.Path)"
- }
-}
-
-class VsixPackage{
- static [string] $PackageName = "Microsoft.Build.vsix"
- [String] $Path
-
- VsixPackage([string] $path) {
- $this.Path = CombineAndNormalize($path, [VsixPackage]::PackageName)
- }
-
- Check([Checker] $checker) {
- $checker.Check($this)
- }
-
- [string] ToString() {
- return "Vsix package: $($this.Path)"
- }
-}
-
-class Layout {
-
- [String] $FFx86;
- [String] $FFx64;
- [String] $FFAnyCPU;
- [String] $CoreAnyCPU;
- [String] $NugetPackagePath;
- [String] $VsixPath;
-
- [BuildInstance[]] $BuildInstances
- [NugetPackage[]] $NugetPackages
- [VsixPackage] $VsixPackage
-
- Layout($FFx86, $FFx64, $FFAnyCPU, $CoreAnyCPU, $NugetPackagePath, $VsixPath) {
- $this.FFx86 = $FFx86
- $this.FFx64 = $FFx64
- $this.FFAnyCPU = $FFAnyCPU
- $this.CoreAnyCPU = $CoreAnyCPU
- $this.NugetPackagePath = $NugetPackagePath
- $this.VsixPath = $VsixPath
-
- $this.BuildInstances = @(
- [FullFrameworkBuildInstance]::new($this.FFx86),
- [FullFrameworkBuildInstance]::new($this.FFx64),
- [FullFrameworkBuildInstance]::new($this.FFAnyCPU),
- [NetCoreBuildInstance]::new($this.CoreAnyCPU)
- )
-
- $this.NugetPackages = Get-ChildItem $nugetPackagePath *.nupkg | foreach {[NugetPackage]::new($_.FullName)}
- $this.VsixPackage = [VsixPackage]::new($VsixPath)
- }
-
- [String] ToString() {
- $instances = ($this.BuildInstances | foreach{ "`t" + $_.ToString() }) -join "`n`n"
- $nugets = ($this.NugetPackages | foreach{"`t" + $_.ToString()}) -join "`n"
- return "Build Instances:`n$instances`n`nNuget Packages`n$($nugets)`n`n$($this.VsixPackage.ToString())"
- }
-
- static [Layout] FromMicrobuild() {
- $root = $env:BUILD_REPOSITORY_LOCALPATH
-
- $layout = [Layout]::new(
- (CombineAndNormalize @($root, $env:FFBINPATH86)),
- (CombineAndNormalize @($root, $env:FFBINPATH64)),
- (CombineAndNormalize @($root, $env:BINPATH)),
- (CombineAndNormalize @($root, $env:BINPATHNETCORE)),
- (CombineAndNormalize @($root, $env:NUGETPACKAGESPATH)),
- (CombineAndNormalize @($root, $env:SETUPLAYOUTPATH)))
-
- return $layout
- }
-
- static [Layout] FromCPVSDrop([string] $root) {
-
- $layout = [Layout]::new(
- (CombineAndNormalize @($root, "bin\Release\x86\Windows_NT\Output")),
- (CombineAndNormalize @($root, "bin\Release\x64\Windows_NT\Output")),
- (CombineAndNormalize @($root, "bin\Release\AnyCPU\Windows_NT\Output")),
- (CombineAndNormalize @($root, "bin\Release-NetCore\AnyCPU\Windows_NT\Output")),
- (CombineAndNormalize @($root, "bin\Packages")),
- (CombineAndNormalize @($root, "pkg")))
-
- return $layout
- }
-
- Check([Checker] $checker) {
- $checker.Check($this)
-
- $this.BuildInstances | foreach {$_.Check($checker)}
- $this.NugetPackages | foreach {$_.Check($checker)}
- $this.VsixPackage.Check($checker)
- }
-}
-
-class Diagnostic{
- [String] $Type
- [String] $Message
-
- Diagnostic([String] $type, [String] $message) {
- $this.Type = $type
- $this.Message = $message
- }
-}
-
-class Checker{
- [Diagnostic[]] $Diagnostics = @()
-
- Check($obj) {
- $diags = $this.HandleObject($obj)
-
- if ($diags -ne $null) {
- $this.Diagnostics += $diags
- }
- }
-
- [Diagnostic[]] HandleObject($obj) {
- $type = $obj.GetType()
-
- $diags = @()
-
- if ($type -eq [Layout]) {
- $diags = $this.CheckLayout($obj)
- }
- elseif ($type -eq [FullFrameworkBuildInstance]) {
- $diags = $this.CheckFullFrameworkBuildInstance($obj)
- }
- elseif ($type -eq [NetCoreBuildInstance]) {
- $diags = $this.CheckNetCoreBuildInstance($obj)
- }
- elseif ($type -eq [NugetPackage]) {
- $diags = $this.CheckNugetPackage($obj)
- }
- elseif ($type -eq [VsixPackage]) {
- $diags = $this.CheckVSixPackage($obj)
- }
- else {
- $diags = $this.HandleUnknownType($obj, $type)
- }
-
- return $diags
- }
-
- [Diagnostic[]] HandleUnknownType($obj, [Type] $type) {
- throw [System.NotImplementedException]
- }
-
- [Diagnostic[]] CheckLayout([Layout] $l) {return @()}
- [Diagnostic[]] CheckFullFrameworkBuildInstance([FullFrameworkBuildInstance] $b) {return @()}
- [Diagnostic[]] CheckNetCoreBuildInstance([NetCoreBuildInstance] $b) {return @()}
- [Diagnostic[]] CheckNugetPackage([NugetPackage] $n) {return @()}
- [Diagnostic[]] CheckVSixPackage([VsixPackage] $v) {return @()}
-
- [Diagnostic] NewDiagnostic([String] $message) {
- return [Diagnostic]::new($this.GetType().Name, $message)
- }
-}
-
-class TestChecker : Checker{
- [Diagnostic[]] CheckLayout([Layout] $l) {return $this.NewDiagnostic("Checked Layout")}
- [Diagnostic[]] CheckFullFrameworkBuildInstance([FullFrameworkBuildInstance] $b) {return $this.NewDiagnostic("Checked FF Build Instance: $($b.Root)")}
- [Diagnostic[]] CheckNetCoreBuildInstance([NetCoreBuildInstance] $b) {return $this.NewDiagnostic("Checked Core Build Instance: $($b.Root)")}
- [Diagnostic[]] CheckNugetPackage([NugetPackage] $n) {return $this.NewDiagnostic("Checked Nuget Package: $($n.Path)")}
- [Diagnostic[]] CheckVSixPackage([VsixPackage] $v) {return $this.NewDiagnostic("Checked VsixPackage: $($v.Path)")}
-}
-
-class FileChecker : Checker{
- $ExpectedNumberOfNugetPackages = 8
-
- [Diagnostic[]] CheckPathExists([string] $path) {
- if (-Not (Test-Path $path)) {
- return $this.NewDiagnostic("Path does not exist: $path");
- }
-
- return @()
- }
-
- [Diagnostic[]] CheckLayout([Layout] $l) {
- $diags = @()
-
- $diags += $this.CheckPathExists($l.FFx86)
- $diags += $this.CheckPathExists($l.FFx64)
- $diags += $this.CheckPathExists($l.FFAnyCPU)
- $diags += $this.CheckPathExists($l.CoreAnyCPU)
- $diags += $this.CheckPathExists($l.NugetPackagePath)
- $diags += $this.CheckPathExists($l.VsixPath)
-
- if ($l.NugetPackages.Count -ne $this.ExpectedNumberOfNugetPackages) {
- $diags += $this.NewDiagnostic("There should be $($this.ExpectedNumberOfNugetPackages) nuget packages in $($l.NugetPackagePath) but $($l.NugetPackages.Count) were found")
- }
-
- return $diags
- }
-
- [Diagnostic[]] CheckBuildInstance([BuildInstance] $b) {
- return $b.BuildFiles() | foreach{$this.CheckPathExists($_)}
- }
-
- [Diagnostic[]] CheckFullFrameworkBuildInstance([FullFrameworkBuildInstance] $b) {
- return $this.CheckBuildInstance($b)
- }
-
- [Diagnostic[]] CheckNetCoreBuildInstance([NetCoreBuildInstance] $b) {
- return $this.CheckBuildInstance($b)
- }
-
- [Diagnostic[]] CheckNugetPackage([NugetPackage] $n) {
- return $this.CheckPathExists($n.Path)
- }
-
- [Diagnostic[]] CheckVSixPackage([VsixPackage] $v) {
- return $this.CheckPathExists($v.Path)
- }
-}
-
-class RealSignedChecker : Checker {
- [Diagnostic[]] CheckIsSigned([String] $assembly) {
- if (-Not (Test-Path $assembly)) {
- return @()
- }
-
- $signature = Get-AuthenticodeSignature $assembly
-
- $looksRealSigned = $signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid
- $looksRealSigned = $looksRealSigned -and ($signature.SignatureType -eq [System.Management.Automation.SignatureType]::Authenticode)
- $looksRealSigned = $looksRealSigned -and ($signature.SignerCertificate.Issuer -match ".*Microsoft.*Redmond.*")
- $looksRealSigned = $looksRealSigned -and (-not ($signature.SignerCertificate.Issuer -match "Test"))
-
- if (-Not $looksRealSigned) {
- $strongNamed = Test-AssemblyStrongNamed($assembly)
- return $this.NewDiagnostic("Assembly not real signed: $assembly.`nStrong named: $strongNamed; CertificateIssuer: [$($signature.SignerCertificate.Issuer)]")
- }
-
- return @()
- }
-
- [Diagnostic[]] CheckBuildInstance([BuildInstance] $b) {
- return $b.BuildFiles() | foreach{$this.CheckIsSigned($_)}
- }
-
- [Diagnostic[]] CheckFullFrameworkBuildInstance([FullFrameworkBuildInstance] $b) {
- return $this.CheckBuildInstance($b)
- }
-
- [Diagnostic[]] CheckNetCoreBuildInstance([NetCoreBuildInstance] $b) {
- return $this.CheckBuildInstance($b)
- }
-}
-
-class NugetVersionChecker : Checker {
- [Diagnostic[]] CheckNugetPackage([NugetPackage] $n) {
- $packageNameRegex = "Microsoft\.Build\..*\d+\.\d+\.\d+.*\.nupkg"
- $packageName = [System.IO.Path]::GetFileName($n.Path)
-
- if (-Not ($packageName -match $packageNameRegex)) {
- return $this.NewDiagnostic("Package `"$packageName`" does not match regex `"$packageNameRegex`"")
- }
-
- return @()
- }
-}
-
-[String[]] $diagnostics = @()
-
-$layout = $null
-
-if ($CPVSDrop) {
- Log "Used `$CPVSDrop=$CPVSDrop"
- $layout = [Layout]::FromCPVSDrop($CPVSDrop)
-}
-else {
- Log "Running inside microbuild environment"
- $layout = [Layout]::FromMicrobuild()
-}
-
-Log $layout
-
-# $checkers = @([TestChecker]::new())
-$checkers = @(
- [FileChecker]::new(),
- [RealSignedChecker]::new(),
- [NugetVersionChecker]::new()
- )
-
-$checkers | foreach{$layout.Check($_)}
-
-$diagnosticCount = 0
-
-Log "Failed checks:"
-
-foreach ($checker in $checkers) {
- $diags = $checker.Diagnostics
-
- $diagnosticCount += $diags.Count
-
- $diags | foreach{Log "$($_.Type): $($_.Message)"}
-}
-
-if ($diagnosticCount -eq 0) {
- Log "No failed checks"
-}
-else {
- Throw "$diagnosticCount failed checks"
-}
diff --git a/eng/Signing.props b/eng/Signing.props
index 00e8367eb86..4679ce9708d 100644
--- a/eng/Signing.props
+++ b/eng/Signing.props
@@ -4,12 +4,13 @@
and thus this file is not populated to VisualStudioSetupInsertionPath -->
+
+
+
-
-
diff --git a/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs b/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs
new file mode 100644
index 00000000000..1ce5dff91d6
--- /dev/null
+++ b/src/Build.UnitTests/BackEnd/AppHostSupport_Tests.cs
@@ -0,0 +1,159 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Build.BackEnd;
+using Microsoft.Build.Internal;
+using Microsoft.Build.Shared;
+using Microsoft.Build.UnitTests;
+using Shouldly;
+using Xunit;
+using Xunit.Abstractions;
+
+#nullable disable
+
+namespace Microsoft.Build.Engine.UnitTests.BackEnd
+{
+ ///
+ /// Tests for MSBuild App Host support functionality.
+ /// Tests the DOTNET_ROOT environment variable handling for app host bootstrap.
+ ///
+ public sealed class AppHostSupport_Tests
+ {
+ private readonly ITestOutputHelper _output;
+
+ private readonly string _dotnetHostPath = NativeMethodsShared.IsWindows
+ ? @"C:\Program Files\dotnet\dotnet.exe"
+ : "/usr/share/dotnet/dotnet";
+
+ public AppHostSupport_Tests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void CreateDotnetRootEnvironmentOverrides_SetsDotnetRootFromHostPath()
+ {
+ var overrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(_dotnetHostPath);
+
+ overrides.ShouldNotBeNull();
+ overrides.ShouldContainKey("DOTNET_ROOT");
+
+ string expectedDotnetRoot = Path.GetDirectoryName(_dotnetHostPath);
+ overrides["DOTNET_ROOT"].ShouldBe(expectedDotnetRoot);
+ }
+
+ [Fact]
+ public void CreateDotnetRootEnvironmentOverrides_ClearsArchitectureSpecificVariables()
+ {
+ var overrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(_dotnetHostPath);
+
+ // Assert - architecture-specific variables should be set to null (to be cleared)
+ overrides.ShouldContainKey("DOTNET_ROOT_X64");
+ overrides["DOTNET_ROOT_X64"].ShouldBeNull();
+
+ overrides.ShouldContainKey("DOTNET_ROOT_X86");
+ overrides["DOTNET_ROOT_X86"].ShouldBeNull();
+
+ overrides.ShouldContainKey("DOTNET_ROOT_ARM64");
+ overrides["DOTNET_ROOT_ARM64"].ShouldBeNull();
+ }
+
+
+ [WindowsOnlyTheory]
+ [InlineData(@"C:\custom\sdk\dotnet.exe", @"C:\custom\sdk")]
+ [InlineData(@"D:\tools\dotnet\dotnet.exe", @"D:\tools\dotnet")]
+ public void CreateDotnetRootEnvironmentOverrides_HandlesVariousPaths_Windows(string hostPath, string expectedRoot)
+ {
+ var overrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(hostPath);
+
+ overrides["DOTNET_ROOT"].ShouldBe(expectedRoot);
+ }
+
+ [UnixOnlyTheory]
+ [InlineData("/usr/local/share/dotnet/dotnet", "/usr/local/share/dotnet")]
+ [InlineData("/home/user/.dotnet/dotnet", "/home/user/.dotnet")]
+ public void CreateDotnetRootEnvironmentOverrides_HandlesVariousPaths_Unix(string hostPath, string expectedRoot)
+ {
+ var overrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(hostPath);
+
+ overrides["DOTNET_ROOT"].ShouldBe(expectedRoot);
+ }
+
+ [Fact]
+ public void ClearBootstrapDotnetRootEnvironment_ClearsVariablesNotInOriginalEnvironment()
+ {
+ using (TestEnvironment env = TestEnvironment.Create(_output))
+ {
+ // Arrange - set DOTNET_ROOT variants that simulate app host bootstrap
+ env.SetEnvironmentVariable("DOTNET_ROOT", @"C:\TestDotnet");
+ env.SetEnvironmentVariable("DOTNET_ROOT_X64", @"C:\TestDotnetX64");
+
+ // Original environment does NOT have these variables
+ var originalEnvironment = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ DotnetHostEnvironmentHelper.ClearBootstrapDotnetRootEnvironment(originalEnvironment);
+
+ Environment.GetEnvironmentVariable("DOTNET_ROOT").ShouldBeNull();
+ Environment.GetEnvironmentVariable("DOTNET_ROOT_X64").ShouldBeNull();
+ }
+ }
+
+ [Fact]
+ public void ClearBootstrapDotnetRootEnvironment_PreservesVariablesInOriginalEnvironment()
+ {
+ using (TestEnvironment env = TestEnvironment.Create(_output))
+ {
+ // Arrange - set DOTNET_ROOT that was in the original environment
+ string originalValue = @"C:\OriginalDotnet";
+ env.SetEnvironmentVariable("DOTNET_ROOT", originalValue);
+
+ // Register other DOTNET_ROOT variants with TestEnvironment so cleanup works correctly.
+ // These will be cleared by the helper if not in originalEnvironment.
+ env.SetEnvironmentVariable("DOTNET_ROOT_X64", null);
+ env.SetEnvironmentVariable("DOTNET_ROOT_X86", null);
+ env.SetEnvironmentVariable("DOTNET_ROOT_ARM64", null);
+
+ // Original environment HAS DOTNET_ROOT
+ var originalEnvironment = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["DOTNET_ROOT"] = originalValue
+ };
+
+ DotnetHostEnvironmentHelper.ClearBootstrapDotnetRootEnvironment(originalEnvironment);
+
+ // Assert - DOTNET_ROOT should be preserved since it was in original environment
+ Environment.GetEnvironmentVariable("DOTNET_ROOT").ShouldBe(originalValue);
+ }
+ }
+
+ [Fact]
+ public void ClearBootstrapDotnetRootEnvironment_HandlesMixedScenario()
+ {
+ using (TestEnvironment env = TestEnvironment.Create(_output))
+ {
+ string originalDotnetRoot = @"C:\OriginalDotnet";
+ string bootstrapX64 = @"C:\BootstrapX64";
+
+ env.SetEnvironmentVariable("DOTNET_ROOT", originalDotnetRoot);
+ env.SetEnvironmentVariable("DOTNET_ROOT_X64", bootstrapX64);
+ env.SetEnvironmentVariable("DOTNET_ROOT_X86", @"C:\BootstrapX86");
+
+ // Original environment has DOTNET_ROOT but not the architecture-specific ones
+ var originalEnvironment = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["DOTNET_ROOT"] = originalDotnetRoot
+ };
+
+ DotnetHostEnvironmentHelper.ClearBootstrapDotnetRootEnvironment(originalEnvironment);
+
+ Environment.GetEnvironmentVariable("DOTNET_ROOT").ShouldBe(originalDotnetRoot); // Preserved
+ Environment.GetEnvironmentVariable("DOTNET_ROOT_X64").ShouldBeNull(); // Cleared
+ Environment.GetEnvironmentVariable("DOTNET_ROOT_X86").ShouldBeNull(); // Cleared
+ Environment.GetEnvironmentVariable("DOTNET_ROOT_ARM64").ShouldBeNull(); // Was already null
+ }
+ }
+ }
+}
diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs
index 37ed394b4cd..acd78200020 100644
--- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs
@@ -412,9 +412,10 @@ public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool
try
{
- // Setup BuildEnvironment based on test scenario
- // needsFallback = true: Mode = Standalone && RunningInMSBuildExe = false (API/dotnet CLI)
- // needsFallback = false: Mode = Standalone && RunningInMSBuildExe = true (MSBuild.exe direct usage)
+ // Setup BuildEnvironment based on test scenario.
+ // The needsFallback logic is: Mode == Standalone && !RunningInMSBuildExe
+ // RunningInMSBuildExe is true only when the actual process is MSBuild.exe (app host).
+ // In test environment, process is testhost.exe/dotnet.exe, so RunningInMSBuildExe = false.
// Note: We use Standalone mode for both cases to avoid VisualStudio mode requiring VisualStudioInstallRootDirectory
BuildEnvironmentMode mode = BuildEnvironmentMode.Standalone;
bool runningInMSBuildExe = !needsFallback;
@@ -486,10 +487,11 @@ public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool
}
else
{
- // Should throw InvalidProjectFileException because:
- // 1. needsFallback = false → no fallback, uses Assembly.Load directly
- // 2. Assembly.Load fails on invalid assembly
- // 3. No fallback → exception propagates
+ // Note: This test scenario may not accurately test the no-fallback path in actual code
+ // because in test environment, RunningInMSBuildExe will be false (process is testhost/dotnet).
+ // We override RunningInMSBuildExe=true via ResetInstance_ForUnitTestsOnly to simulate the
+ // MSBuild.exe app host scenario. The test validates that Assembly.Load (no fallback) would
+ // fail on an invalid assembly. In practice, this path is only taken when running MSBuild.exe.
var exception = Should.Throw(() =>
loader.LoadAllResolvers(new MockElementLocation("file")));
diff --git a/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs b/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs
index bc5f79f7ac9..95572078dff 100644
--- a/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs
+++ b/src/Build.UnitTests/BuildEnvironmentHelper_Tests.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;
@@ -14,23 +15,21 @@ namespace Microsoft.Build.Engine.UnitTests
{
public class BuildEnvironmentHelper_Tests
{
-#if USE_MSBUILD_DLL_EXTN
- private const string MSBuildExeName = "MSBuild.dll";
-#else
- private const string MSBuildExeName = "MSBuild.exe";
-#endif
-
[Fact]
public void GetExecutablePath()
{
var msbuildPath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath);
- string expectedMSBuildPath = Path.Combine(msbuildPath, MSBuildExeName).ToLowerInvariant();
+ string expectedMSBuildPath = Path.Combine(msbuildPath, Constants.MSBuildExecutableName).ToLowerInvariant();
string configFilePath = BuildEnvironmentHelper.Instance.CurrentMSBuildConfigurationFile.ToLowerInvariant();
string toolsDirectoryPath = BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory.ToLowerInvariant();
string actualMSBuildPath = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath.ToLowerInvariant();
-
+#if NETFRAMEWORK
configFilePath.ShouldBe($"{actualMSBuildPath}.config");
+#else
+ // Even after app host introduction we still use MSBuild.dll.config as a source of tool paths.
+ configFilePath.ShouldBe($"{Path.GetDirectoryName(actualMSBuildPath)}{Path.DirectorySeparatorChar}{Constants.MSBuildAssemblyName.ToLowerInvariant()}.config");
+#endif
actualMSBuildPath.ShouldBe(expectedMSBuildPath);
Path.GetDirectoryName(expectedMSBuildPath).ShouldBe(toolsDirectoryPath);
BuildEnvironmentHelper.Instance.Mode.ShouldBe(BuildEnvironmentMode.Standalone);
@@ -39,11 +38,16 @@ public void GetExecutablePath()
[Fact]
public void FindBuildEnvironmentByEnvironmentVariable()
{
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName))
{
var path = env.BuildDirectory;
- var msBuildPath = Path.Combine(path, MSBuildExeName);
- var msBuildConfig = Path.Combine(path, $"{MSBuildExeName}.config");
+ var msBuildPath = Path.Combine(path, Constants.MSBuildExecutableName);
+ var msBuildConfig = Path.Combine(path,
+#if NET
+ "MSBuild.dll.config");
+#else
+ "MSBuild.exe.config");
+#endif
env.WithEnvironment("MSBUILD_EXE_PATH", env.MSBuildExePath);
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(ReturnNull, ReturnNull, ReturnNull, env.VsInstanceMock, env.EnvironmentMock, () => false);
@@ -69,8 +73,8 @@ public void FindVisualStudioEnvironmentByEnvironmentVariable()
{
var msbuildBinDirectory = env.BuildDirectory;
- var msBuildPath = Path.Combine(msbuildBinDirectory, MSBuildExeName);
- var msBuildConfig = Path.Combine(msbuildBinDirectory, $"{MSBuildExeName}.config");
+ var msBuildPath = Path.Combine(msbuildBinDirectory, Constants.MSBuildExecutableName);
+ var msBuildConfig = Path.Combine(msbuildBinDirectory, $"{Constants.MSBuildExecutableName}.config");
var vsMSBuildDirectory = Path.Combine(env.TempFolderRoot, "MSBuild");
env.WithEnvironment("MSBUILD_EXE_PATH", msBuildPath);
@@ -108,7 +112,7 @@ public void FindBuildEnvironmentFromCommandLineVisualStudio()
public void FindBuildEnvironmentFromCommandLineStandalone()
{
// Path will not be under a Visual Studio install like path.
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName))
{
// All we know about is path to msbuild.exe as the command-line arg[0]
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(() => env.MSBuildExePath, ReturnNull, ReturnNull, env.VsInstanceMock, env.EnvironmentMock, () => false);
@@ -141,7 +145,7 @@ public void FindBuildEnvironmentFromRunningProcessVisualStudio()
public void FindBuildEnvironmentFromRunningProcessStandalone()
{
// Path will not be under a Visual Studio install like path.
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName))
{
// All we know about is path to msbuild.exe as the current process
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(ReturnNull, () => env.MSBuildExePath, ReturnNull, env.VsInstanceMock, env.EnvironmentMock, () => false);
@@ -157,8 +161,8 @@ public void FindBuildEnvironmentFromRunningProcessStandalone()
[Fact]
public void FindBuildEnvironmentFromExecutingAssemblyAsDll()
{
- // Ensure the correct file is found (.dll not .exe)
- using (var env = new EmptyStandaloneEnviroment("MSBuild.dll"))
+ // Ensure the correct file is found
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName))
{
// All we know about is path to msbuild.exe as the current process
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(ReturnNull, () => env.MSBuildExePath, ReturnNull, env.VsInstanceMock, env.EnvironmentMock, () => false);
@@ -174,13 +178,13 @@ public void FindBuildEnvironmentFromExecutingAssemblyAsDll()
[Fact]
public void FindBuildEnvironmentFromAppContextDirectory()
{
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName))
{
// Only the app base directory will be available
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(ReturnNull, ReturnNull, () => env.BuildDirectory, env.VsInstanceMock, env.EnvironmentMock, () => false);
- // Make sure we get the right MSBuild entry point. On .NET Core this will be MSBuild.dll, otherwise MSBuild.exe
- Path.GetFileName(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath).ShouldBe(MSBuildExeName);
+ // Make sure we get the right MSBuild entry point.
+ Path.GetFileName(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath).ShouldBe(Constants.MSBuildExecutableName);
BuildEnvironmentHelper.Instance.MSBuildToolsDirectory32.ShouldBe(env.BuildDirectory);
BuildEnvironmentHelper.Instance.MSBuildToolsDirectory64.ShouldBe(env.BuildDirectory);
@@ -354,9 +358,9 @@ public void BuildEnvironmentFindsAmd64()
[WindowsOnlyFact]
public void BuildEnvironmentFindsAmd64RunningInAmd64NoVS()
{
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName, writeFakeFiles: true, includeAmd64Folder: true))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName, writeFakeFiles: true, includeAmd64Folder: true))
{
- var msBuild64Exe = Path.Combine(env.BuildDirectory, "amd64", MSBuildExeName);
+ var msBuild64Exe = Path.Combine(env.BuildDirectory, "amd64", Constants.MSBuildExecutableName);
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(() => msBuild64Exe, ReturnNull, ReturnNull,
env.VsInstanceMock, env.EnvironmentMock, () => false);
@@ -371,7 +375,7 @@ public void BuildEnvironmentFindsAmd64RunningInAmd64NoVS()
[ActiveIssue("https://github.com/dotnet/msbuild/issues/7552", TargetFrameworkMonikers.Any)]
public void BuildEnvironmentFindsAmd64NoVS()
{
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName, writeFakeFiles: true, includeAmd64Folder: true))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName, writeFakeFiles: true, includeAmd64Folder: true))
{
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(() => env.MSBuildExePath, ReturnNull,
ReturnNull, env.VsInstanceMock, env.EnvironmentMock, () => false);
@@ -400,7 +404,7 @@ public void BuildEnvironmentFindsAmd64RunningInAmd64()
[Fact]
public void BuildEnvironmentNoneWhenNotAvailable()
{
- using (var env = new EmptyStandaloneEnviroment(MSBuildExeName))
+ using (var env = new EmptyStandaloneEnviroment(Constants.MSBuildExecutableName))
{
var entryProcess = Path.Combine(Path.GetTempPath(), "foo.exe");
BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(() => entryProcess, ReturnNull, ReturnNull,
@@ -460,9 +464,10 @@ private sealed class EmptyVSEnviroment : EmptyStandaloneEnviroment
public string BuildDirectory64 { get; }
- public string MSBuildExePath64 => Path.Combine(BuildDirectory64, MSBuildExeName);
+ public string MSBuildExePath64 => Path.Combine(BuildDirectory64, Constants.MSBuildExecutableName);
- public EmptyVSEnviroment(string toolsVersion = MSBuildConstants.CurrentToolsVersion) : base("MSBuild.exe", false)
+ public EmptyVSEnviroment(string toolsVersion = MSBuildConstants.CurrentToolsVersion)
+ : base(Constants.MSBuildExecutableName, false)
{
try
{
@@ -501,9 +506,9 @@ private class EmptyStandaloneEnviroment : IDisposable
public string BuildDirectory { get; protected set; }
- public string MSBuildExeName { get; }
+ public string MSBuildExecutableName { get; }
- public string MSBuildExePath => Path.Combine(BuildDirectory, MSBuildExeName);
+ public string MSBuildExePath => Path.Combine(BuildDirectory, Constants.MSBuildExecutableName);
private readonly Dictionary _mockEnvironment = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -513,7 +518,7 @@ public EmptyStandaloneEnviroment(string msBuildExeName, bool writeFakeFiles = tr
{
try
{
- MSBuildExeName = msBuildExeName;
+ MSBuildExecutableName = msBuildExeName;
TempFolderRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
BuildDirectory = Path.Combine(TempFolderRoot, "MSBuild");
diff --git a/src/Build.UnitTests/Definition/ToolsetReader_Tests.cs b/src/Build.UnitTests/Definition/ToolsetReader_Tests.cs
index ab4962f0bc8..03be7c5c120 100644
--- a/src/Build.UnitTests/Definition/ToolsetReader_Tests.cs
+++ b/src/Build.UnitTests/Definition/ToolsetReader_Tests.cs
@@ -21,6 +21,7 @@
using InvalidToolsetDefinitionException = Microsoft.Build.Exceptions.InvalidToolsetDefinitionException;
using InternalUtilities = Microsoft.Build.Internal.Utilities;
using Xunit;
+using Microsoft.Build.Framework;
#nullable disable
diff --git a/src/Build.UnitTests/Definition/ToolsetRegistryReader_Tests.cs b/src/Build.UnitTests/Definition/ToolsetRegistryReader_Tests.cs
index f43fb379395..2c66a841a16 100644
--- a/src/Build.UnitTests/Definition/ToolsetRegistryReader_Tests.cs
+++ b/src/Build.UnitTests/Definition/ToolsetRegistryReader_Tests.cs
@@ -9,6 +9,7 @@
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
+using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Win32;
using Constants = Microsoft.Build.Framework.Constants;
diff --git a/src/Build.UnitTests/EscapingInProjects_Tests.cs b/src/Build.UnitTests/EscapingInProjects_Tests.cs
index 2f2931fc8e3..d517bc50c07 100644
--- a/src/Build.UnitTests/EscapingInProjects_Tests.cs
+++ b/src/Build.UnitTests/EscapingInProjects_Tests.cs
@@ -110,16 +110,16 @@ public void SemicolonInPropertyPassedIntoStringParam()
public void SemicolonInPropertyPassedIntoStringParam_UsingTaskHost()
{
MockLogger logger = Helpers.BuildProjectWithNewOMExpectSuccess(@"
-
-
-
- abc %3b def %3b ghi
-
-
-
-
-
- ", logger: new MockLogger(_output));
+
+
+
+ abc %3b def %3b ghi
+
+
+
+
+
+ ", logger: new MockLogger(_output));
logger.AssertLogContains("Property value is 'abc ; def ; ghi'");
}
diff --git a/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs b/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs
index 058bca545f0..70a8d38733d 100644
--- a/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs
+++ b/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
@@ -15,8 +16,6 @@ namespace Microsoft.Build.Engine.UnitTests
{
public class NetTaskHost_E2E_Tests
{
- private const string LatestDotNetCoreForMSBuild = "net10.0";
-
private static string AssemblyLocation { get; } = Path.Combine(Path.GetDirectoryName(typeof(NetTaskHost_E2E_Tests).Assembly.Location) ?? AppContext.BaseDirectory);
private static string TestAssetsRootPath { get; } = Path.Combine(AssemblyLocation, "TestAssets");
@@ -29,8 +28,10 @@ public NetTaskHost_E2E_Tests(ITestOutputHelper output)
}
[WindowsFullFrameworkOnlyFact]
- public void NetTaskHostTest()
+ public void NetTaskHostTest_FallbackToDotnet()
{
+ // This test verifies the fallback behavior when app host is not used.
+ // When DOTNET_HOST_PATH points to system dotnet, it uses dotnet.exe + MSBuild.dll.
using TestEnvironment env = TestEnvironment.Create(_output);
var dotnetPath = env.GetEnvironmentVariable("DOTNET_ROOT");
@@ -47,7 +48,7 @@ public void NetTaskHostTest()
testTaskOutput.ShouldContain($"The task is executed in process: dotnet");
testTaskOutput.ShouldContain($"Process path: {dotnetPath}", customMessage: testTaskOutput);
- var customTaskAssemblyLocation = Path.GetFullPath(Path.Combine(AssemblyLocation, "..", LatestDotNetCoreForMSBuild, "ExampleTask.dll"));
+ var customTaskAssemblyLocation = Path.GetFullPath(Path.Combine(AssemblyLocation, "..", RunnerUtilities.LatestDotNetCoreForMSBuild, "ExampleTask.dll"));
var resource = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(
"TaskAssemblyLocationMismatch",
@@ -57,6 +58,51 @@ public void NetTaskHostTest()
testTaskOutput.ShouldNotContain(resource);
}
+ [WindowsFullFrameworkOnlyFact] // Verifies that when MSBuild.exe app host is available in the SDK, it is used instead of dotnet.exe + MSBuild.dll.
+ public void NetTaskHostTest_AppHostUsedWhenAvailable()
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+
+ // Set the .NET SDK's SDK resolver override to point to our bootstrap, which is guaranteed to have the apphost.
+ var coreDirectory = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
+ env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", coreDirectory);
+
+ string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTask", "TestNetTask.csproj");
+
+ string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild}", out bool successTestTask, outputHelper: _output);
+
+ successTestTask.ShouldBeTrue();
+
+ // When app host is used, the process name should be "MSBuild" not "dotnet"
+ testTaskOutput.ShouldContain("The task is executed in process: MSBuild");
+
+ // The process path should point to MSBuild.exe, not dotnet.exe
+ testTaskOutput.ShouldContain(Constants.MSBuildExecutableName, customMessage: "Expected MSBuild.exe app host to be used");
+ testTaskOutput.ShouldNotContain("Process path: " + Path.Combine(env.GetEnvironmentVariable("DOTNET_ROOT") ?? "", "dotnet.exe"));
+ }
+
+ [WindowsFullFrameworkOnlyFact] // Verifies that when using the app host, DOTNET_ROOT is properly set for child processes to find the runtime.
+ public void NetTaskHostTest_AppHostSetsDotnetRoot()
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+
+ // Clear DOTNET_ROOT to ensure app host sets it
+ env.SetEnvironmentVariable("DOTNET_ROOT", null);
+ env.SetEnvironmentVariable("DOTNET_ROOT_X64", null);
+ env.SetEnvironmentVariable("DOTNET_ROOT_X86", null);
+ env.SetEnvironmentVariable("DOTNET_ROOT_ARM64", null);
+
+ string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTask", "TestNetTask.csproj");
+
+ string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild}", out bool successTestTask);
+
+ _output.WriteLine(testTaskOutput);
+
+ // The build should succeed - this proves DOTNET_ROOT was properly set for the task host
+ // to find the runtime, even though we cleared it from the parent environment
+ successTestTask.ShouldBeTrue(customMessage: "Build should succeed with app host setting DOTNET_ROOT");
+ }
+
[WindowsFullFrameworkOnlyFact]
public void NetTaskHost_CorrectPathsEscapingTest()
{
@@ -75,8 +121,8 @@ public void NetTaskHost_CorrectPathsEscapingTest()
// Explicitly validate escaping for paths with spaces
var msBuildDllPathMatch = System.Text.RegularExpressions.Regex.Match(
- testTaskOutput,
- @"Arg\[0\]:\s*(.+)",
+ testTaskOutput,
+ @"Arg\[0\]:\s*(.+)",
System.Text.RegularExpressions.RegexOptions.CultureInvariant);
msBuildDllPathMatch.Success.ShouldBeTrue();
@@ -90,10 +136,10 @@ public void NetTaskHost_CorrectPathsEscapingTest()
{
// Check that later path segments don't appear as separate command line arguments
var arg1Match = System.Text.RegularExpressions.Regex.Match(
- testTaskOutput,
- @"Arg\[1\]:\s*(.+)",
+ testTaskOutput,
+ @"Arg\[1\]:\s*(.+)",
System.Text.RegularExpressions.RegexOptions.CultureInvariant);
-
+
if (arg1Match.Success)
{
string arg1Value = arg1Match.Groups[1].Value.Trim();
@@ -108,7 +154,7 @@ public void NetTaskHost_CorrectPathsEscapingTest()
testTaskOutput,
@"Command line:\s*(.+)",
System.Text.RegularExpressions.RegexOptions.CultureInvariant);
-
+
if (cmdLineMatch.Success)
{
string cmdLine = cmdLineMatch.Groups[1].Value.Trim();
@@ -123,7 +169,6 @@ public void MSBuildTaskInNetHostTest()
using TestEnvironment env = TestEnvironment.Create(_output);
string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestMSBuildTaskInNet", "TestMSBuildTaskInNet.csproj");
-
string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild}", out bool successTestTask);
if (!successTestTask)
@@ -142,12 +187,9 @@ public void NetTaskHost_CallbackIsRunningMultipleNodesTest()
env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1");
// Point dotnet resolution to the bootstrap layout so the .NET Core TaskHost
- // uses the locally-built MSBuild.dll (with callback support) instead of the system SDK.
- string bootstrapCorePath = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
- string bootstrapDotnet = Path.Combine(bootstrapCorePath, "dotnet.exe");
- env.SetEnvironmentVariable("DOTNET_HOST_PATH", bootstrapDotnet);
- env.SetEnvironmentVariable("DOTNET_ROOT", bootstrapCorePath);
- env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", bootstrapCorePath);
+ // uses the locally-built MSBuild.exe (with callback support) instead of the system SDK.
+ var coreDirectory = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
+ env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", coreDirectory);
string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTaskCallback", "TestNetTaskCallback.csproj");
@@ -168,13 +210,9 @@ public void NetTaskHost_CallbackRequestCoresTest()
using TestEnvironment env = TestEnvironment.Create(_output);
env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1");
- // Point dotnet resolution to the bootstrap layout so the .NET Core TaskHost
- // uses the locally-built MSBuild.dll (with callback support) instead of the system SDK.
- string bootstrapCorePath = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
- string bootstrapDotnet = Path.Combine(bootstrapCorePath, "dotnet.exe");
- env.SetEnvironmentVariable("DOTNET_HOST_PATH", bootstrapDotnet);
- env.SetEnvironmentVariable("DOTNET_ROOT", bootstrapCorePath);
- env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", bootstrapCorePath);
+ // Set the .NET SDK's SDK resolver override to point to our bootstrap, which is guaranteed to have the apphost.
+ var coreDirectory = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
+ env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", coreDirectory);
string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTaskResourceCallback", "TestNetTaskResourceCallback.csproj");
@@ -189,8 +227,8 @@ public void NetTaskHost_CallbackRequestCoresTest()
testTaskOutput.ShouldContain("CallbackResult: RequestCores(2) =");
}
- [WindowsFullFrameworkOnlyFact]
- public void NetTaskWithImplicitHostParamsTest()
+ [WindowsFullFrameworkOnlyFact] // This test verifies the fallback behavior with implicit host parameters.
+ public void NetTaskWithImplicitHostParamsTest_FallbackToDotnet()
{
using TestEnvironment env = TestEnvironment.Create(_output);
var dotnetPath = env.GetEnvironmentVariable("DOTNET_ROOT");
@@ -217,5 +255,24 @@ public void NetTaskWithImplicitHostParamsTest()
// Output from the task where neither TaskHost nor Runtime were specified
testTaskOutput.ShouldContain("Found item: Banana");
}
+
+ [WindowsFullFrameworkOnlyFact] // This test verifies app host behavior with implicit host parameters.
+ public void NetTaskWithImplicitHostParamsTest_AppHost()
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+ var coreDirectory = Path.Combine(RunnerUtilities.BootstrapRootPath, "core");
+ env.SetEnvironmentVariable("PATH", $"{coreDirectory}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}");
+ env.SetEnvironmentVariable("DOTNET_ROOT", coreDirectory);
+
+ string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTaskWithImplicitParams", "TestNetTaskWithImplicitParams.csproj");
+ string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild}", out bool successTestTask, outputHelper: _output);
+
+ _output.WriteLine(testTaskOutput);
+
+ successTestTask.ShouldBeTrue();
+
+ testTaskOutput.ShouldContain("The task is executed in process: MSBuild");
+ testTaskOutput.ShouldContain("/nodereuse:True");
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs b/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
index 9f0c43fe6e6..f7e7bd70711 100644
--- a/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
+++ b/src/Build.UnitTests/ProjectCache/ProjectCacheTests.cs
@@ -14,7 +14,6 @@
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
-using Microsoft.Build.Internal;
using Microsoft.Build.ProjectCache;
using Microsoft.Build.Shared;
using Microsoft.Build.Unittest;
diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs
index 0404585bd13..34f1712c12b 100644
--- a/src/Build/BackEnd/Client/MSBuildClient.cs
+++ b/src/Build/BackEnd/Client/MSBuildClient.cs
@@ -459,7 +459,7 @@ private bool TryLaunchServer()
];
NodeLauncher nodeLauncher = new NodeLauncher();
CommunicationsUtilities.Trace("Starting Server...");
- using Process msbuildProcess = nodeLauncher.Start(_msbuildLocation, string.Join(" ", msBuildServerOptions), nodeId: 0);
+ using Process msbuildProcess = nodeLauncher.Start(new NodeLaunchData(_msbuildLocation, string.Join(" ", msBuildServerOptions)), nodeId: 0);
CommunicationsUtilities.Trace("Server started with PID: {0}", msbuildProcess?.Id);
}
catch (Exception ex)
diff --git a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs
index f526f7ad260..5016a20ecb4 100644
--- a/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs
+++ b/src/Build/BackEnd/Components/Communications/DetouredNodeLauncher.cs
@@ -26,8 +26,6 @@ internal sealed class DetouredNodeLauncher : INodeLauncher, IBuildComponent
{
private readonly List _sandboxedProcesses = new();
- private readonly BuildParameters.IBuildParameters _environmentVariables = CreateEnvironmentVariables();
-
private IFileAccessManager _fileAccessManager;
public static IBuildComponent CreateComponent(BuildComponentType type)
@@ -54,27 +52,27 @@ public void ShutdownComponent()
}
///
- /// Creates a new MSBuild process
+ /// Creates a new MSBuild process using the specified launch configuration.
///
- public Process Start(string msbuildLocation, string commandLineArgs, int nodeId)
+ public Process Start(NodeLaunchData launchData, int nodeId)
{
// Should always have been set already.
- ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation));
+ ErrorUtilities.VerifyThrowInternalLength(launchData.MSBuildLocation, nameof(launchData.MSBuildLocation));
ErrorUtilities.VerifyThrowInternalNull(_fileAccessManager, nameof(_fileAccessManager));
- if (!FileSystems.Default.FileExists(msbuildLocation))
+ if (!FileSystems.Default.FileExists(launchData.MSBuildLocation))
{
- throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation));
+ throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", launchData.MSBuildLocation));
}
// Repeat the executable name as the first token of the command line because the command line
// parser logic expects it and will otherwise skip the first argument
- commandLineArgs = $"\"{msbuildLocation}\" {commandLineArgs}";
+ var commandLineArgs = $"\"{launchData.MSBuildLocation}\" {launchData.CommandLineArgs}";
- CommunicationsUtilities.Trace("Launching node from {0}", msbuildLocation);
+ CommunicationsUtilities.Trace("Launching node from {0}", launchData.MSBuildLocation);
- string exeName = msbuildLocation;
+ string exeName = launchData.MSBuildLocation;
#if RUNTIME_TYPE_NETCORE
// Run the child process with the same host as the currently-running process.
@@ -95,7 +93,7 @@ public Process Start(string msbuildLocation, string commandLineArgs, int nodeId)
PipDescription = "MSBuild",
PipSemiStableHash = 0,
Arguments = commandLineArgs,
- EnvironmentVariables = _environmentVariables,
+ EnvironmentVariables = CreateEnvironmentVariables(launchData.EnvironmentOverrides),
MaxLengthInMemory = 0, // Don't buffer any output
};
@@ -143,7 +141,10 @@ public Process Start(string msbuildLocation, string commandLineArgs, int nodeId)
return Process.GetProcessById(sp.ProcessId);
}
- private static BuildParameters.IBuildParameters CreateEnvironmentVariables()
+ ///
+ /// Creates environment variables with optional overrides for app host bootstrap.
+ ///
+ private static BuildParameters.IBuildParameters CreateEnvironmentVariables(IDictionary environmentOverrides)
{
var envVars = new Dictionary();
foreach (DictionaryEntry baseVar in Environment.GetEnvironmentVariables())
@@ -151,6 +152,8 @@ private static BuildParameters.IBuildParameters CreateEnvironmentVariables()
envVars.Add((string)baseVar.Key, (string)baseVar.Value);
}
+ DotnetHostEnvironmentHelper.ApplyEnvironmentOverrides(envVars, environmentOverrides);
+
return BuildParameters.GetFactory().PopulateFromDictionary(envVars);
}
diff --git a/src/Build/BackEnd/Components/Communications/INodeLauncher.cs b/src/Build/BackEnd/Components/Communications/INodeLauncher.cs
index e1e7414c8fd..74fe28309fc 100644
--- a/src/Build/BackEnd/Components/Communications/INodeLauncher.cs
+++ b/src/Build/BackEnd/Components/Communications/INodeLauncher.cs
@@ -1,35 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Diagnostics;
+
using Microsoft.Build.Internal;
namespace Microsoft.Build.BackEnd
{
///
- /// Represents the configuration data needed to launch a node process.
+ /// Represents the configuration data needed to launch a node process.
///
- ///
- /// The path to the MSBuild binary to launch (e.g., MSBuild.exe, MSBuild.dll or MSBuildTaskHost.exe).
- /// If is passed, will
- /// be used as the default MSBuild location.
- ///
+ /// The path to the executable to launch (e.g., MSBuild.exe or dotnet.exe).
/// The command line arguments to pass to the executable.
/// The handshake data used to establish communication with the node process.
- ///
- /// if the dotnet.exe should be used to launch the MSBuild assembly;
- /// if the MSBuild executable should be launched directly.
+ ///
+ /// Optional environment variable overrides for the process.
+ /// A non-null value sets or overrides that variable. A null value removes the variable
+ /// from the child process environment - this is used to clear architecture-specific
+ /// DOTNET_ROOT variants (e.g., DOTNET_ROOT_X64) that would otherwise take precedence
+ /// over DOTNET_ROOT when launching an app host.
///
internal readonly record struct NodeLaunchData(
- string? MSBuildLocation,
+ string MSBuildLocation,
string CommandLineArgs,
- Handshake Handshake,
- bool UsingDotNetExe = false)
- {
- }
+ Handshake? Handshake = null,
+ IDictionary? EnvironmentOverrides = null);
internal interface INodeLauncher
{
- Process Start(string msbuildLocation, string commandLineArgs, int nodeId);
+ ///
+ /// Creates a new MSBuild process using the specified launch configuration.
+ ///
+ /// The configuration data for launching the node process.
+ /// The unique identifier for the node being launched.
+ /// The started process.
+ Process Start(NodeLaunchData launchData, int nodeId);
}
}
diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs
index 744784dc13f..f746924d6c1 100644
--- a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs
@@ -2,9 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+
+#if RUNTIME_TYPE_NETCORE
+using System.IO;
+#endif
+
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Text;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
@@ -33,149 +41,189 @@ public void ShutdownComponent()
}
///
- /// Creates a new MSBuild process
+ /// Creates a new MSBuild process using the specified launch configuration.
///
- public Process Start(string msbuildLocation, string commandLineArgs, int nodeId)
+ public Process Start(NodeLaunchData launchData, int nodeId)
{
// Disable MSBuild server for a child process.
// In case of starting msbuild server it prevents an infinite recursion. In case of starting msbuild node we also do not want this variable to be set.
- return DisableMSBuildServer(() => StartInternal(msbuildLocation, commandLineArgs));
+ return DisableMSBuildServer(() => StartInternal(launchData));
}
///
/// Creates new MSBuild or dotnet process.
///
- private Process StartInternal(string msbuildLocation, string commandLineArgs)
+ private Process StartInternal(NodeLaunchData nodeLaunchData)
{
- // Should always have been set already.
- ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation));
+ ValidateMSBuildLocation(nodeLaunchData.MSBuildLocation);
- if (!FileSystems.Default.FileExists(msbuildLocation))
- {
- throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation));
- }
+ string exeName = ResolveExecutableName(nodeLaunchData.MSBuildLocation, out bool isNativeAppHost);
+ uint creationFlags = GetCreationFlags(out bool redirectStreams);
- // Repeat the executable name as the first token of the command line because the command line
- // parser logic expects it and will otherwise skip the first argument
- commandLineArgs = $"\"{msbuildLocation}\" {commandLineArgs}";
+ CommunicationsUtilities.Trace("Launching node from {0}", nodeLaunchData.MSBuildLocation);
- BackendNativeMethods.STARTUP_INFO startInfo = new();
- startInfo.cb = Marshal.SizeOf();
+ return NativeMethodsShared.IsWindows
+ ? StartProcessWindows(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost)
+ : StartProcessUnix(nodeLaunchData, exeName, creationFlags, redirectStreams, isNativeAppHost);
- // Null out the process handles so that the parent process does not wait for the child process
- // to exit before it can exit.
- uint creationFlags = 0;
- if (Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout)
+ static void ValidateMSBuildLocation(string msbuildLocation)
{
- creationFlags = BackendNativeMethods.NORMALPRIORITYCLASS;
- }
+ // Should always have been set already.
+ ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation));
- if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW")))
- {
- if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout)
+ if (!FileSystems.Default.FileExists(msbuildLocation))
{
- // Redirect the streams of worker nodes so that this MSBuild.exe's
- // parent doesn't wait on idle worker nodes to close streams
- // after the build is complete.
- startInfo.hStdError = BackendNativeMethods.InvalidHandle;
- startInfo.hStdInput = BackendNativeMethods.InvalidHandle;
- startInfo.hStdOutput = BackendNativeMethods.InvalidHandle;
- startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES;
- creationFlags |= BackendNativeMethods.CREATENOWINDOW;
+ throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation));
}
}
- else
- {
- creationFlags |= BackendNativeMethods.CREATE_NEW_CONSOLE;
- }
-
- CommunicationsUtilities.Trace("Launching node from {0}", msbuildLocation);
+ }
- string exeName = msbuildLocation;
+ private string ResolveExecutableName(string msbuildLocation, out bool isNativeAppHost)
+ {
+ isNativeAppHost = false;
#if RUNTIME_TYPE_NETCORE
- // Run the child process with the same host as the currently-running process.
- exeName = CurrentHost.GetCurrentHost();
+ // If msbuildLocation is a native app host (e.g., MSBuild.exe on Windows, MSBuild on Linux), run it directly.
+ // Otherwise, use dotnet.exe to run the managed assembly (e.g., MSBuild.dll).
+ string fileName = Path.GetFileName(msbuildLocation);
+ isNativeAppHost = fileName.Equals(Constants.MSBuildExecutableName, StringComparison.OrdinalIgnoreCase);
+ if (!isNativeAppHost)
+ {
+ return CurrentHost.GetCurrentHost();
+ }
#endif
+ return msbuildLocation;
+ }
+
+ private uint GetCreationFlags(out bool redirectStreams)
+ {
+ bool ensureStdOut = Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout;
+ bool showNodeWindow = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW"));
+
+ redirectStreams = !ensureStdOut && !showNodeWindow;
- if (!NativeMethodsShared.IsWindows)
+ uint flags = (ensureStdOut, showNodeWindow) switch
{
- ProcessStartInfo processStartInfo = new ProcessStartInfo();
- processStartInfo.FileName = exeName;
- processStartInfo.Arguments = commandLineArgs;
- if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout)
- {
- // Redirect the streams of worker nodes so that this MSBuild.exe's
- // parent doesn't wait on idle worker nodes to close streams
- // after the build is complete.
- processStartInfo.RedirectStandardInput = true;
- processStartInfo.RedirectStandardOutput = true;
- processStartInfo.RedirectStandardError = true;
- processStartInfo.CreateNoWindow = (creationFlags | BackendNativeMethods.CREATENOWINDOW) == BackendNativeMethods.CREATENOWINDOW;
- }
- processStartInfo.UseShellExecute = false;
+ (true, true) => BackendNativeMethods.NORMALPRIORITYCLASS | BackendNativeMethods.CREATE_NEW_CONSOLE,
+ (true, false) => BackendNativeMethods.NORMALPRIORITYCLASS,
+ (false, true) => BackendNativeMethods.CREATE_NEW_CONSOLE,
+ (false, false) => BackendNativeMethods.CREATENOWINDOW,
+ };
- Process process;
- try
- {
- process = Process.Start(processStartInfo);
- }
- catch (Exception ex)
- {
- CommunicationsUtilities.Trace(
- "Failed to launch node from {0}. CommandLine: {1}" + Environment.NewLine + "{2}",
- msbuildLocation,
- commandLineArgs,
- ex.ToString());
+ return flags;
+ }
- throw new NodeFailedToLaunchException(ex);
- }
+ [UnsupportedOSPlatform("windows")]
+ private Process StartProcessUnix(NodeLaunchData nodeLaunchData, string exeName, uint creationFlags, bool redirectStreams, bool isNativeAppHost)
+ {
+ // Builds command line args for Unix Process.Start, which sets argv[0] from FileName
+ // automatically. We must not duplicate the executable name in Arguments for native
+ // app hosts. For dotnet-hosted launches, the assembly path must be included so dotnet
+ // knows which assembly to run.
+ string commandLineArgs = isNativeAppHost ? nodeLaunchData.CommandLineArgs : $"\"{nodeLaunchData.MSBuildLocation}\" {nodeLaunchData.CommandLineArgs}";
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = exeName,
+ Arguments = commandLineArgs,
+ UseShellExecute = false,
+ RedirectStandardInput = redirectStreams,
+ RedirectStandardOutput = redirectStreams,
+ RedirectStandardError = redirectStreams,
+ CreateNoWindow = redirectStreams && (creationFlags & BackendNativeMethods.CREATENOWINDOW) != 0,
+ };
+ DotnetHostEnvironmentHelper.ApplyEnvironmentOverrides(processStartInfo.Environment, nodeLaunchData.EnvironmentOverrides);
+
+ try
+ {
+ Process process = Process.Start(processStartInfo);
CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", process.Id, exeName);
return process;
}
- else
+ catch (Exception ex)
{
+ CommunicationsUtilities.Trace(
+ "Failed to launch node from {0}. CommandLine: {1}" + Environment.NewLine + "{2}",
+ nodeLaunchData.MSBuildLocation,
+ commandLineArgs,
+ ex.ToString());
+
+ throw new NodeFailedToLaunchException(ex);
+ }
+ }
+
+ [SupportedOSPlatform("windows")]
+ private static Process StartProcessWindows(NodeLaunchData nodeLaunchData, string exeName, uint creationFlags, bool redirectStreams, bool isNativeAppHost)
+ {
+ // Repeat the executable name as the first token of the command line because the command line
+ // parser logic expects it and will otherwise skip the first argument
+ string commandLineArgs = $"\"{nodeLaunchData.MSBuildLocation}\" {nodeLaunchData.CommandLineArgs}";
+
#if RUNTIME_TYPE_NETCORE
- // Repeat the executable name in the args to suit CreateProcess
+ if (!isNativeAppHost)
+ {
commandLineArgs = $"\"{exeName}\" {commandLineArgs}";
+ }
#endif
- BackendNativeMethods.PROCESS_INFORMATION processInfo = new();
- BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new();
- BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new();
- processSecurityAttributes.nLength = Marshal.SizeOf();
- threadSecurityAttributes.nLength = Marshal.SizeOf();
+ BackendNativeMethods.STARTUP_INFO startInfo = CreateStartupInfo(redirectStreams);
+ BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new() { nLength = Marshal.SizeOf() };
+ BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new() { nLength = Marshal.SizeOf() };
+ IntPtr environmentBlock = BuildEnvironmentBlock(nodeLaunchData.EnvironmentOverrides);
+
+ // When passing a Unicode environment block, we must set CREATE_UNICODE_ENVIRONMENT.
+ // Without this flag, CreateProcess interprets the block as ANSI, causing error 87.
+ uint effectiveCreationFlags = creationFlags;
+ if (environmentBlock != BackendNativeMethods.NullPtr)
+ {
+ effectiveCreationFlags |= BackendNativeMethods.CREATE_UNICODE_ENVIRONMENT;
+ }
+
+ try
+ {
bool result = BackendNativeMethods.CreateProcess(
- exeName,
- commandLineArgs,
- ref processSecurityAttributes,
- ref threadSecurityAttributes,
- false,
- creationFlags,
- BackendNativeMethods.NullPtr,
- null,
- ref startInfo,
- out processInfo);
+ exeName,
+ commandLineArgs,
+ ref processSecurityAttributes,
+ ref threadSecurityAttributes,
+ false,
+ effectiveCreationFlags,
+ environmentBlock,
+ null,
+ ref startInfo,
+ out BackendNativeMethods.PROCESS_INFORMATION processInfo);
if (!result)
{
- // Creating an instance of this exception calls GetLastWin32Error and also converts it to a user-friendly string.
- System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception();
+ var e = new System.ComponentModel.Win32Exception();
CommunicationsUtilities.Trace(
- "Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {2}",
- msbuildLocation,
- e.NativeErrorCode.ToString(CultureInfo.InvariantCulture),
- e.Message,
- commandLineArgs);
+ "Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {3}",
+ nodeLaunchData.MSBuildLocation,
+ e.NativeErrorCode.ToString(CultureInfo.InvariantCulture),
+ e.Message,
+ commandLineArgs);
throw new NodeFailedToLaunchException(e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), e.Message);
}
- int childProcessId = processInfo.dwProcessId;
+ CloseProcessHandles(processInfo);
+ CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", processInfo.dwProcessId, exeName);
+ return Process.GetProcessById(processInfo.dwProcessId);
+ }
+ finally
+ {
+ if (environmentBlock != BackendNativeMethods.NullPtr)
+ {
+ Marshal.FreeHGlobal(environmentBlock);
+ }
+ }
+
+ static void CloseProcessHandles(BackendNativeMethods.PROCESS_INFORMATION processInfo)
+ {
if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != NativeMethods.InvalidHandle)
{
NativeMethodsShared.CloseHandle(processInfo.hProcess);
@@ -185,10 +233,66 @@ private Process StartInternal(string msbuildLocation, string commandLineArgs)
{
NativeMethodsShared.CloseHandle(processInfo.hThread);
}
+ }
+ }
- CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", childProcessId, exeName);
- return Process.GetProcessById(childProcessId);
+ [SupportedOSPlatform("windows")]
+ private static BackendNativeMethods.STARTUP_INFO CreateStartupInfo(bool redirectStreams)
+ {
+ var startInfo = new BackendNativeMethods.STARTUP_INFO
+ {
+ cb = Marshal.SizeOf(),
+ };
+
+ if (redirectStreams)
+ {
+ startInfo.hStdError = BackendNativeMethods.InvalidHandle;
+ startInfo.hStdInput = BackendNativeMethods.InvalidHandle;
+ startInfo.hStdOutput = BackendNativeMethods.InvalidHandle;
+ startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES;
+ }
+
+ return startInfo;
+ }
+
+ ///
+ /// Builds a Windows environment block for CreateProcess.
+ ///
+ /// Environment variable overrides. Null values remove variables.
+ /// Pointer to environment block that must be freed with Marshal.FreeHGlobal, or BackendNativeMethods.NullPtr.
+ [SupportedOSPlatform("windows")]
+ private static IntPtr BuildEnvironmentBlock(IDictionary environmentOverrides)
+ {
+ if (environmentOverrides == null || environmentOverrides.Count == 0)
+ {
+ return BackendNativeMethods.NullPtr;
}
+
+ var environment = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (System.Collections.DictionaryEntry entry in Environment.GetEnvironmentVariables())
+ {
+ environment[(string)entry.Key] = (string)entry.Value;
+ }
+
+ DotnetHostEnvironmentHelper.ApplyEnvironmentOverrides(environment, environmentOverrides);
+
+ // Build the environment block: "key=value\0key=value\0\0"
+ // Windows CreateProcess requires the environment block to be sorted alphabetically by name (case-insensitive).
+ var sortedKeys = new List(environment.Keys);
+ sortedKeys.Sort(StringComparer.OrdinalIgnoreCase);
+
+ var sb = new StringBuilder();
+ foreach (string key in sortedKeys)
+ {
+ sb.Append(key);
+ sb.Append('=');
+ sb.Append(environment[key]);
+ sb.Append('\0');
+ }
+
+ sb.Append('\0');
+
+ return Marshal.StringToHGlobalUni(sb.ToString());
}
private static Process DisableMSBuildServer(Func func)
diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs
index ffe430eeb6b..02e998cfa1e 100644
--- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProc.cs
@@ -89,17 +89,21 @@ public IList CreateNodes(int nextNodeId, INodePacketFactory factory, F
}
ConcurrentBag nodes = new();
+ Handshake hostHandshake = new(CommunicationsUtilities.GetHandshakeOptions(taskHost: false, taskHostParameters: TaskHostParameters.Empty, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture(), nodeReuse: ComponentHost.BuildParameters.EnableNodeReuse, lowPriority: ComponentHost.BuildParameters.LowPriority));
// Start the new process. We pass in a node mode with a node number of 1, to indicate that we
// want to start up just a standard MSBuild out-of-proc node.
// Note: We need to always pass /nodeReuse to ensure the value for /nodeReuse from msbuild.rsp
// (next to msbuild.exe) is ignored.
- string commandLineArgs = $"/noautoresponse /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcNode)} /nodeReuse:{ComponentHost.BuildParameters.EnableNodeReuse.ToString().ToLower()} /low:{ComponentHost.BuildParameters.LowPriority.ToString().ToLower()}";
+ NodeLaunchData nodeLaunchData = new(
+ MSBuildLocation: null,
+ CommandLineArgs: $"/noautoresponse /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcNode)} /nodeReuse:{ComponentHost.BuildParameters.EnableNodeReuse.ToString().ToLower()} /low:{ComponentHost.BuildParameters.LowPriority.ToString().ToLower()}",
+ Handshake: hostHandshake,
+ EnvironmentOverrides: DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides());
CommunicationsUtilities.Trace("Starting to acquire {1} new or existing node(s) to establish nodes from ID {0} to {2}...", nextNodeId, numberOfNodesToCreate, nextNodeId + numberOfNodesToCreate - 1);
-
- Handshake hostHandshake = new(CommunicationsUtilities.GetHandshakeOptions(taskHost: false, taskHostParameters: TaskHostParameters.Empty, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture(), nodeReuse: ComponentHost.BuildParameters.EnableNodeReuse, lowPriority: ComponentHost.BuildParameters.LowPriority));
- IList nodeContexts = GetNodes(null, commandLineArgs, nextNodeId, factory, hostHandshake, NodeContextCreated, NodeContextTerminated, numberOfNodesToCreate);
+
+ IList nodeContexts = GetNodes(nodeLaunchData, nextNodeId, factory, NodeContextCreated, NodeContextTerminated, numberOfNodesToCreate);
if (nodeContexts.Count > 0)
{
diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
index ff6ecabbc91..9f701208dde 100644
--- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs
@@ -198,18 +198,23 @@ protected void ShutdownAllNodes(bool nodeReuse, NodeContextTerminateDelegate ter
}
///
- /// Finds or creates a child processes which can act as a node.
+ /// Finds or creates child processes which can act as nodes using the provided launch data.
///
+ /// The launch configuration containing executable path, arguments, and environment overrides.
+ /// The next node ID to use.
+ /// The packet factory for communication.
+ /// Callback when a node is created.
+ /// Callback when a node terminates.
+ /// Number of nodes to create.
protected IList GetNodes(
- string msbuildLocation,
- string commandLineArgs,
+ NodeLaunchData nodeLaunchData,
int nextNodeId,
INodePacketFactory factory,
- Handshake hostHandshake,
NodeContextCreatedDelegate createNode,
NodeContextTerminateDelegate terminateNode,
int numberOfNodesToCreate)
{
+ string commandLineArgs = nodeLaunchData.CommandLineArgs;
#if DEBUG
if (Execution.BuildManager.WaitForDebugger)
{
@@ -217,6 +222,7 @@ protected IList GetNodes(
}
#endif
+ var msbuildLocation = nodeLaunchData.MSBuildLocation;
if (String.IsNullOrEmpty(msbuildLocation))
{
msbuildLocation = _componentHost.BuildParameters.NodeExeLocation;
@@ -233,11 +239,11 @@ protected IList GetNodes(
}
}
- bool nodeReuseRequested = Handshake.IsHandshakeOptionEnabled(hostHandshake.HandshakeOptions, HandshakeOptions.NodeReuse);
-
+ bool nodeReuseRequested = Handshake.IsHandshakeOptionEnabled(nodeLaunchData.Handshake.HandshakeOptions, HandshakeOptions.NodeReuse);
+
// Extract the expected NodeMode from the command line arguments
NodeMode? expectedNodeMode = NodeModeHelper.ExtractFromCommandLine(commandLineArgs);
-
+
// Get all process of possible running node processes for reuse and put them into ConcurrentQueue.
// Processes from this queue will be concurrently consumed by TryReusePossibleRunningNodes while
// trying to connect to them and reuse them. When queue is empty, no process to reuse left
@@ -284,7 +290,9 @@ protected IList GetNodes(
});
if (!exceptions.IsEmpty)
{
- ErrorUtilities.ThrowInternalError("Cannot acquire required number of nodes.", new AggregateException(exceptions.ToArray()));
+ ErrorUtilities.ThrowInternalError(
+ $"Cannot acquire required number of nodes. MSBuildLocation: '{msbuildLocation}', CommandLineArgs: '{commandLineArgs}', NumberOfNodesToCreate: {numberOfNodesToCreate}, NextNodeId: {nextNodeId}.",
+ new AggregateException(exceptions.ToArray()));
}
return nodeContexts.ToList();
@@ -300,7 +308,7 @@ bool TryReuseAnyFromPossibleRunningNodes(int currentProcessId, int nodeId)
}
// Get the full context of this inspection so that we can always skip this process when we have the same taskhost context
- string nodeLookupKey = GetProcessesToIgnoreKey(hostHandshake, nodeToReuse.Id);
+ string nodeLookupKey = GetProcessesToIgnoreKey(nodeLaunchData.Handshake, nodeToReuse.Id);
if (_processesToIgnore.ContainsKey(nodeLookupKey))
{
continue;
@@ -310,7 +318,7 @@ bool TryReuseAnyFromPossibleRunningNodes(int currentProcessId, int nodeId)
_processesToIgnore.TryAdd(nodeLookupKey, default);
// Attempt to connect to each process in turn.
- Stream nodeStream = TryConnectToProcess(nodeToReuse.Id, 0 /* poll, don't wait for connections */, hostHandshake, out HandshakeResult result);
+ Stream nodeStream = TryConnectToProcess(nodeToReuse.Id, 0 /* poll, don't wait for connections */, nodeLaunchData.Handshake, out HandshakeResult result);
if (nodeStream != null)
{
// Connection successful, use this node.
@@ -335,7 +343,7 @@ bool StartNewNode(int nodeId)
CommunicationsUtilities.Trace("Could not connect to existing process, now creating a process...");
// We try this in a loop because it is possible that there is another MSBuild multiproc
- // host process running somewhere which is also trying to create nodes right now. It might
+ // host process running somewhere which is also trying to create nodes right now. It might
// find our newly created node and connect to it before we get a chance.
int retries = NodeCreationRetries;
while (retries-- > 0)
@@ -362,16 +370,17 @@ bool StartNewNode(int nodeId)
#endif
// Create the node process
INodeLauncher nodeLauncher = (INodeLauncher)_componentHost.GetComponent(BuildComponentType.NodeLauncher);
- Process msbuildProcess = nodeLauncher.Start(msbuildLocation, commandLineArgs, nodeId);
+ NodeLaunchData launchData = new(msbuildLocation, commandLineArgs, nodeLaunchData.Handshake, nodeLaunchData.EnvironmentOverrides);
+ Process msbuildProcess = nodeLauncher.Start(launchData, nodeId);
- _processesToIgnore.TryAdd(GetProcessesToIgnoreKey(hostHandshake, msbuildProcess.Id), default);
+ _processesToIgnore.TryAdd(GetProcessesToIgnoreKey(nodeLaunchData.Handshake, msbuildProcess.Id), default);
// Note, when running under IMAGEFILEEXECUTIONOPTIONS registry key to debug, the process ID
// gotten back from CreateProcess is that of the debugger, which causes this to try to connect
// to the debugger process. Instead, use MSBUILDDEBUGONSTART=1
// Now try to connect to it.
- Stream nodeStream = TryConnectToProcess(msbuildProcess.Id, TimeoutForNewNodeCreation, hostHandshake, out HandshakeResult result);
+ Stream nodeStream = TryConnectToProcess(msbuildProcess.Id, TimeoutForNewNodeCreation, nodeLaunchData.Handshake, out HandshakeResult result);
if (nodeStream != null)
{
// Connection successful, use this node.
@@ -408,7 +417,7 @@ bool StartNewNode(int nodeId)
void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte negotiatedVersion)
{
- NodeContext nodeContext = new(nodeId, nodeToReuse, nodeStream, factory, terminateNode, negotiatedVersion, hostHandshake.HandshakeOptions);
+ NodeContext nodeContext = new(nodeId, nodeToReuse, nodeStream, factory, terminateNode, negotiatedVersion, nodeLaunchData.Handshake.HandshakeOptions);
nodeContexts.Enqueue(nodeContext);
createNode(nodeContext);
}
@@ -416,25 +425,20 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte
///
/// Finds processes that could be reusable MSBuild nodes.
- /// Discovers both msbuild.exe processes and dotnet processes hosting MSBuild.dll.
- /// Filters candidates by NodeMode when available.
///
- /// The location of the MSBuild executable
- /// The NodeMode to filter for, or null to include all
+ /// The location of the MSBuild executable used to derive the expected process name.
+ /// The NodeMode to filter for, or null to include all.
///
/// Item 1 is a descriptive name of the processes being searched for.
/// Item 2 is the list of matching processes, sorted by ID.
///
- private (string expectedProcessName, IList nodeProcesses) GetPossibleRunningNodes(string msbuildLocation = null, NodeMode? expectedNodeMode = null)
+ private (string expectedProcessName, IList nodeProcesses) GetPossibleRunningNodes(
+ string msbuildLocation = null,
+ NodeMode? expectedNodeMode = null)
{
- if (String.IsNullOrEmpty(msbuildLocation))
- {
- msbuildLocation = Constants.MSBuildExecutableName;
- }
+ bool isNativeHost = msbuildLocation != null && Path.GetFileName(msbuildLocation).Equals(Constants.MSBuildExecutableName, StringComparison.OrdinalIgnoreCase);
+ string expectedProcessName = Path.GetFileNameWithoutExtension(isNativeHost ? msbuildLocation : (CurrentHost.GetCurrentHost() ?? msbuildLocation));
- var expectedProcessName = Path.GetFileNameWithoutExtension(CurrentHost.GetCurrentHost() ?? msbuildLocation);
-
- // Get all processes with the expected MSBuild executable name
Process[] processes;
try
{
@@ -442,90 +446,89 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream, byte
}
catch
{
- // Process enumeration failed, return empty list
+ // Process enumeration can fail due to permissions or transient OS errors.
return (expectedProcessName, Array.Empty());
}
- // If we have an expected NodeMode, filter by command line parsing
- if (expectedNodeMode.HasValue && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5))
+ bool shouldFilterByNodeMode = expectedNodeMode.HasValue && ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5);
+ if (shouldFilterByNodeMode)
{
- CommunicationsUtilities.Trace("Filtering {0} candidate processes by NodeMode {1} for process name '{2}'",
- processes.Length, expectedNodeMode.Value, expectedProcessName);
- List filteredProcesses = [];
- bool isDotnetProcess = expectedProcessName.Equals(Path.GetFileNameWithoutExtension(Constants.DotnetProcessName), StringComparison.OrdinalIgnoreCase);
-
- foreach (var process in processes)
- {
- try
- {
- if (!process.TryGetCommandLine(out string commandLine))
- {
- // If we can't get the command line, skip this process
- CommunicationsUtilities.Trace("Skipping process {0} - unable to retrieve command line", process.Id);
- continue;
- }
+ return (expectedProcessName, FilterProcessesByNodeMode(processes, expectedNodeMode.Value, expectedProcessName));
+ }
- if (commandLine is null)
- {
- // If we can't get the command line, then allow it as a candidate. This allows reuse to work on platforms where command line retrieval isn't supported, but still filters by NodeMode on platforms where it is supported.
- CommunicationsUtilities.Trace("Including process {0} with unknown NodeMode because command line retrieval is not supported on this platform", process.Id);
- filteredProcesses.Add(process);
- continue;
- }
+ Array.Sort(processes, static (left, right) => left.Id.CompareTo(right.Id));
- // If expected process is dotnet, filter to only those hosting MSBuild.dll
- if (isDotnetProcess && !commandLine.Contains("MSBuild.dll", StringComparison.OrdinalIgnoreCase))
- {
- CommunicationsUtilities.Trace("Skipping dotnet process {0} - not hosting MSBuild.dll. Command line: {1}", process.Id, commandLine);
- continue;
- }
+ return (expectedProcessName, processes);
+ }
- // Extract NodeMode from command line
- NodeMode? processNodeMode = NodeModeHelper.ExtractFromCommandLine(commandLine);
-
- // Only include processes that match the expected NodeMode
- if (processNodeMode.HasValue && processNodeMode.Value == expectedNodeMode.Value)
- {
- CommunicationsUtilities.Trace("Including process {0} with matching NodeMode {1}", process.Id, processNodeMode.Value);
- filteredProcesses.Add(process);
- }
- else
- {
- CommunicationsUtilities.Trace("Skipping process {0} - NodeMode mismatch. Expected: {1}, Found: {2}. Command line: {3}",
- process.Id, expectedNodeMode.Value, processNodeMode?.ToString() ?? "", commandLine);
- }
+ ///
+ /// Filters candidate processes whose command-line NodeMode argument matches the expected value.
+ /// Processes whose command line cannot be retrieved (unsupported platform) are included
+ /// unconditionally to preserve node reuse on those platforms.
+ ///
+ private static IList FilterProcessesByNodeMode(Process[] processes, NodeMode expectedNodeMode, string expectedProcessName)
+ {
+ CommunicationsUtilities.Trace("Filtering {0} candidate processes by NodeMode {1} for process name '{2}'", processes.Length, expectedNodeMode, expectedProcessName);
+
+ List filtered = new(capacity: processes.Length);
+
+ foreach (Process process in processes)
+ {
+ try
+ {
+ if (!process.TryGetCommandLine(out string commandLine))
+ {
+ CommunicationsUtilities.Trace("Skipping process {0} - unable to retrieve command line", process.Id);
+ continue;
}
- catch (Exception ex)
+
+ if (commandLine is null)
{
- // If we encounter any error processing this process, skip it but log
- CommunicationsUtilities.Trace("Failed to get command line for process {0}: {1}", process.Id, ex.Message);
+ // Command-line retrieval is not supported on this platform.
+ // Include the process so that node reuse is not silently broken.
+ CommunicationsUtilities.Trace("Including process {0} - command line retrieval not supported on this platform", process.Id);
+ filtered.Add(process);
continue;
}
- }
- // Sort by process ID for consistent ordering
- filteredProcesses.Sort((left, right) => left.Id.CompareTo(right.Id));
- CommunicationsUtilities.Trace("Filtered to {0} processes matching NodeMode {1}", filteredProcesses.Count, expectedNodeMode.Value);
- return (expectedProcessName, filteredProcesses);
+ NodeMode? processNodeMode = NodeModeHelper.ExtractFromCommandLine(commandLine);
+ if (processNodeMode.HasValue && processNodeMode.Value == expectedNodeMode)
+ {
+ CommunicationsUtilities.Trace("Including process {0} with matching NodeMode {1}", process.Id, processNodeMode.Value);
+ filtered.Add(process);
+ }
+ else
+ {
+ CommunicationsUtilities.Trace(
+ "Skipping process {0} - NodeMode mismatch. Expected: {1}, Found: {2}. Command line: {3}",
+ process.Id, expectedNodeMode,
+ processNodeMode?.ToString() ?? "",
+ commandLine);
+ }
+ }
+ catch (Exception ex)
+ {
+ CommunicationsUtilities.Trace("Skipping process {0} - error retrieving command line: {1}", process.Id, ex.Message);
+ }
}
- // No NodeMode filtering, return all processes sorted by ID
- Array.Sort(processes, (left, right) => left.Id.CompareTo(right.Id));
- return (expectedProcessName, processes);
+ filtered.Sort(static (left, right) => left.Id.CompareTo(right.Id));
+
+ CommunicationsUtilities.Trace("Filtered to {0} processes matching NodeMode {1}", filtered.Count, expectedNodeMode);
+
+ return filtered;
}
///
/// Generate a string from task host context and the remote process to be used as key to lookup processes we have already
/// attempted to connect to or are already connected to
///
- private string GetProcessesToIgnoreKey(Handshake hostHandshake, int nodeProcessId)
- {
+ private string GetProcessesToIgnoreKey(Handshake hostHandshake, int nodeProcessId) =>
#if NET
- return string.Create(CultureInfo.InvariantCulture, $"{hostHandshake}|{nodeProcessId}");
+ string.Create(CultureInfo.InvariantCulture, $"{hostHandshake}|{nodeProcessId}");
#else
- return $"{hostHandshake}|{nodeProcessId.ToString(CultureInfo.InvariantCulture)}";
+ $"{hostHandshake}|{nodeProcessId.ToString(CultureInfo.InvariantCulture)}";
#endif
- }
///
/// Determines which nodes should be reused based on system-wide node count to avoid over-provisioning.
@@ -767,7 +770,7 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han
/// Connect to named pipe stream and ensure validate handshake and security.
///
///
- /// Reused by MSBuild server client .
+ /// Reused by MSBuild server client .
///
internal static bool TryConnectToPipeStream(NamedPipeClientStream nodeStream, string pipeName, Handshake handshake, int timeout, out HandshakeResult result)
{
diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs
index 81bc379ad4f..468fea63eaf 100644
--- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs
@@ -9,7 +9,6 @@
using System.Linq;
using System.Threading;
using Microsoft.Build.Exceptions;
-using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
@@ -238,7 +237,7 @@ public void ShutdownAllNodes()
/// The component host.
public void InitializeComponent(IBuildComponentHost host)
{
- this.ComponentHost = host;
+ ComponentHost = host;
_nodeContexts = new ConcurrentDictionary();
_nodeIdToNodeKey = new ConcurrentDictionary();
_nodeIdToPacketFactory = new ConcurrentDictionary();
@@ -414,19 +413,8 @@ internal static string GetTaskHostNameFromHostContext(HandshakeOptions hostConte
return s_msbuildName;
}
-#if NETFRAMEWORK
- // In .NET Framework, use dotnet for .NET task hosts
- if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET))
- {
- s_msbuildName = Constants.DotnetProcessName;
-
- return s_msbuildName;
- }
-#endif
// Default based on whether it's .NET or Framework
- s_msbuildName = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET)
- ? Constants.MSBuildAssemblyName
- : Constants.MSBuildExecutableName;
+ s_msbuildName = Constants.MSBuildExecutableName;
}
return s_msbuildName;
@@ -484,16 +472,16 @@ internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOption
///
/// A tuple containing:
/// - RuntimeHostPath: The path to the dotnet executable that will host the .NET runtime
- /// - MSBuildAssemblyPath: The full path to MSBuild.dll that will be loaded by the dotnet host.
+ /// - MSBuildPath: The path to MSBuild.dll/MSBuild app host.
///
- internal static (string RuntimeHostPath, string MSBuildAssemblyPath) GetMSBuildLocationForNETRuntime(HandshakeOptions hostContext, TaskHostParameters taskHostParameters)
+ internal static (string RuntimeHostPath, string MSBuildPath) GetMSBuildLocationForNETRuntime(HandshakeOptions hostContext, TaskHostParameters taskHostParameters)
{
ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost));
- return (taskHostParameters.DotnetHostPath, GetMSBuildAssemblyPath(taskHostParameters));
+ return (taskHostParameters.DotnetHostPath, GetMSBuildPath(taskHostParameters));
}
- private static string GetMSBuildAssemblyPath(in TaskHostParameters taskHostParameters)
+ private static string GetMSBuildPath(in TaskHostParameters taskHostParameters)
{
if (taskHostParameters.MSBuildAssemblyPath != null)
{
@@ -502,7 +490,12 @@ private static string GetMSBuildAssemblyPath(in TaskHostParameters taskHostParam
return taskHostParameters.MSBuildAssemblyPath;
}
+#if NET
+ // In .NET we resolve the full path based on the tools directory that points to the directory with App Host
+ return BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory;
+#else
throw new InvalidProjectFileException(ResourceUtilities.GetResourceString("NETHostTaskLoad_Failed"));
+#endif
static void ValidateNetHostSdkVersion(string path)
{
@@ -530,7 +523,7 @@ static void ValidateNetHostSdkVersion(string path)
/// Extracts the major version number from an SDK directory path by parsing the last directory name.
///
///
- /// The full path to an SDK directory.
+ /// The full path to an SDK directory.
/// Example: "C:\Program Files\dotnet\sdk\10.0.100-preview.7.25322.101".
///
///
@@ -542,8 +535,8 @@ static void ValidateNetHostSdkVersion(string path)
/// 1. Extracting the last directory name from the path (e.g., "10.0.100-preview.7.25322.101")
/// 2. Finding the first dot in that directory name
/// 3. Parsing the substring before the first dot as an integer (the major version)
- ///
- /// Returns null if the path is invalid, the last directory name is empty,
+ ///
+ /// Returns null if the path is invalid, the last directory name is empty,
/// there's no dot in the directory name, or the major version cannot be parsed as an integer.
///
private static int? ExtractSdkVersionFromPath(string path)
@@ -670,98 +663,83 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN
// Create callbacks that capture the TaskHostNodeKey
void OnNodeContextCreated(NodeContext context) => NodeContextCreated(context, nodeKey);
- NodeLaunchData nodeLaunchData = ResolveNodeLaunchConfiguration(hostContext, in taskHostParameters);
+ NodeLaunchData nodeLaunchData = ResolveNodeLaunchConfiguration(hostContext, taskHostParameters);
if (nodeLaunchData.MSBuildLocation == null)
{
- return false;
+ return default;
}
- if (nodeLaunchData.UsingDotNetExe)
- {
- CommunicationsUtilities.Trace("For a host context of {0}, spawning dotnet.exe from {1}.", hostContext.ToString(), nodeLaunchData.MSBuildLocation);
- }
- else
- {
- CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext.ToString(), nodeLaunchData.MSBuildLocation);
- }
+ CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext, nodeLaunchData.MSBuildLocation);
- // There is always one task host per host context so we always create just 1 one task host node here.
IList nodeContexts = GetNodes(
- nodeLaunchData.MSBuildLocation,
- nodeLaunchData.CommandLineArgs,
+ nodeLaunchData,
communicationNodeId,
- factory: this,
- nodeLaunchData.Handshake,
+ this,
OnNodeContextCreated,
NodeContextTerminated,
- numberOfNodesToCreate: 1);
+ 1);
return nodeContexts.Count == 1;
- }
-
- private NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, ref readonly TaskHostParameters taskHostParameters)
- {
- string msbuildLocation;
- string commandLineArgs;
- Handshake handshake;
- bool nodeReuse;
-
- BuildParameters buildParameters = ComponentHost.BuildParameters;
-
-#if NETFRAMEWORK
-
- // Handle scenario where a .NET task host is launched from .NET Framework
- if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET))
- {
- (string runtimeHostPath, string msbuildAssemblyDirectory) = GetMSBuildLocationForNETRuntime(hostContext, taskHostParameters);
- msbuildLocation = Path.Combine(msbuildAssemblyDirectory, Constants.MSBuildAssemblyName);
- nodeReuse = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse);
+ // Resolves the node launch configuration based on the host context.
+ NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, in TaskHostParameters taskHostParameters) =>
- commandLineArgs = $"""
- "{msbuildLocation}" /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{nodeReuse.ToString().ToLower()} /low:{buildParameters.LowPriority.ToString().ToLower()} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion}
- """;
-
- handshake = new Handshake(hostContext, toolsDirectory: msbuildAssemblyDirectory);
-
- return new NodeLaunchData(runtimeHostPath, commandLineArgs, handshake, UsingDotNetExe: true);
- }
-#endif
+ // Handle .NET task host context
+ Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET)
+ ? ResolveAppHostOrFallback(GetMSBuildPath(taskHostParameters), taskHostParameters.DotnetHostPath, hostContext, IsNodeReuseEnabled(hostContext))
+ : new NodeLaunchData(GetMSBuildExecutablePathForNonNETRuntimes(hostContext), BuildCommandLineArgs(IsNodeReuseEnabled(hostContext)), new Handshake(hostContext));
+ }
- msbuildLocation = GetMSBuildExecutablePathForNonNETRuntimes(hostContext);
+ ///
+ /// Determines whether node reuse should be enabled for the given host context.
+ /// Node reuse is disabled for CLR2 because it uses legacy MSBuildTaskHost.
+ ///
+ private static bool IsNodeReuseEnabled(HandshakeOptions hostContext) =>
+ Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse) && !Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2);
- // we couldn't even figure out the location we're trying to launch ... just go ahead and fail.
- if (msbuildLocation == null)
- {
- return default;
- }
+ ///
+ /// Resolves whether to use the MSBuild app host or fall back to dotnet.exe.
+ ///
+ /// Path to the MSBuild assembly/app host directory.
+ /// Path to the dotnet executable.
+ /// The handshake options for the host context.
+ /// Whether node reuse is enabled.
+ /// The resolved node launch configuration.
+ private NodeLaunchData ResolveAppHostOrFallback(
+ string msbuildAssemblyPath,
+ string dotnetHostPath,
+ HandshakeOptions hostContext,
+ bool nodeReuseEnabled)
+ {
+ string appHostPath = Path.Combine(msbuildAssemblyPath, Constants.MSBuildExecutableName);
+ string commandLineArgs = BuildCommandLineArgs(nodeReuseEnabled);
-#if FEATURE_NET35_TASKHOST
- if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2))
+ if (FileSystems.Default.FileExists(appHostPath))
{
- // The .NET 3.5 task host uses the directory of its EXE when calculating salt for the handshake.
- string toolsDirectory = Path.GetDirectoryName(msbuildLocation) ?? string.Empty;
+ CommunicationsUtilities.Trace("For a host context of {0}, using app host from {1}.", hostContext, appHostPath);
- // MSBuildTaskHost doesn't use command-line arguments.
- commandLineArgs = "";
- handshake = new Handshake(hostContext, toolsDirectory);
+ IDictionary dotnetOverrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(dotnetHostPath);
- return new NodeLaunchData(msbuildLocation, commandLineArgs, handshake);
+ return dotnetOverrides == null
+ ? throw new NodeFailedToLaunchException(errorCode: null, ResourceUtilities.GetResourceString("DotnetHostPathNotSet"))
+ : new NodeLaunchData(
+ appHostPath,
+ commandLineArgs,
+ new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath),
+ dotnetOverrides);
}
-#endif
-
- nodeReuse = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse);
- commandLineArgs = $"""
- /nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{nodeReuse.ToString().ToLower()} /low:{buildParameters.LowPriority.ToString().ToLower()} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion}
- """;
+ CommunicationsUtilities.Trace("For a host context of {0}, app host not found at {1}, falling back to dotnet.exe from {2}.", hostContext, appHostPath, dotnetHostPath);
- handshake = new Handshake(hostContext);
-
- return new NodeLaunchData(msbuildLocation, commandLineArgs, handshake);
+ return new NodeLaunchData(
+ dotnetHostPath,
+ $"\"{Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName)}\" {commandLineArgs}",
+ new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath));
}
+ private string BuildCommandLineArgs(bool nodeReuseEnabled) => $"/nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{nodeReuseEnabled} /low:{ComponentHost.BuildParameters.LowPriority} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion} ";
+
///
/// Method called when a context created.
///
diff --git a/src/Build/BackEnd/Components/Communications/RarNodeLauncher.cs b/src/Build/BackEnd/Components/Communications/RarNodeLauncher.cs
index 78730f2c607..34442b786cd 100644
--- a/src/Build/BackEnd/Components/Communications/RarNodeLauncher.cs
+++ b/src/Build/BackEnd/Components/Communications/RarNodeLauncher.cs
@@ -74,7 +74,7 @@ private void LaunchNode()
{
string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath;
string commandLineArgs = string.Join(" ", ["/nologo", NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcRarNode)]);
- _ = _nodeLauncher.Start(msbuildLocation, commandLineArgs, nodeId: 0);
+ _ = _nodeLauncher.Start(new NodeLaunchData(msbuildLocation, commandLineArgs), nodeId: 0);
}
}
}
diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs
index 125e5afde36..70cf7f82154 100644
--- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs
+++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverLoader.cs
@@ -240,13 +240,13 @@ protected virtual Assembly LoadResolverAssembly(string resolverPath)
// We very much prefer the default load context because it allows native images to be used by the CLR, improving startup perf.
var buildEnvironment = BuildEnvironmentHelper.Instance;
AssemblyName assemblyName = CreateAssemblyNameWithCodeBase(resolverFileName, resolverPath);
-
- // Check if we're in a scenario that needs fallback (API usage or dotnet CLI)
- // These scenarios are detected by: Mode = Standalone and not running in MSBuild.exe
- // This matches the condition set by TryFromMSBuildAssembly when MSBuild is called from external APIs
- // VS and MSBuild.exe direct usage can use Assembly.Load reliably, so they don't need fallback
+
+ // Need fallback for scenarios where we're not running directly in MSBuild.exe app host.
+ // RunningInMSBuildExe is true when the actual process is MSBuild.exe (app host),
+ // false when running via dotnet CLI (dotnet MSBuild.dll), external API callers, or test runners.
+ // VS and MSBuild.exe app host can use Assembly.Load reliably and benefit from NGEN.
bool needsFallback = buildEnvironment.Mode == BuildEnvironmentMode.Standalone && !buildEnvironment.RunningInMSBuildExe;
-
+
if (needsFallback)
{
// For external API users and dotnet CLI, use LoadFrom directly
diff --git a/src/Build/BackEnd/Node/NativeMethods.cs b/src/Build/BackEnd/Node/NativeMethods.cs
index 41ed12e50ec..31054f8cbd2 100644
--- a/src/Build/BackEnd/Node/NativeMethods.cs
+++ b/src/Build/BackEnd/Node/NativeMethods.cs
@@ -42,6 +42,11 @@ internal static class NativeMethods
///
internal const Int32 CREATE_NEW_CONSOLE = 0x00000010;
+ ///
+ /// Indicates that the environment block uses Unicode characters.
+ ///
+ internal const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
+
///
/// Create a new process
///
diff --git a/src/Build/BackEnd/Node/OutOfProcNode.cs b/src/Build/BackEnd/Node/OutOfProcNode.cs
index 7225c1a9988..c691a1c69c2 100644
--- a/src/Build/BackEnd/Node/OutOfProcNode.cs
+++ b/src/Build/BackEnd/Node/OutOfProcNode.cs
@@ -743,6 +743,7 @@ private void HandleNodeConfiguration(NodeConfiguration configuration)
}
Traits.UpdateFromEnvironment();
+ DotnetHostEnvironmentHelper.ClearBootstrapDotnetRootEnvironment(_buildParameters.BuildProcessEnvironment);
// We want to make sure the global project collection has the toolsets which were defined on the parent
// so that any custom toolsets defined can be picked up by tasks who may use the global project collection but are
diff --git a/src/Build/Evaluation/IntrinsicFunctions.cs b/src/Build/Evaluation/IntrinsicFunctions.cs
index f0417038f7b..df3783f5ba9 100644
--- a/src/Build/Evaluation/IntrinsicFunctions.cs
+++ b/src/Build/Evaluation/IntrinsicFunctions.cs
@@ -510,7 +510,7 @@ internal static bool DoesTaskHostExist(string runtime, string architecture)
#if NETFRAMEWORK
if (Handshake.IsHandshakeOptionEnabled(desiredContext, HandshakeOptions.NET))
{
- taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(desiredContext, parameters).MSBuildAssemblyPath;
+ taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(desiredContext, parameters).MSBuildPath;
}
#endif
diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs
index 9970831e264..1b76ee832ef 100644
--- a/src/Build/Instance/TaskFactories/TaskHostTask.cs
+++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs
@@ -406,9 +406,9 @@ public bool Execute()
LogErrorUnableToCreateTaskHost(_requiredContext, _taskHostParameters.Runtime, _taskHostParameters.Architecture, null);
}
}
- catch (BuildAbortedException)
+ catch (BuildAbortedException ex)
{
- LogErrorUnableToCreateTaskHost(_requiredContext, _taskHostParameters.Runtime, _taskHostParameters.Architecture, null);
+ LogErrorUnableToCreateTaskHost(_requiredContext, _taskHostParameters.Runtime, _taskHostParameters.Architecture, ex);
}
catch (NodeFailedToLaunchException e)
{
@@ -698,13 +698,13 @@ private void HandleCoresRequest(TaskHostCoresRequest request)
/// Since we log that we weren't able to connect to the task host in a couple of different places,
/// extract it out into a separate method.
///
- private void LogErrorUnableToCreateTaskHost(HandshakeOptions requiredContext, string runtime, string architecture, NodeFailedToLaunchException e)
+ private void LogErrorUnableToCreateTaskHost(HandshakeOptions requiredContext, string runtime, string architecture, Exception e)
{
string taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildExecutablePathForNonNETRuntimes(requiredContext);
#if NETFRAMEWORK
if (Handshake.IsHandshakeOptionEnabled(requiredContext, HandshakeOptions.NET))
{
- taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(requiredContext, _taskHostParameters).MSBuildAssemblyPath;
+ taskHostLocation = NodeProviderOutOfProcTaskHost.GetMSBuildLocationForNETRuntime(requiredContext, _taskHostParameters).MSBuildPath;
}
#endif
string msbuildLocation = taskHostLocation ??
@@ -720,7 +720,15 @@ private void LogErrorUnableToCreateTaskHost(HandshakeOptions requiredContext, st
}
else
{
- _taskLoggingContext.LogError(new BuildEventFileInfo(_taskLocation), "TaskHostNodeFailedToLaunch", _taskType.Type.Name, runtime, architecture, msbuildLocation, e.ErrorCode, e.Message);
+ _taskLoggingContext.LogError(
+ new BuildEventFileInfo(_taskLocation),
+ "TaskHostNodeFailedToLaunch",
+ _taskType.Type.Name,
+ runtime,
+ architecture,
+ msbuildLocation,
+ e is NodeFailedToLaunchException nodeFailedExc ? nodeFailedExc.ErrorCode : string.Empty,
+ e.Message);
}
}
}
diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx
index c573bbdd54d..b9bb5011c83 100644
--- a/src/Build/Resources/Strings.resx
+++ b/src/Build/Resources/Strings.resx
@@ -2444,6 +2444,9 @@ Utilization: {0} Average Utilization: {1:###.0}
The directory does not exist: {0}. .NET Runtime Task Host could not be instantiated. See https://aka.ms/nettaskhost for details on how to resolve this error.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
MSB4279: Failed to copy binary log from "{0}" to "{1}". {2}{StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations.
diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf
index 043072c5530..540938dad60 100644
--- a/src/Build/Resources/xlf/Strings.cs.xlf
+++ b/src/Build/Resources/xlf/Strings.cs.xlf
@@ -466,6 +466,11 @@
MSB4280: Proměnná prostředí DOTNET_HOST_PATH je nastavena na adresář ({0}) místo na cestu ke spustitelnému souboru dotnet. To může způsobit chyby sestavení v úlohách, které tuto proměnnou používají, například v kompilátoru Roslyn. Buď nastavení proměnné zrušte, nebo ji aktualizujte tak, aby přímo odkazovala na spustitelný soubor dotnet (např. „C:\Program Files\dotnet\dotnet.exe“).{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: Došlo k pokusu o vytvoření více přepsání stejné úlohy: {0}
diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf
index 13e8c4a1028..78015225985 100644
--- a/src/Build/Resources/xlf/Strings.de.xlf
+++ b/src/Build/Resources/xlf/Strings.de.xlf
@@ -466,6 +466,11 @@
MSB4280: Die Umgebungsvariable DOTNET_HOST_PATH ist auf ein Verzeichnis („{0}“) und nicht auf den Pfad zur ausführbaren dotnet-Datei festgelegt. Dies kann zu Buildfehlern bei Aufgaben führen, die diese Variable verwenden, wie zum Beispiel beim Roslyn-Compiler. Entfernen Sie die Variable oder aktualisieren Sie sie so, dass sie direkt auf die ausführbare dotnet-Datei zeigt (z. B. „C:\Program Files\dotnet\dotnet.exe“).{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: Es wurde versucht, mehrere Außerkraftsetzungen derselben Aufgabe zu erstellen: {0}
diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf
index 03acc62388f..67c386f6ad1 100644
--- a/src/Build/Resources/xlf/Strings.es.xlf
+++ b/src/Build/Resources/xlf/Strings.es.xlf
@@ -466,6 +466,11 @@
MSB4280: la variable de entorno DOTNET_HOST_PATH se establece en un directorio ("{0}") en lugar de una ruta de acceso al ejecutable dotnet. Esto puede provocar errores de compilación en tareas que usan esta variable, como el compilador Roslyn. Quite la variable o actualícela para que apunte directamente al ejecutable de dotnet (por ejemplo, "C:\Archivos de programa\dotnet\dotnet.exe").{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: Se intentaron crear varias invalidaciones de la misma tarea: {0}
diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf
index 4887128eaca..cf03e0c9acc 100644
--- a/src/Build/Resources/xlf/Strings.fr.xlf
+++ b/src/Build/Resources/xlf/Strings.fr.xlf
@@ -466,6 +466,11 @@
MSB4280: la variable d’environnement DOTNET_HOST_PATH est définie sur un répertoire (« {0} ») au lieu d’un chemin vers l’exécutable dotnet. Cela peut provoquer des erreurs de compilation dans les tâches qui utilisent cette variable, comme le compilateur Roslyn. Désactivez la variable ou modifiez-la pour qu’elle pointe directement vers l’exécutable dotnet (par exemple, « C:\Program Files\dotnet\dotnet.exe »).{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: Tentative de création de plusieurs remplacements de la même tâche : {0}
diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf
index 6f3a39ae585..aa3fe18f5be 100644
--- a/src/Build/Resources/xlf/Strings.it.xlf
+++ b/src/Build/Resources/xlf/Strings.it.xlf
@@ -466,6 +466,11 @@
MSB4280: La variabile di ambiente DOTNET_HOST_PATH è impostata su una directory ("{0}") anziché sul percorso di un file eseguibile dotnet. Questo può causare errori di compilazione nelle attività che usano questa variabile, come il compilatore Roslyn. Annullare la variabile oppure aggiornarla in modo che punti direttamente al file eseguibile dotnet (ad esempio "C:\Programmi\dotnet\dotnet.exe").{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: tentativo di creare più sostituzioni della stessa attività: {0}
diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf
index d8ce6bf4357..4a736da2699 100644
--- a/src/Build/Resources/xlf/Strings.ja.xlf
+++ b/src/Build/Resources/xlf/Strings.ja.xlf
@@ -466,6 +466,11 @@
MSB4280: 環境変数 DOTNET_HOST_PATH が dotnet 実行可能ファイルへのパスではなく、ディレクトリ ("{0}") に設定されています。このため、Roslyn コンパイラなど、この変数を使用するタスクでビルド エラーが発生する可能性があります。変数の設定を解除するか、dotnet 実行可能ファイルを直接指すように更新してください (例: "C:\Program Files\dotnet\dotnet.exe")。{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: 同じタスクの複数のオーバーライドを作成しようとしました: {0}
diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf
index 7c369855cd3..be22f1be839 100644
--- a/src/Build/Resources/xlf/Strings.ko.xlf
+++ b/src/Build/Resources/xlf/Strings.ko.xlf
@@ -466,6 +466,11 @@
MSB4280: DOTNET_HOST_PATH 환경 변수가 dotnet 실행 파일에 대한 경로가 아닌 디렉터리("{0}")으(로) 설정되어 있습니다. 이로 인해 Roslyn 컴파일러와 같은 이 변수를 사용하는 작업에서 빌드 오류가 발생할 수 있습니다. 변수를 설정 해제하거나 dotnet 실행 파일을 직접 가리키도록 업데이트합니다(예: "C:\Program Files\dotnet\dotnet.exe").{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: 동일한 작업의 여러 재정의를 만들려고 했습니다. {0}
diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf
index bd540f5ff51..6d766c47c96 100644
--- a/src/Build/Resources/xlf/Strings.pl.xlf
+++ b/src/Build/Resources/xlf/Strings.pl.xlf
@@ -466,6 +466,11 @@
MSB4280: zmienna środowiskowa DOTNET_HOST_PATH jest ustawiona na katalog („{0}”) zamiast ścieżki do pliku wykonywalnego dotnet. Może to prowadzić do błędów kompilacji w zadaniach korzystających z tej zmiennej, takich jak kompilator Roslyn. Usuń ustawienie zmiennej lub zaktualizuj ją, aby wskazywała bezpośrednio plik wykonywalny dotnet (np. „C:\Program Files\dotnet\dotnet.exe”).{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: Podjęto próbę utworzenia wielu zastąpień tego samego zadania: {0}
diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf
index dba114aa2f6..22f57028ae3 100644
--- a/src/Build/Resources/xlf/Strings.pt-BR.xlf
+++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf
@@ -466,6 +466,11 @@
MSB4280: a variável de ambiente DOTNET_HOST_PATH é definida como um diretório ("{0}") em vez de um caminho para o executável dotnet. Isso pode levar a erros de compilação em tarefas que usam essa variável, como o compilador Roslyn. Remova a definição da variável ou atualize-a para apontar diretamente para o executável dotnet (por exemplo, "C:\Arquivos de Programas\dotnet\dotnet.exe").{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: tentativa de criar várias substituições da mesma tarefa: {0}
diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf
index 6b069c84db6..9e61881a5d9 100644
--- a/src/Build/Resources/xlf/Strings.ru.xlf
+++ b/src/Build/Resources/xlf/Strings.ru.xlf
@@ -466,6 +466,11 @@
MSB4280: переменная среды DOTNET_HOST_PATH настроена на каталог ("{0}"), а не на путь к исполняемому файлу dotnet. Это может вызвать ошибки сборки в задачах, использующих эту переменную, например в компиляторе Roslyn. Либо удалите эту переменную, либо обновите ее, указав прямой путь к исполняемому файлу dotnet (например, "C:\Program Files\dotnet\dotnet.exe").{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: попытка создать несколько переопределений одной задачи: {0}
diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf
index 5f47925406e..3f6e526580f 100644
--- a/src/Build/Resources/xlf/Strings.tr.xlf
+++ b/src/Build/Resources/xlf/Strings.tr.xlf
@@ -466,6 +466,11 @@
MSB4280: DOTNET_HOST_PATH ortam değişkeni, dotnet yürütülebilir dosyasının yolu yerine bir dizin (“{0}”) olarak ayarlanmıştır. Bu, Roslyn derleyicisi gibi bu değişkeni kullanan görevlerde derleme hatalarına yol açabilir. Değişkeni kaldırın veya doğrudan dotnet yürütülebilir dosyasını gösterecek şekilde güncelleyin (örneğin, “C:\Program Files\dotnet\dotnet.exe”).{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: Aynı {0} görevi için birden çok geçersiz kılma işlemi oluşturulmaya çalışıldı
diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf
index 1e431883513..2dbf5ad88d2 100644
--- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf
+++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf
@@ -466,6 +466,11 @@
MSB4280: 环境变量 DOTNET_HOST_PATH 设置为目录(“{0}”),而不是指向 dotnet 可执行文件的路径。这可能导致使用该变量的任务(如 Roslyn 编译器)出现构建错误。请取消设置该变量,或将其更新为直接指向 dotnet 可执行文件的路径(例如 "C:\Program Files\dotnet\dotnet.exe")。{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: 已尝试创建同一任务的多个重写: {0}
@@ -2399,12 +2404,12 @@ Errors: {3}
MSB4216: Could not run the "{0}" task because MSBuild could not create or connect to a task host with runtime "{1}" and architecture "{2}". Please ensure that (1) the requested runtime and/or architecture are available on the machine, and (2) that the required executable "{3}" exists and can be run.
- MSB4216: 无法运行“{0}”任务,因为 MSBuild 无法创建或连接到运行时为“{1}”、体系结构为“{2}”的任务宿主。请确保 (1) 请求的运行时和/或体系结构在计算机上可用,以及 (2) 所需的可执行文件“{3}”存在,且可以运行。
+ MSB4216: 无法运行"{0}"任务,因为 MSBuild 无法创建或连接到运行时为"{1}"、体系结构为"{2}"的任务宿主。请确保 (1) 请求的运行时和/或体系结构在计算机上可用,以及 (2) 所需的可执行文件"{3}"存在,且可以运行。{StrBegin="MSB4216: "}MSB4221: Could not run the "{0}" task because MSBuild could not create or connect to a task host with runtime "{1}" and architecture "{2}". Please ensure that (1) the requested runtime and/or architecture are available on the machine, and (2) that the required executable "{3}" exists and can be run. Error {4} {5}.
- MSB4221: 无法运行“{0}”任务,因为 MSBuild 无法创建或连接到运行时为“{1}”、体系结构为“{2}”的任务宿主。请确保 (1) 请求的运行时和/或体系结构在计算机上可用,以及 (2) 所需的可执行文件“{3}”存在,且可以运行。错误 {4} {5}。
+ MSB4221: 无法运行"{0}"任务,因为 MSBuild 无法创建或连接到运行时为"{1}"、体系结构为"{2}"的任务宿主。请确保 (1) 请求的运行时和/或体系结构在计算机上可用,以及 (2) 所需的可执行文件"{3}"存在,且可以运行。错误 {4} {5}。{StrBegin="MSB4221: "}
diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf
index c056dd7c8a1..8416c61b8e0 100644
--- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf
+++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf
@@ -466,6 +466,11 @@
MSB4280: 環境變數DOTNET_HOST_PATH 設定為目錄 ("{0}") 而不是 .NET 可執行檔的路徑。這可能會導致使用此變數的工作 (例如,Roslyn 編譯程式) 建置錯誤。取消設定該變數,或更新變數以直接指向 .NET 可執行檔 (例如 "C:\Program Files\dotnet\dotnet.exe")。{StrBegin="MSB4280: "}UE: This warning is shown when the DOTNET_HOST_PATH environment variable is set to a directory path rather than a file path. The variable should point to the dotnet executable, not a directory containing it.
+
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+ DOTNET_HOST_PATH is not set. Cannot determine runtime location for app host bootstrap. This should always be set when running under the .NET SDK.
+
+ MSB4275: Attempted to create multiple overrides of the same task: {0}MSB4275: 已嘗試建立相同工作的多個覆寫: {0}
diff --git a/src/Framework/BinaryTranslator.cs b/src/Framework/BinaryTranslator.cs
index 684929529b0..b42cbddd95e 100644
--- a/src/Framework/BinaryTranslator.cs
+++ b/src/Framework/BinaryTranslator.cs
@@ -1123,12 +1123,12 @@ public void Translate(ref TaskHostParameters value)
string runtime = value.Runtime;
string architecture = value.Architecture;
string dotnetHostPath = value.DotnetHostPath;
- string msBuildAssemblyPath = value.MSBuildAssemblyPath;
+ string msBuildExecutablePath = value.MSBuildAssemblyPath;
Translate(ref runtime);
Translate(ref architecture);
Translate(ref dotnetHostPath);
- Translate(ref msBuildAssemblyPath);
+ Translate(ref msBuildExecutablePath);
bool hasTaskHostFactory = value.TaskHostFactoryExplicitlyRequested.HasValue;
_writer.Write(hasTaskHostFactory);
diff --git a/src/Framework/DotnetHostEnvironmentHelper.cs b/src/Framework/DotnetHostEnvironmentHelper.cs
new file mode 100644
index 00000000000..7ecd6e91bab
--- /dev/null
+++ b/src/Framework/DotnetHostEnvironmentHelper.cs
@@ -0,0 +1,158 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Build.Framework;
+
+#if NET
+using System.Runtime.InteropServices;
+#endif
+
+namespace Microsoft.Build.Internal
+{
+ ///
+ /// Helper methods for managing DOTNET_ROOT environment variables during MSBuild app host bootstrap.
+ /// When MSBuild runs as an app host (native executable), child processes need DOTNET_ROOT set
+ /// to find the runtime, but this should not leak to tools those processes execute.
+ ///
+ internal static class DotnetHostEnvironmentHelper
+ {
+ private static string? _cachedDotnetHostPath;
+ private static IDictionary? _cachedOverrides;
+
+ // Environment variable name for .NET runtime root directory.
+ private const string DotnetRootEnvVarName = "DOTNET_ROOT";
+
+#if NET
+ // Architecture-specific DOTNET_ROOT environment variable names, dynamically generated
+ // to match the native implementation and cover all architectures supported by the runtime.
+ private static readonly string[] _archSpecificRootVars = Array.ConvertAll(Enum.GetNames(), name => $"{DotnetRootEnvVarName}_{name.ToUpperInvariant()}");
+#else
+ // On .NET Framework, Architecture enum doesn't exist, so we use hardcoded values.
+ // This is sufficient since .NET Framework only runs on Windows x86/x64/ARM64.
+ private static readonly string[] _archSpecificRootVars =
+ [
+ "DOTNET_ROOT_X86",
+ "DOTNET_ROOT_X64",
+ "DOTNET_ROOT_ARM64",
+ ];
+#endif
+
+ ///
+ /// Clears DOTNET_ROOT environment variables that were set only for app host bootstrap.
+ /// These should not leak to tools executed by the build.
+ /// Only clears if the variable was NOT present in the original build process environment.
+ ///
+ /// The original environment from the entry-point process.
+ internal static void ClearBootstrapDotnetRootEnvironment(IDictionary buildProcessEnvironment)
+ {
+ if (!buildProcessEnvironment.ContainsKey(DotnetRootEnvVarName))
+ {
+ Environment.SetEnvironmentVariable(DotnetRootEnvVarName, null);
+ }
+
+ foreach (string varName in _archSpecificRootVars)
+ {
+ if (!buildProcessEnvironment.ContainsKey(varName))
+ {
+ Environment.SetEnvironmentVariable(varName, null);
+ }
+ }
+ }
+
+ ///
+ /// Creates environment variable overrides for app host.
+ /// Sets DOTNET_ROOT derived from the specified dotnet host path.
+ /// Results are cached so repeated calls with the same path avoid allocations.
+ ///
+ /// Path to the dotnet executable.
+ /// Dictionary of environment variable overrides, or null if dotnetHostPath is empty.
+ internal static IDictionary? CreateDotnetRootEnvironmentOverrides(string? dotnetHostPath = null)
+ {
+ string? resolvedPath = dotnetHostPath ?? Environment.GetEnvironmentVariable(Constants.DotnetHostPathEnvVarName);
+
+ // Return cached result if the input hasn't changed.
+ // Race conditions are benign here: the computation is idempotent, so the worst case is a redundant allocation.
+ string? cachedPath = _cachedDotnetHostPath;
+ IDictionary? cachedResult = _cachedOverrides;
+ if (string.Equals(cachedPath, resolvedPath, StringComparison.Ordinal) && (cachedPath is not null || cachedResult is not null))
+ {
+ return cachedResult;
+ }
+
+ string? dotnetRoot = ResolveDotnetRoot(dotnetHostPath);
+
+ if (string.IsNullOrEmpty(dotnetRoot))
+ {
+ _cachedOverrides = null;
+ _cachedDotnetHostPath = resolvedPath;
+ return null;
+ }
+
+ var overrides = new Dictionary
+ {
+ [DotnetRootEnvVarName] = dotnetRoot,
+ };
+
+ // Clear architecture-specific overrides that would take precedence over DOTNET_ROOT
+ foreach (string varName in _archSpecificRootVars)
+ {
+ overrides[varName] = null!;
+ }
+
+ _cachedOverrides = overrides;
+ _cachedDotnetHostPath = resolvedPath;
+
+ return overrides;
+ }
+
+ ///
+ /// Applies environment variable overrides to a dictionary.
+ /// A non-null value sets or overrides that variable. A null value removes the variable.
+ ///
+ /// The environment dictionary to modify.
+ /// The overrides to apply. If null, no changes are made.
+ internal static void ApplyEnvironmentOverrides(IDictionary environment, IDictionary? overrides)
+ {
+ if (overrides is null)
+ {
+ return;
+ }
+
+ foreach (KeyValuePair kvp in overrides)
+ {
+ if (kvp.Value is null)
+ {
+ environment.Remove(kvp.Key);
+ }
+ else
+ {
+ environment[kvp.Key] = kvp.Value;
+ }
+ }
+ }
+
+ private static string? ResolveDotnetRoot(string? dotnetHostPath)
+ {
+ dotnetHostPath ??= Environment.GetEnvironmentVariable(Constants.DotnetHostPathEnvVarName);
+
+ if (!string.IsNullOrEmpty(dotnetHostPath))
+ {
+ return Path.GetDirectoryName(dotnetHostPath);
+ }
+
+#if RUNTIME_TYPE_NETCORE && BUILD_ENGINE
+ // DOTNET_HOST_PATH not set - use CurrentHost to find the dotnet executable.
+ string? currentHost = CurrentHost.GetCurrentHost();
+ if (!string.IsNullOrEmpty(currentHost))
+ {
+ return Path.GetDirectoryName(currentHost);
+ }
+#endif
+
+ return null;
+ }
+ }
+}
diff --git a/src/Framework/SupportedOSPlatform.cs b/src/Framework/SupportedOSPlatform.cs
index f532d4569e6..8c89553d9be 100644
--- a/src/Framework/SupportedOSPlatform.cs
+++ b/src/Framework/SupportedOSPlatform.cs
@@ -23,5 +23,12 @@ internal SupportedOSPlatform(string platformName)
{
}
}
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Class)]
+ internal class UnsupportedOSPlatform : Attribute
+ {
+ internal UnsupportedOSPlatform(string platformName)
+ {
+ }
+ }
}
#endif
diff --git a/src/Framework/TaskHostParameters.cs b/src/Framework/TaskHostParameters.cs
index 97ee9223a96..3c316627cd4 100644
--- a/src/Framework/TaskHostParameters.cs
+++ b/src/Framework/TaskHostParameters.cs
@@ -63,9 +63,9 @@ internal TaskHostParameters(
public string? DotnetHostPath => _dotnetHostPath;
///
- /// Gets the path to the MSBuild assembly.
+ /// Gets the path to the MSBuild assembly or executable. It can be either path to MSBuild.dll or to app host (MSBuild.exe).
///
- /// The MSBuild assembly path, or null if not specified.
+ /// The MSBuild assembly or executable path, or null if not specified.
public string? MSBuildAssemblyPath => _msBuildAssemblyPath;
///
diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs
index 1c9a2558d24..0941af26946 100644
--- a/src/MSBuild.UnitTests/XMake_Tests.cs
+++ b/src/MSBuild.UnitTests/XMake_Tests.cs
@@ -19,6 +19,7 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.Build.Shared;
+using Microsoft.Build.Tasks;
using Microsoft.Build.UnitTests.Shared;
using Microsoft.Build.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities;
@@ -74,12 +75,6 @@ private static string GenerateMessageImportanceProjectFile(MessageImportance exp
+ "");
}
-#if USE_MSBUILD_DLL_EXTN
- private const string MSBuildExeName = "MSBuild.dll";
-#else
- private const string MSBuildExeName = "MSBuild.exe";
-#endif
-
private readonly ITestOutputHelper _output;
private readonly TestEnvironment _env;
@@ -567,10 +562,6 @@ public void VersionSwitch()
List cmdLine = new()
{
-#if !FEATURE_RUN_EXE_IN_TESTS
- EnvironmentProvider.GetDotnetExePath(),
-#endif
- FileUtilities.EnsureDoubleQuotes(RunnerUtilities.PathToCurrentlyRunningMsBuildExe),
"-nologo",
"-version"
};
@@ -579,8 +570,8 @@ public void VersionSwitch()
{
StartInfo =
{
- FileName = cmdLine[0],
- Arguments = string.Join(" ", cmdLine.Skip(1)),
+ FileName = RunnerUtilities.PathToCurrentlyRunningMsBuildExe,
+ Arguments = string.Join(" ", cmdLine),
UseShellExecute = false,
RedirectStandardOutput = true,
},
@@ -611,10 +602,6 @@ public void VersionSwitchDisableChangeWave()
List cmdLine = new()
{
-#if !FEATURE_RUN_EXE_IN_TESTS
- EnvironmentProvider.GetDotnetExePath(),
-#endif
- FileUtilities.EnsureDoubleQuotes(RunnerUtilities.PathToCurrentlyRunningMsBuildExe),
"-nologo",
"-version"
};
@@ -623,8 +610,8 @@ public void VersionSwitchDisableChangeWave()
{
StartInfo =
{
- FileName = cmdLine[0],
- Arguments = string.Join(" ", cmdLine.Skip(1)),
+ FileName = RunnerUtilities.PathToCurrentlyRunningMsBuildExe,
+ Arguments = string.Join(" ", cmdLine),
UseShellExecute = false,
RedirectStandardOutput = true,
},
@@ -1472,7 +1459,7 @@ public void ResponseFileInProjectDirectoryWinsOverMainMSBuildRsp()
string rspPath = Path.Combine(directory, AutoResponseFileName);
exeDirectory = CopyMSBuild();
- string exePath = Path.Combine(exeDirectory, MSBuildExeName);
+ string exePath = Path.Combine(exeDirectory, Constants.MSBuildExecutableName);
string mainRspPath = Path.Combine(exeDirectory, AutoResponseFileName);
Directory.CreateDirectory(exeDirectory);
@@ -1512,7 +1499,7 @@ public void ProjectDirectoryIsMSBuildExeDirectory()
directory = CopyMSBuild();
string projectPath = Path.Combine(directory, "my.proj");
string rspPath = Path.Combine(directory, AutoResponseFileName);
- string exePath = Path.Combine(directory, MSBuildExeName);
+ string exePath = Path.Combine(directory, Constants.MSBuildExecutableName);
string content = ObjectModelHelpers.CleanupFileContents("");
File.WriteAllText(projectPath, content);
diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs
index 8405bf79fc6..fc94bcb0b34 100644
--- a/src/MSBuild/CommandLine/CommandLineParser.cs
+++ b/src/MSBuild/CommandLine/CommandLineParser.cs
@@ -104,19 +104,8 @@ internal void GatherAllSwitches(
// discard the first piece, because that's the path to the executable -- the rest are args
commandLineArgs = commandLineArgs.Skip(1);
-
exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath;
-#if USE_MSBUILD_DLL_EXTN
- var msbuildExtn = ".dll";
-#else
- var msbuildExtn = ".exe";
-#endif
- if (!exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase))
- {
- exeName += msbuildExtn;
- }
-
fullCommandLine = $"'{string.Join(" ", commandLineArgs)}'";
// parse the command line, and flag syntax errors and obvious switch errors
diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj
index b0a10d5dd60..cf205b2366d 100644
--- a/src/MSBuild/MSBuild.csproj
+++ b/src/MSBuild/MSBuild.csproj
@@ -10,10 +10,6 @@
arm64
-
- false
-
@@ -174,9 +170,10 @@
PreserveNewest
+
-
+ PreserveNewest
@@ -325,4 +322,3 @@
-
diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs
index 59ab803cce0..75716b705a7 100644
--- a/src/MSBuild/OutOfProcTaskHostNode.cs
+++ b/src/MSBuild/OutOfProcTaskHostNode.cs
@@ -209,15 +209,13 @@ internal class OutOfProcTaskHostNode :
/// Minimum packet version required for IBuildEngine callback support.
/// When all callback stages are complete, PacketVersion will be bumped to this value.
///
- private const byte CallbacksMinPacketVersion = 3;
+ private const byte CallbacksMinPacketVersion = 4;
///
/// Whether the owning worker node supports IBuildEngine callbacks.
/// True if the worker node's packet version is high enough, or if the feature is force-enabled via env var.
///
- private bool CallbacksSupported =>
- _parentPacketVersion >= CallbacksMinPacketVersion
- || Traits.Instance.EnableTaskHostCallbacks;
+ private bool CallbacksSupported => _parentPacketVersion >= CallbacksMinPacketVersion || Traits.Instance.EnableTaskHostCallbacks;
#endif
///
@@ -1128,6 +1126,9 @@ private void RunTask(object state)
// Now set the new environment
SetTaskHostEnvironment(taskConfiguration.BuildProcessEnvironment);
+#if !CLR2COMPATIBILITY
+ DotnetHostEnvironmentHelper.ClearBootstrapDotnetRootEnvironment(taskConfiguration.BuildProcessEnvironment);
+#endif
// Set culture
Thread.CurrentThread.CurrentCulture = taskConfiguration.Culture;
diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs
index 095fc4311e9..9bfc0f99562 100644
--- a/src/MSBuild/XMake.cs
+++ b/src/MSBuild/XMake.cs
@@ -284,9 +284,7 @@ private static void HandleStaticConstructorException(Exception ex)
public static int Main(string[] args)
{
// When running on CoreCLR(.NET), insert the command executable path as the first element of the args array.
- // This is needed because on .NET the first element of Environment.CommandLine is the dotnet executable path
- // and not the msbuild executable path. CoreCLR version didn't support Environment.CommandLine initially, so
- // workaround was needed.
+ // Use the native process path if available.
#if NET
args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args];
#else
diff --git a/src/Shared/BuildEnvironmentHelper.cs b/src/Shared/BuildEnvironmentHelper.cs
index 3a0c945eb7d..3a5e80e2e8f 100644
--- a/src/Shared/BuildEnvironmentHelper.cs
+++ b/src/Shared/BuildEnvironmentHelper.cs
@@ -33,11 +33,6 @@ internal sealed class BuildEnvironmentHelper
///
private static readonly string[] s_msBuildProcess = { "MSBUILD", "MSBUILDTASKHOST" };
- ///
- /// Name of MSBuild executable files.
- ///
- private static readonly string[] s_msBuildExeNames = { "MSBuild.exe", "MSBuild.dll" };
-
///
/// Gets the cached Build Environment instance.
///
@@ -81,7 +76,7 @@ private static BuildEnvironment Initialize()
TryFromEnvironmentVariable,
TryFromVisualStudioProcess,
TryFromMSBuildProcess,
- TryFromMSBuildAssembly,
+ TryFromMSBuildAppHost,
TryFromDevConsole,
TryFromSetupApi,
TryFromAppContextBaseDirectory
@@ -160,36 +155,42 @@ private static BuildEnvironment TryFromVisualStudioProcess()
private static BuildEnvironment TryFromMSBuildProcess()
{
- var msBuildExe = s_getProcessFromRunningProcess();
- if (!IsProcessInList(msBuildExe, s_msBuildProcess))
+ var processName = s_getProcessFromRunningProcess();
+ if (!IsProcessInList(processName, s_msBuildProcess))
{
return null;
}
// First check if we're in a VS installation
if (NativeMethodsShared.IsWindows &&
- Regex.IsMatch(msBuildExe, $@".*\\MSBuild\\{CurrentToolsVersion}\\Bin\\.*MSBuild(?:TaskHost)?\.exe", RegexOptions.IgnoreCase))
+ Regex.IsMatch(processName, $@".*\\MSBuild\\{CurrentToolsVersion}\\Bin\\.*MSBuild(?:TaskHost)?\.exe", RegexOptions.IgnoreCase))
{
return new BuildEnvironment(
BuildEnvironmentMode.VisualStudio,
- msBuildExe,
+ processName,
runningTests: false,
runningInMSBuildExe: true,
runningInVisualStudio: false,
- visualStudioPath: GetVsRootFromMSBuildAssembly(msBuildExe));
+ visualStudioPath: GetVsRootFromMSBuildAssembly(processName));
}
- // Standalone mode running in MSBuild.exe
+ // Standalone mode - may be running in MSBuild.exe app host or dotnet MSBuild.dll
return new BuildEnvironment(
BuildEnvironmentMode.Standalone,
- msBuildExe,
+ processName,
runningTests: false,
- runningInMSBuildExe: true,
+ runningInMSBuildExe: IsRunningInMSBuildExe(processName),
runningInVisualStudio: false,
visualStudioPath: null);
}
- private static BuildEnvironment TryFromMSBuildAssembly()
+ // Check if we're actually running from MSBuild.exe app host (not dotnet MSBuild.dll).
+ // GetProcessFromRunningProcess returns the entry assembly location (MSBuild.dll) when
+ // running via dotnet, so we need to check the actual process path to distinguish.
+ private static bool IsRunningInMSBuildExe(string processPath) =>
+ !string.IsNullOrEmpty(processPath) && processPath.EndsWith(Constants.MSBuildExecutableName, StringComparison.OrdinalIgnoreCase);
+
+ private static BuildEnvironment TryFromMSBuildAppHost()
{
var buildAssembly = s_getExecutingAssemblyPath();
if (buildAssembly == null)
@@ -197,12 +198,11 @@ private static BuildEnvironment TryFromMSBuildAssembly()
return null;
}
- // Check for MSBuild.[exe|dll] next to the current assembly
- var msBuildExe = Path.Combine(FileUtilities.GetFolderAbove(buildAssembly), "MSBuild.exe");
- var msBuildDll = Path.Combine(FileUtilities.GetFolderAbove(buildAssembly), "MSBuild.dll");
+ // Check for MSBuild.[exe] next to the current assembly
+ var msBuildExecutableCandidate = Path.Combine(Path.GetDirectoryName(buildAssembly), Constants.MSBuildExecutableName);
// First check if we're in a VS installation
- var environment = TryFromMSBuildExeUnderVisualStudio(msBuildExe);
+ var environment = TryFromMSBuildExeUnderVisualStudio(msBuildExecutableCandidate);
if (environment != null)
{
return environment;
@@ -210,13 +210,9 @@ private static BuildEnvironment TryFromMSBuildAssembly()
// We're not in VS, check for MSBuild.exe / dll to consider this a standalone environment.
string msBuildPath = null;
- if (FileSystems.Default.FileExists(msBuildExe))
+ if (FileSystems.Default.FileExists(msBuildExecutableCandidate))
{
- msBuildPath = msBuildExe;
- }
- else if (FileSystems.Default.FileExists(msBuildDll))
- {
- msBuildPath = msBuildDll;
+ msBuildPath = msBuildExecutableCandidate;
}
if (!string.IsNullOrEmpty(msBuildPath))
@@ -330,10 +326,10 @@ private static BuildEnvironment TryFromAppContextBaseDirectory()
return null;
}
- // Look for possible MSBuild exe names in the AppContextBaseDirectory
- return s_msBuildExeNames
- .Select((name) => TryFromStandaloneMSBuildExe(Path.Combine(appContextBaseDirectory, name)))
- .FirstOrDefault(env => env != null);
+ // Prioritize MSBuild[.exe] over MSBuild.dll
+ return TryFromStandaloneMSBuildExe(Path.Combine(appContextBaseDirectory, Constants.MSBuildExecutableName))
+ // Fall back to MSBuild.dll
+ ?? TryFromStandaloneMSBuildExe(Path.Combine(appContextBaseDirectory, Constants.MSBuildAssemblyName));
}
private static BuildEnvironment TryFromStandaloneMSBuildExe(string msBuildExePath)
@@ -372,7 +368,7 @@ private static string GetMSBuildExeFromVsRoot(string visualStudioRoot)
"Bin",
NativeMethodsShared.ProcessorArchitecture == Framework.NativeMethods.ProcessorArchitectures.X64 ? "amd64" :
NativeMethodsShared.ProcessorArchitecture == Framework.NativeMethods.ProcessorArchitectures.ARM64 ? "arm64" : string.Empty,
- "MSBuild.exe");
+ Constants.MSBuildExecutableName);
}
private static bool? _runningTests;
@@ -426,14 +422,19 @@ private static bool IsProcessInList(string processName, string[] processList)
private static string GetProcessFromRunningProcess()
{
#if RUNTIME_TYPE_NETCORE
- // The EntryAssembly property can return null when a managed assembly has been loaded from
- // an unmanaged application (for example, using custom CLR hosting).
- if (AssemblyUtilities.EntryAssembly == null)
+
+ // Respect the case when app host isn't available yet and we still in dotnet MSBuild.dll environment
+ string processName = EnvironmentUtilities.ProcessPath;
+ if (IsRunningInMSBuildExe(processName))
{
- return EnvironmentUtilities.ProcessPath;
+ return processName;
}
- return AssemblyUtilities.GetAssemblyLocation(AssemblyUtilities.EntryAssembly);
+ // EntryAssembly can be null in some hosting scenarios (e.g., when loaded as a library)
+ var entryAssembly = AssemblyUtilities.EntryAssembly;
+ return entryAssembly != null
+ ? AssemblyUtilities.GetAssemblyLocation(entryAssembly)
+ : processName;
#else
return EnvironmentUtilities.ProcessPath;
@@ -538,8 +539,13 @@ internal enum BuildEnvironmentMode
///
internal sealed class BuildEnvironment
{
- public BuildEnvironment(BuildEnvironmentMode mode, string currentMSBuildExePath, bool runningTests, bool runningInMSBuildExe, bool runningInVisualStudio,
- string visualStudioPath)
+ public BuildEnvironment(
+ BuildEnvironmentMode mode,
+ string currentMSBuildExePath,
+ bool runningTests,
+ bool runningInMSBuildExe,
+ bool runningInVisualStudio,
+ string visualStudioPath)
{
FileInfo currentMSBuildExeFile = null;
DirectoryInfo currentToolsDirectory = null;
@@ -562,7 +568,13 @@ public BuildEnvironment(BuildEnvironmentMode mode, string currentMSBuildExePath,
currentToolsDirectory = currentMSBuildExeFile.Directory;
CurrentMSBuildToolsDirectory = currentMSBuildExeFile.DirectoryName;
- CurrentMSBuildConfigurationFile = string.Concat(currentMSBuildExePath, ".config");
+ const string configFileExtension =
+#if NET
+ ".dll.config"; // Compat with what we looked for before 18.5
+#else
+ ".exe.config";
+#endif
+ CurrentMSBuildConfigurationFile = Path.ChangeExtension(currentMSBuildExePath, configFileExtension);
MSBuildToolsDirectory32 = CurrentMSBuildToolsDirectory;
MSBuildToolsDirectory64 = CurrentMSBuildToolsDirectory;
MSBuildToolsDirectoryRoot = CurrentMSBuildToolsDirectory;
diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs
index a80c985bff0..8c4f6a50167 100644
--- a/src/Shared/CommunicationsUtilities.cs
+++ b/src/Shared/CommunicationsUtilities.cs
@@ -262,10 +262,11 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string too
#if NETFRAMEWORK
ErrorUtilities.VerifyThrow(
toolsDirectory is null || IsNetTaskHost || IsClr2TaskHost,
- $"{toolsDirectory} should only be provided for .NET or CLR2 TaskHost nodes (and only when running on .NET Framework).");
+ $"{toolsDirectory} should only be provided for .NET or CLR2 TaskHost nodes.");
#else
+ // IsNetTaskHost covers the case when NET process spawns NET TaskHost.
ErrorUtilities.VerifyThrow(
- toolsDirectory is null,
+ toolsDirectory is null || IsNetTaskHost,
$"{toolsDirectory} should not have been provided.");
#endif
@@ -297,8 +298,7 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string too
: CreateStandardComponents(options, salt, sessionId);
}
- private bool IsNetTaskHost
- => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.NET | HandshakeOptions.TaskHost);
+ private bool IsNetTaskHost => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.NET | HandshakeOptions.TaskHost);
#if NETFRAMEWORK
private bool IsClr2TaskHost
diff --git a/src/Shared/EnvironmentUtilities.cs b/src/Shared/EnvironmentUtilities.cs
index b64e792b53d..a66cbb26251 100644
--- a/src/Shared/EnvironmentUtilities.cs
+++ b/src/Shared/EnvironmentUtilities.cs
@@ -5,20 +5,12 @@
using System;
using System.Diagnostics;
-using System.Runtime.InteropServices;
using System.Threading;
namespace Microsoft.Build.Shared
{
internal static partial class EnvironmentUtilities
{
-#if NET472_OR_GREATER || NETCOREAPP
- public static bool Is64BitProcess => Marshal.SizeOf() == 8;
-
- public static bool Is64BitOperatingSystem =>
- Environment.Is64BitOperatingSystem;
-#endif
-
#if !NETCOREAPP
private static volatile int s_processId;
private static volatile string? s_processPath;
@@ -33,7 +25,7 @@ public static int CurrentProcessId
#if NETCOREAPP
return Environment.ProcessId;
#else
- // copied from Environment.ProcessId
+ // copied from Environment.ProcessPath
int processId = s_processId;
if (processId == 0)
{
@@ -70,7 +62,7 @@ public static string? ProcessPath
// The value is cached both as a performance optimization and to ensure that the API always returns
// the same path in a given process.
using Process currentProcess = Process.GetCurrentProcess();
- Interlocked.CompareExchange(ref s_processPath, currentProcess.MainModule.FileName ?? "", null);
+ Interlocked.CompareExchange(ref s_processPath, currentProcess?.MainModule?.FileName ?? "", null);
processPath = s_processPath;
Debug.Assert(processPath != null);
}
diff --git a/src/Shared/FrameworkLocationHelper.cs b/src/Shared/FrameworkLocationHelper.cs
index a7e5d74b727..6762aad8d66 100644
--- a/src/Shared/FrameworkLocationHelper.cs
+++ b/src/Shared/FrameworkLocationHelper.cs
@@ -1434,7 +1434,7 @@ public virtual string GetPathToDotNetFramework(DotNetFrameworkArchitecture archi
// Rollback see https://developercommunity.visualstudio.com/t/Unable-to-locate-MSBuild-path-with-Lates/10824132
if (this._hasMsBuild &&
generatedPathToDotNetFramework != null &&
- (!FileSystems.Default.FileExists(Path.Combine(generatedPathToDotNetFramework, NativeMethodsShared.IsWindows ? "MSBuild.exe" : "mcs.exe")) &&
+ (!FileSystems.Default.FileExists(Path.Combine(generatedPathToDotNetFramework, Constants.MSBuildExecutableName)) &&
!FileSystems.Default.FileExists(Path.Combine(generatedPathToDotNetFramework, "Microsoft.Build.dll"))))
{
return null;
diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs
index 47f590a448f..d67d48c49f1 100644
--- a/src/Shared/INodePacket.cs
+++ b/src/Shared/INodePacket.cs
@@ -344,12 +344,14 @@ internal static class NodePacketTypeExtensions
///
/// null: CLR2 (NET35) task host. Version-dependent fields skipped (not compiled in NET35).
/// 0: The constant value for Framework-to-Framework (CLR4) task host. Supports HostServices, TargetName, ProjectFile.
- /// 2+: .NET task host with full support for version-dependent fields.
+ /// 1: .NET task host support.
+ /// 2: Added support for translating/reading HostServices, ProjectFile, TargetName in TaskHostConfiguration.
+ /// 3: Added App Host support.
///
/// When incrementing this version, ensure compatibility with existing
/// task hosts and update the corresponding deserialization logic.
///
- public const byte PacketVersion = 2;
+ public const byte PacketVersion = 3;
// Flag bits in upper 2 bits
private const byte ExtendedHeaderFlag = 0x40; // Bit 6: 01000000
diff --git a/src/Shared/UnitTests/TestAssemblyInfo.cs b/src/Shared/UnitTests/TestAssemblyInfo.cs
index 39020c6c7db..2654f8f55c8 100644
--- a/src/Shared/UnitTests/TestAssemblyInfo.cs
+++ b/src/Shared/UnitTests/TestAssemblyInfo.cs
@@ -7,9 +7,11 @@
using System.Runtime.InteropServices;
using System.Xml;
using System.Xml.Linq;
+using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.UnitTests;
+using Microsoft.Build.UnitTests.Shared;
using Xunit;
#nullable disable
@@ -63,6 +65,9 @@ public MSBuildTestAssemblyFixture()
_testEnvironment.DoNotLaunchDebugger();
+ var bootstrapCorePath = Path.Combine(Path.Combine(RunnerUtilities.BootstrapRootPath, "core"), Constants.DotnetProcessName);
+ _testEnvironment.SetEnvironmentVariable(Constants.DotnetHostPathEnvVarName, bootstrapCorePath);
+
// Reset the VisualStudioVersion environment variable. This will be set if tests are run from a VS command prompt. However,
// if the environment variable is set, it will interfere with tests which set the SubToolsetVersion
// (VerifySubToolsetVersionSetByConstructorOverridable), as the environment variable would take precedence.
@@ -72,8 +77,6 @@ public MSBuildTestAssemblyFixture()
// https://github.com/dotnet/msbuild/pull/6274
_testEnvironment.SetEnvironmentVariable("DOTNET_PERFLOG_DIR", null);
- SetDotnetHostPath(_testEnvironment);
-
// Use a project-specific temporary path
// This is so multiple test projects can be run in parallel without sharing the same temp directory
var subdirectory = Path.GetRandomFileName();
@@ -107,49 +110,7 @@ public MSBuildTestAssemblyFixture()
fileName: "Directory.Build.targets",
contents: "");
}
-
- ///
- /// Find correct version of "dotnet", and set DOTNET_HOST_PATH so that the Roslyn tasks will use the right host
- ///
- ///
- private static void SetDotnetHostPath(TestEnvironment testEnvironment)
- {
- var currentFolder = AppContext.BaseDirectory;
-
- while (currentFolder != null)
- {
- string potentialVersionsPropsPath = Path.Combine(currentFolder, "build", "Versions.props");
- if (FileSystems.Default.FileExists(potentialVersionsPropsPath))
- {
- XDocument doc = null;
- var xrs = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true, IgnoreWhitespace = true };
- using (XmlReader xr = XmlReader.Create(File.OpenRead(potentialVersionsPropsPath), xrs))
- {
- doc = XDocument.Load(xr);
- }
-
- var ns = doc.Root.Name.Namespace;
- var cliVersionElement = doc.Root.Elements(ns + "PropertyGroup").Elements(ns + "DotNetCliVersion").FirstOrDefault();
- if (cliVersionElement != null)
- {
- string cliVersion = cliVersionElement.Value;
- string dotnetPath = Path.Combine(currentFolder, "artifacts", ".dotnet", cliVersion, "dotnet");
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- dotnetPath += ".exe";
- }
-
- testEnvironment.SetEnvironmentVariable("DOTNET_HOST_PATH", dotnetPath);
- }
-
- break;
- }
-
- currentFolder = Directory.GetParent(currentFolder)?.FullName;
- }
- }
-
+
public void Dispose()
{
if (!_disposed)
diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs
index 0253b720504..713c5613419 100644
--- a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs
+++ b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs
@@ -899,7 +899,7 @@ public void RoslynCodeTaskFactory_UsingAPI(bool forceOutOfProc)
}
RunnerUtilities.ApplyDotnetHostPathEnvironmentVariable(env);
- var dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
+ var dotnetPath = Environment.GetEnvironmentVariable(Constants.DotnetHostPathEnvVarName);
var project = env.CreateTestProjectWithFiles("p1.proj", text);
var logger = project.BuildProjectExpectSuccess();
diff --git a/src/UnitTests.Shared/BootstrapLocationAttribute.cs b/src/UnitTests.Shared/BootstrapLocationAttribute.cs
index bc0d0211dfb..2d941f62882 100644
--- a/src/UnitTests.Shared/BootstrapLocationAttribute.cs
+++ b/src/UnitTests.Shared/BootstrapLocationAttribute.cs
@@ -14,7 +14,7 @@ internal sealed class BootstrapLocationAttribute(string bootstrapRoot, string bo
public string BootstrapRoot { get; } = bootstrapRoot;
///
- /// Resolves path to MSBuild.exe or MSBuild.dll, depending on the runtime.
+ /// Resolves path to MSBuild[.exe], depending on the runtime and OS.
///
public string BootstrapMsBuildBinaryLocation { get; } = bootstrapMsBuildBinaryLocation;
diff --git a/src/UnitTests.Shared/RunnerUtilities.cs b/src/UnitTests.Shared/RunnerUtilities.cs
index e6ac57de4c5..0874b996907 100644
--- a/src/UnitTests.Shared/RunnerUtilities.cs
+++ b/src/UnitTests.Shared/RunnerUtilities.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
@@ -58,14 +59,7 @@ public static string ExecMSBuild(string msbuildParameters, out bool successfulEx
///
public static string ExecMSBuild(string pathToMsBuildExe, string msbuildParameters, out bool successfulExit, bool shellExecute = false, ITestOutputHelper outputHelper = null)
{
-#if FEATURE_RUN_EXE_IN_TESTS
- var pathToExecutable = pathToMsBuildExe;
-#else
- var pathToExecutable = s_dotnetExePath;
- msbuildParameters = FileUtilities.EnsureDoubleQuotes(pathToMsBuildExe) + " " + msbuildParameters;
-#endif
-
- return RunProcessAndGetOutput(pathToExecutable, msbuildParameters, out successfulExit, shellExecute, outputHelper);
+ return RunProcessAndGetOutput(pathToMsBuildExe, msbuildParameters, out successfulExit, shellExecute, outputHelper, environmentVariables: GetMSBuildEnvironmentVariables());
}
public static string ExecBootstrapedMSBuild(
@@ -77,12 +71,28 @@ public static string ExecBootstrapedMSBuild(
int timeoutMilliseconds = 30_000)
{
#if NET
- string pathToExecutable = EnvironmentProvider.GetDotnetExePathFromFolder(BootstrapMsBuildBinaryLocation);
- msbuildParameters = Path.Combine(BootstrapMsBuildBinaryLocation, "sdk", BootstrapLocationAttribute.BootstrapSdkVersion, Constants.MSBuildAssemblyName) + " " + msbuildParameters;
+ string pathToExecutable = Path.Combine(BootstrapMsBuildBinaryLocation, "sdk", BootstrapLocationAttribute.BootstrapSdkVersion, Constants.MSBuildExecutableName);
#else
string pathToExecutable = Path.Combine(BootstrapMsBuildBinaryLocation, Constants.MSBuildExecutableName);
#endif
- return RunProcessAndGetOutput(pathToExecutable, msbuildParameters, out successfulExit, shellExecute, outputHelper, attachProcessId, timeoutMilliseconds);
+ return RunProcessAndGetOutput(pathToExecutable, msbuildParameters, out successfulExit, shellExecute, outputHelper, attachProcessId, timeoutMilliseconds, environmentVariables: GetMSBuildEnvironmentVariables());
+ }
+
+ ///
+ /// Returns environment variables that should be set when launching MSBuild as a child process.
+ /// On .NET Core, this includes DOTNET_HOST_PATH so that tasks like RoslynCodeTaskFactory
+ /// can locate the dotnet host even when MSBuild runs as a native app host.
+ ///
+ private static Dictionary GetMSBuildEnvironmentVariables()
+ {
+#if !FEATURE_RUN_EXE_IN_TESTS
+ return new Dictionary
+ {
+ [Constants.DotnetHostPathEnvVarName] = s_dotnetExePath,
+ };
+#else
+ return null;
+#endif
}
private static void AdjustForShellExecution(ref string pathToExecutable, ref string arguments)
@@ -111,7 +121,8 @@ public static string RunProcessAndGetOutput(
bool shellExecute = false,
ITestOutputHelper outputHelper = null,
bool attachProcessId = true,
- int timeoutMilliseconds = 30_000)
+ int timeoutMilliseconds = 30_000,
+ Dictionary environmentVariables = null)
{
if (shellExecute)
{
@@ -128,6 +139,14 @@ public static string RunProcessAndGetOutput(
UseShellExecute = false,
Arguments = parameters
};
+
+ if (environmentVariables != null)
+ {
+ foreach (var kvp in environmentVariables)
+ {
+ psi.Environment[kvp.Key] = kvp.Value;
+ }
+ }
string output = string.Empty;
int pid = -1;
diff --git a/src/UnitTests.Shared/TestEnvironment.cs b/src/UnitTests.Shared/TestEnvironment.cs
index 586afed20e0..f031a0bd834 100644
--- a/src/UnitTests.Shared/TestEnvironment.cs
+++ b/src/UnitTests.Shared/TestEnvironment.cs
@@ -10,9 +10,11 @@
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
+using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.Debugging;
using Microsoft.Build.Shared.FileSystem;
+using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
diff --git a/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs b/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs
index 4104fc438d5..a1b959497c8 100644
--- a/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs
+++ b/src/Utilities.UnitTests/ToolLocationHelper_Tests.cs
@@ -34,12 +34,6 @@ public sealed class ToolLocationHelper_Tests
private readonly ITestOutputHelper _output;
#endif
-#if USE_MSBUILD_DLL_EXTN
- private const string MSBuildExeName = "MSBuild.dll";
-#else
- private const string MSBuildExeName = "MSBuild.exe";
-#endif
-
public ToolLocationHelper_Tests(ITestOutputHelper output)
{
#if FEATURE_CODETASKFACTORY
@@ -682,20 +676,20 @@ public void ExerciseMiscToolLocationHelperMethods()
[Fact]
public void TestGetPathToBuildToolsFile()
{
- string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("MSBuild.exe", TargetDotNetFrameworkVersion.Version20);
+ string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile(Constants.MSBuildExecutableName, TargetDotNetFrameworkVersion.Version20);
- net20Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("MSBuild.exe", "2.0"));
+ net20Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, "2.0"));
- string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("MSBuild.exe", TargetDotNetFrameworkVersion.Version35);
+ string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile(Constants.MSBuildExecutableName, TargetDotNetFrameworkVersion.Version35);
- net35Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("MSBuild.exe", "3.5"));
+ net35Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, "3.5"));
- ToolLocationHelper.GetPathToDotNetFrameworkFile("MSBuild.exe", TargetDotNetFrameworkVersion.Version40).ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("MSBuild.exe", "4.0"));
+ ToolLocationHelper.GetPathToDotNetFrameworkFile(Constants.MSBuildExecutableName, TargetDotNetFrameworkVersion.Version40).ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, "4.0"));
- string tv12path = Path.Combine(ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion).ToolsPath, MSBuildExeName);
+ string tv12path = Path.Combine(ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion).ToolsPath, Constants.MSBuildExecutableName);
- tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(MSBuildExeName, ObjectModelHelpers.MSBuildDefaultToolsVersion));
- tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(MSBuildExeName, ToolLocationHelper.CurrentToolsVersion));
+ tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, ObjectModelHelpers.MSBuildDefaultToolsVersion));
+ tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, ToolLocationHelper.CurrentToolsVersion));
}
#if RUNTIME_TYPE_NETCORE
@@ -705,20 +699,20 @@ public void TestGetPathToBuildToolsFile()
#endif
public void TestGetPathToBuildToolsFile_32Bit()
{
- string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version20, UtilitiesDotNetFrameworkArchitecture.Bitness32);
- net20Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "2.0", UtilitiesDotNetFrameworkArchitecture.Bitness32));
+ string net20Path = ToolLocationHelper.GetPathToDotNetFrameworkFile(Constants.MSBuildExecutableName, TargetDotNetFrameworkVersion.Version20, UtilitiesDotNetFrameworkArchitecture.Bitness32);
+ net20Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, "2.0", UtilitiesDotNetFrameworkArchitecture.Bitness32));
- string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version35, UtilitiesDotNetFrameworkArchitecture.Bitness32);
- net35Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "3.5", UtilitiesDotNetFrameworkArchitecture.Bitness32));
+ string net35Path = ToolLocationHelper.GetPathToDotNetFrameworkFile(Constants.MSBuildExecutableName, TargetDotNetFrameworkVersion.Version35, UtilitiesDotNetFrameworkArchitecture.Bitness32);
+ net35Path?.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, "3.5", UtilitiesDotNetFrameworkArchitecture.Bitness32));
- ToolLocationHelper.GetPathToDotNetFrameworkFile("msbuild.exe", TargetDotNetFrameworkVersion.Version40, UtilitiesDotNetFrameworkArchitecture.Bitness32).ShouldBe(
- ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", "4.0", UtilitiesDotNetFrameworkArchitecture.Bitness32));
+ ToolLocationHelper.GetPathToDotNetFrameworkFile(Constants.MSBuildExecutableName, TargetDotNetFrameworkVersion.Version40, UtilitiesDotNetFrameworkArchitecture.Bitness32).ShouldBe(
+ ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, "4.0", UtilitiesDotNetFrameworkArchitecture.Bitness32));
var toolsPath32 = ProjectCollection.GlobalProjectCollection.GetToolset(ObjectModelHelpers.MSBuildDefaultToolsVersion).Properties["MSBuildToolsPath32"];
- string tv12path = Path.Combine(Path.GetFullPath(toolsPath32.EvaluatedValue), "msbuild.exe");
+ string tv12path = Path.Combine(Path.GetFullPath(toolsPath32.EvaluatedValue), Constants.MSBuildExecutableName);
- tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ObjectModelHelpers.MSBuildDefaultToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness32));
- tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile("msbuild.exe", ToolLocationHelper.CurrentToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness32));
+ tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, ObjectModelHelpers.MSBuildDefaultToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness32));
+ tv12path.ShouldBe(ToolLocationHelper.GetPathToBuildToolsFile(Constants.MSBuildExecutableName, ToolLocationHelper.CurrentToolsVersion, UtilitiesDotNetFrameworkArchitecture.Bitness32));
}
[Fact]