From 1b772ef583eb7a2387cde86fcd0a6ee5e306d795 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Tue, 7 Apr 2026 10:20:58 +0200 Subject: [PATCH 1/2] Migrate GetReferenceAssemblyPaths task --- .../GetReferencePaths_Tests.cs | 87 +++++++++++++++++++ src/Tasks/GetReferenceAssemblyPaths.cs | 76 ++++++++++++---- 2 files changed, 145 insertions(+), 18 deletions(-) diff --git a/src/Tasks.UnitTests/GetReferencePaths_Tests.cs b/src/Tasks.UnitTests/GetReferencePaths_Tests.cs index f4352c0322a..140c3328747 100644 --- a/src/Tasks.UnitTests/GetReferencePaths_Tests.cs +++ b/src/Tasks.UnitTests/GetReferencePaths_Tests.cs @@ -328,6 +328,93 @@ public void TestGeneralFrameworkMonikerGoodWithFrameworkInFallbackPaths() Assert.Equal(".NET Framework 4.1", displayName); } } + + /// + /// Test that a relative RootPath resolved via TaskEnvironment produces the same result as an absolute RootPath. + /// + [Fact] + public void TestRelativeRootPathProducesSameResultAsAbsolute() + { + using (var env = TestEnvironment.Create()) + { + string baseDir = env.DefaultTestDirectory.Path; + string relativeDir = "framework-root"; + string absoluteDir = Path.Combine(baseDir, relativeDir); + var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar)); + var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList")); + env.CreateFile(redistListDirectory, "FrameworkList.xml", + "" + + "" + + ""); + + // Baseline: absolute RootPath + MockEngine absoluteEngine = new MockEngine(); + GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths(); + absoluteTask.BuildEngine = absoluteEngine; + absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + absoluteTask.RootPath = absoluteDir; + absoluteTask.Execute(); + + // Test: relative RootPath with TaskEnvironment + MockEngine relativeEngine = new MockEngine(); + GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths(); + relativeTask.BuildEngine = relativeEngine; + relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir); + relativeTask.RootPath = relativeDir; + relativeTask.Execute(); + + Assert.Equal(absoluteTask.ReferenceAssemblyPaths, relativeTask.ReferenceAssemblyPaths); + Assert.Equal(absoluteTask.TargetFrameworkMonikerDisplayName, relativeTask.TargetFrameworkMonikerDisplayName); + Assert.Equal(0, relativeEngine.Errors); + } + } + + /// + /// Test that a relative path in TargetFrameworkFallbackSearchPaths resolved via TaskEnvironment + /// produces the same result as an absolute fallback path. + /// + [Fact] + public void TestRelativeFallbackSearchPathProducesSameResultAsAbsolute() + { + using (var env = TestEnvironment.Create()) + { + string baseDir = env.DefaultTestDirectory.Path; + string relativeDir = "framework-root"; + string absoluteDir = Path.Combine(baseDir, relativeDir); + var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar)); + var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList")); + env.CreateFile(redistListDirectory, "FrameworkList.xml", + "" + + "" + + ""); + + string nonExistentRoot = Path.Combine(baseDir, "nonexistent"); + + // Baseline: absolute fallback path + MockEngine absoluteEngine = new MockEngine(); + GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths(); + absoluteTask.BuildEngine = absoluteEngine; + absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + absoluteTask.RootPath = nonExistentRoot; + absoluteTask.TargetFrameworkFallbackSearchPaths = absoluteDir; + absoluteTask.Execute(); + + // Test: relative fallback path with TaskEnvironment + MockEngine relativeEngine = new MockEngine(); + GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths(); + relativeTask.BuildEngine = relativeEngine; + relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir); + relativeTask.RootPath = nonExistentRoot; + relativeTask.TargetFrameworkFallbackSearchPaths = relativeDir; + relativeTask.Execute(); + + Assert.Equal(absoluteTask.ReferenceAssemblyPaths, relativeTask.ReferenceAssemblyPaths); + Assert.Equal(absoluteTask.TargetFrameworkMonikerDisplayName, relativeTask.TargetFrameworkMonikerDisplayName); + Assert.Equal(0, relativeEngine.Errors); + } + } } } #endif diff --git a/src/Tasks/GetReferenceAssemblyPaths.cs b/src/Tasks/GetReferenceAssemblyPaths.cs index 81b02cdf5a0..b6c0e95170f 100644 --- a/src/Tasks/GetReferenceAssemblyPaths.cs +++ b/src/Tasks/GetReferenceAssemblyPaths.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using Microsoft.Build.Framework; +using Microsoft.Build.Shared; using Microsoft.Build.Utilities; using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; #if FEATURE_GAC -using Microsoft.Build.Shared; +using System.Threading; using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture; #endif @@ -19,8 +20,12 @@ namespace Microsoft.Build.Tasks /// /// Returns the reference assembly paths to the various frameworks /// - public class GetReferenceAssemblyPaths : TaskExtension + [MSBuildMultiThreadableTask] + public class GetReferenceAssemblyPaths : TaskExtension, IMultiThreadableTask { + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + #region Data #if FEATURE_GAC /// @@ -32,7 +37,23 @@ public class GetReferenceAssemblyPaths : TaskExtension /// /// Cache in a static whether or not we have found the 35sp1sentinel assembly. /// - private static bool? s_net35SP1SentinelAssemblyFound; + private static readonly Lazy s_net35SP1SentinelAssemblyFound = new Lazy(() => + { + // get an assemblyname from the string representation of the sentinel assembly name + var sentinelAssemblyName = new AssemblyNameExtension(NET35SP1SentinelAssemblyName); + string path = GlobalAssemblyCache.GetLocation( + sentinelAssemblyName, + SystemProcessorArchitecture.MSIL, + runtimeVersion => "v2.0.50727", + new Version("2.0.57027"), + false, + new FileExists(p => FileUtilities.FileExistsNoThrow(p)), + GlobalAssemblyCache.pathFromFusionName, + GlobalAssemblyCache.gacEnumerator, + false); + + return !string.IsNullOrEmpty(path); + }, LazyThreadSafetyMode.PublicationOnly); #endif /// @@ -144,6 +165,11 @@ public string TargetFrameworkFallbackSearchPaths /// public override bool Execute() { + AbsolutePath? absoluteRootPath = !string.IsNullOrEmpty(RootPath) + ? TaskEnvironment.GetAbsolutePath(RootPath) + : new AbsolutePath(RootPath, ignoreRootedCheck: true); + IList absoluteFallbackSearchPaths = ResolveAbsoluteFallbackSearchPaths(TargetFrameworkFallbackSearchPaths); + FrameworkNameVersioning moniker; FrameworkNameVersioning monikerWithNoProfile = null; @@ -169,16 +195,6 @@ public override bool Execute() if (!BypassFrameworkInstallChecks && moniker.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase) && moniker.Version.Major < 4) { - // We have not got a value for whether or not the 35 sentinel assembly has been found - if (!s_net35SP1SentinelAssemblyFound.HasValue) - { - // get an assemblyname from the string representation of the sentinel assembly name - var sentinelAssemblyName = new AssemblyNameExtension(NET35SP1SentinelAssemblyName); - - string path = GlobalAssemblyCache.GetLocation(sentinelAssemblyName, SystemProcessorArchitecture.MSIL, runtimeVersion => "v2.0.50727", new Version("2.0.57027"), false, new FileExists(p => FileUtilities.FileExistsNoThrow(p)), GlobalAssemblyCache.pathFromFusionName, GlobalAssemblyCache.gacEnumerator, false); - s_net35SP1SentinelAssemblyFound = !String.IsNullOrEmpty(path); - } - // We did not find the SP1 sentinel assembly in the GAC. Therefore we must assume that SP1 isn't installed if (!s_net35SP1SentinelAssemblyFound.Value) { @@ -195,7 +211,7 @@ public override bool Execute() try { - _tfmPaths = GetPaths(RootPath, TargetFrameworkFallbackSearchPaths, moniker); + _tfmPaths = GetPaths(absoluteRootPath, absoluteFallbackSearchPaths, moniker); if (_tfmPaths?.Count > 0) { @@ -206,7 +222,7 @@ public override bool Execute() // There is no point in generating the full framework paths if profile path could not be found. if (targetingProfile && _tfmPaths != null) { - _tfmPathsNoProfile = GetPaths(RootPath, TargetFrameworkFallbackSearchPaths, monikerWithNoProfile); + _tfmPathsNoProfile = GetPaths(absoluteRootPath, absoluteFallbackSearchPaths, monikerWithNoProfile); } // The path with out the profile is just the reference assembly paths. @@ -236,14 +252,18 @@ public override bool Execute() /// /// Generate the set of chained reference assembly paths /// - private IList GetPaths(string rootPath, string targetFrameworkFallbackSearchPaths, FrameworkNameVersioning frameworkmoniker) + private IList GetPaths(AbsolutePath? rootPath, IList fallbackSearchPaths, FrameworkNameVersioning frameworkmoniker) { + string fallbackSearchPathsJoined = fallbackSearchPaths.Count > 0 + ? string.Join(";", fallbackSearchPaths) + : null; + IList pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies( frameworkmoniker.Identifier, frameworkmoniker.Version.ToString(), frameworkmoniker.Profile, - rootPath, - targetFrameworkFallbackSearchPaths); + rootPath?.Value, + fallbackSearchPathsJoined); if (!SuppressNotFoundError) { @@ -267,6 +287,26 @@ private IList GetPaths(string rootPath, string targetFrameworkFallbackSe return pathsToReturn; } + /// + /// Resolves each semicolon-separated fallback search path to absolute via TaskEnvironment. + /// + private IList ResolveAbsoluteFallbackSearchPaths(string fallbackSearchPaths) + { + if (string.IsNullOrEmpty(fallbackSearchPaths)) + { + return []; + } + + string[] parts = fallbackSearchPaths.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + var result = new AbsolutePath[parts.Length]; + for (int i = 0; i < parts.Length; i++) + { + result[i] = TaskEnvironment.GetAbsolutePath(parts[i]); + } + + return result; + } + #endregion } } From 8336d94a9fa5d9a5192aab7e991353a3d465799e Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Wed, 8 Apr 2026 08:40:00 +0200 Subject: [PATCH 2/2] Test fixes --- .../GetReferencePaths_Tests.cs | 147 +++++++++--------- src/Tasks/GetReferenceAssemblyPaths.cs | 4 +- 2 files changed, 78 insertions(+), 73 deletions(-) diff --git a/src/Tasks.UnitTests/GetReferencePaths_Tests.cs b/src/Tasks.UnitTests/GetReferencePaths_Tests.cs index 140c3328747..6e83c21d309 100644 --- a/src/Tasks.UnitTests/GetReferencePaths_Tests.cs +++ b/src/Tasks.UnitTests/GetReferencePaths_Tests.cs @@ -7,7 +7,9 @@ using Microsoft.Build.Shared; using Microsoft.Build.Tasks; using Microsoft.Build.Utilities; +using Shouldly; using Xunit; +using Xunit.Abstractions; using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName; #if !RUNTIME_TYPE_NETCORE @@ -20,6 +22,13 @@ namespace Microsoft.Build.UnitTests /// public sealed class GetReferenceAssmeblyPath_Tests { + private readonly ITestOutputHelper _output; + + public GetReferenceAssmeblyPath_Tests(ITestOutputHelper output) + { + _output = output; + } + /// /// Test the case where there is a good target framework moniker passed in. /// @@ -335,39 +344,38 @@ public void TestGeneralFrameworkMonikerGoodWithFrameworkInFallbackPaths() [Fact] public void TestRelativeRootPathProducesSameResultAsAbsolute() { - using (var env = TestEnvironment.Create()) - { - string baseDir = env.DefaultTestDirectory.Path; - string relativeDir = "framework-root"; - string absoluteDir = Path.Combine(baseDir, relativeDir); - var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar)); - var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList")); - env.CreateFile(redistListDirectory, "FrameworkList.xml", - "" + - "" + - ""); - - // Baseline: absolute RootPath - MockEngine absoluteEngine = new MockEngine(); - GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths(); - absoluteTask.BuildEngine = absoluteEngine; - absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; - absoluteTask.RootPath = absoluteDir; - absoluteTask.Execute(); - - // Test: relative RootPath with TaskEnvironment - MockEngine relativeEngine = new MockEngine(); - GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths(); - relativeTask.BuildEngine = relativeEngine; - relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; - relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir); - relativeTask.RootPath = relativeDir; - relativeTask.Execute(); - - Assert.Equal(absoluteTask.ReferenceAssemblyPaths, relativeTask.ReferenceAssemblyPaths); - Assert.Equal(absoluteTask.TargetFrameworkMonikerDisplayName, relativeTask.TargetFrameworkMonikerDisplayName); - Assert.Equal(0, relativeEngine.Errors); - } + using TestEnvironment env = TestEnvironment.Create(_output); + + string baseDir = env.DefaultTestDirectory.Path; + string relativeDir = "framework-root"; + string absoluteDir = Path.Combine(baseDir, relativeDir); + var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar)); + var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList")); + env.CreateFile(redistListDirectory, "FrameworkList.xml", + "" + + "" + + ""); + + // Baseline: absolute RootPath + MockEngine absoluteEngine = new MockEngine(); + GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths(); + absoluteTask.BuildEngine = absoluteEngine; + absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + absoluteTask.RootPath = absoluteDir; + absoluteTask.Execute(); + + // Test: relative RootPath with TaskEnvironment + MockEngine relativeEngine = new MockEngine(); + GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths(); + relativeTask.BuildEngine = relativeEngine; + relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir); + relativeTask.RootPath = relativeDir; + relativeTask.Execute(); + + relativeTask.ReferenceAssemblyPaths.ShouldBe(absoluteTask.ReferenceAssemblyPaths); + relativeTask.TargetFrameworkMonikerDisplayName.ShouldBe(absoluteTask.TargetFrameworkMonikerDisplayName); + relativeEngine.Errors.ShouldBe(0); } /// @@ -377,43 +385,42 @@ public void TestRelativeRootPathProducesSameResultAsAbsolute() [Fact] public void TestRelativeFallbackSearchPathProducesSameResultAsAbsolute() { - using (var env = TestEnvironment.Create()) - { - string baseDir = env.DefaultTestDirectory.Path; - string relativeDir = "framework-root"; - string absoluteDir = Path.Combine(baseDir, relativeDir); - var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar)); - var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList")); - env.CreateFile(redistListDirectory, "FrameworkList.xml", - "" + - "" + - ""); - - string nonExistentRoot = Path.Combine(baseDir, "nonexistent"); - - // Baseline: absolute fallback path - MockEngine absoluteEngine = new MockEngine(); - GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths(); - absoluteTask.BuildEngine = absoluteEngine; - absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; - absoluteTask.RootPath = nonExistentRoot; - absoluteTask.TargetFrameworkFallbackSearchPaths = absoluteDir; - absoluteTask.Execute(); - - // Test: relative fallback path with TaskEnvironment - MockEngine relativeEngine = new MockEngine(); - GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths(); - relativeTask.BuildEngine = relativeEngine; - relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; - relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir); - relativeTask.RootPath = nonExistentRoot; - relativeTask.TargetFrameworkFallbackSearchPaths = relativeDir; - relativeTask.Execute(); - - Assert.Equal(absoluteTask.ReferenceAssemblyPaths, relativeTask.ReferenceAssemblyPaths); - Assert.Equal(absoluteTask.TargetFrameworkMonikerDisplayName, relativeTask.TargetFrameworkMonikerDisplayName); - Assert.Equal(0, relativeEngine.Errors); - } + using TestEnvironment env = TestEnvironment.Create(_output); + + string baseDir = env.DefaultTestDirectory.Path; + string relativeDir = "framework-root"; + string absoluteDir = Path.Combine(baseDir, relativeDir); + var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar)); + var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList")); + env.CreateFile(redistListDirectory, "FrameworkList.xml", + "" + + "" + + ""); + + string nonExistentRoot = Path.Combine(baseDir, "nonexistent"); + + // Baseline: absolute fallback path + MockEngine absoluteEngine = new MockEngine(); + GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths(); + absoluteTask.BuildEngine = absoluteEngine; + absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + absoluteTask.RootPath = nonExistentRoot; + absoluteTask.TargetFrameworkFallbackSearchPaths = absoluteDir; + absoluteTask.Execute(); + + // Test: relative fallback path with TaskEnvironment + MockEngine relativeEngine = new MockEngine(); + GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths(); + relativeTask.BuildEngine = relativeEngine; + relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1"; + relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir); + relativeTask.RootPath = nonExistentRoot; + relativeTask.TargetFrameworkFallbackSearchPaths = relativeDir; + relativeTask.Execute(); + + relativeTask.ReferenceAssemblyPaths.ShouldBe(absoluteTask.ReferenceAssemblyPaths); + relativeTask.TargetFrameworkMonikerDisplayName.ShouldBe(absoluteTask.TargetFrameworkMonikerDisplayName); + relativeEngine.Errors.ShouldBe(0); } } } diff --git a/src/Tasks/GetReferenceAssemblyPaths.cs b/src/Tasks/GetReferenceAssemblyPaths.cs index b6c0e95170f..fd2cf698a3e 100644 --- a/src/Tasks/GetReferenceAssemblyPaths.cs +++ b/src/Tasks/GetReferenceAssemblyPaths.cs @@ -254,9 +254,7 @@ public override bool Execute() /// private IList GetPaths(AbsolutePath? rootPath, IList fallbackSearchPaths, FrameworkNameVersioning frameworkmoniker) { - string fallbackSearchPathsJoined = fallbackSearchPaths.Count > 0 - ? string.Join(";", fallbackSearchPaths) - : null; + string fallbackSearchPathsJoined = string.Join(";", fallbackSearchPaths); IList pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies( frameworkmoniker.Identifier,