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 } }