diff --git a/src/Tasks.UnitTests/GetReferencePaths_Tests.cs b/src/Tasks.UnitTests/GetReferencePaths_Tests.cs
index f4352c0322a..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.
///
@@ -328,6 +337,91 @@ 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 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);
+ }
+
+ ///
+ /// Test that a relative path in TargetFrameworkFallbackSearchPaths resolved via TaskEnvironment
+ /// produces the same result as an absolute fallback path.
+ ///
+ [Fact]
+ public void TestRelativeFallbackSearchPathProducesSameResultAsAbsolute()
+ {
+ 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);
+ }
}
}
#endif
diff --git a/src/Tasks/GetReferenceAssemblyPaths.cs b/src/Tasks/GetReferenceAssemblyPaths.cs
index 81b02cdf5a0..fd2cf698a3e 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,16 @@ 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 = string.Join(";", fallbackSearchPaths);
+
IList pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies(
frameworkmoniker.Identifier,
frameworkmoniker.Version.ToString(),
frameworkmoniker.Profile,
- rootPath,
- targetFrameworkFallbackSearchPaths);
+ rootPath?.Value,
+ fallbackSearchPathsJoined);
if (!SuppressNotFoundError)
{
@@ -267,6 +285,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
}
}