diff --git a/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs b/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs index 1eba7068e821..0c08fd266380 100644 --- a/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs +++ b/src/RazorSdk/Tasks/FindAssembliesWithReferencesTo.cs @@ -9,8 +9,12 @@ namespace Microsoft.AspNetCore.Razor.Tasks { - public class FindAssembliesWithReferencesTo : Task + [MSBuildMultiThreadableTask] + public class FindAssembliesWithReferencesTo : Task, IMultiThreadableTask { + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + [Required] public ITaskItem[] TargetAssemblyNames { get; set; } @@ -44,7 +48,7 @@ public override bool Execute() var targetAssemblyNames = TargetAssemblyNames.Select(s => s.ItemSpec).ToList(); - var provider = new ReferenceResolver((IReadOnlyList)targetAssemblyNames, referenceItems); + var provider = new ReferenceResolver((IReadOnlyList)targetAssemblyNames, referenceItems, TaskEnvironment); try { var assemblyNames = provider.ResolveAssemblies(); diff --git a/src/RazorSdk/Tasks/ReferenceResolver.cs b/src/RazorSdk/Tasks/ReferenceResolver.cs index 597c43931f68..c0f0357578f3 100644 --- a/src/RazorSdk/Tasks/ReferenceResolver.cs +++ b/src/RazorSdk/Tasks/ReferenceResolver.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; +using Microsoft.Build.Framework; namespace Microsoft.AspNetCore.Razor.Tasks { @@ -15,9 +16,25 @@ public class ReferenceResolver private readonly HashSet _mvcAssemblies; private readonly IReadOnlyList _assemblyItems; private readonly Dictionary _classifications; + private readonly TaskEnvironment _taskEnvironment = TaskEnvironment.Fallback; - public ReferenceResolver(IReadOnlyList targetAssemblies, IReadOnlyList assemblyItems) + public ReferenceResolver( + IReadOnlyList targetAssemblies, + IReadOnlyList assemblyItems) + : this(targetAssemblies, assemblyItems, null) { + } + + public ReferenceResolver( + IReadOnlyList targetAssemblies, + IReadOnlyList assemblyItems, + TaskEnvironment? taskEnvironment) + { + if (taskEnvironment != null) + { + _taskEnvironment = taskEnvironment; + } + _mvcAssemblies = new HashSet(targetAssemblies, StringComparer.Ordinal); _assemblyItems = assemblyItems; _classifications = new Dictionary(); @@ -103,12 +120,14 @@ protected virtual IReadOnlyList GetReferences(string file) { try { - if (!File.Exists(file)) + var absoluteFilePath = !String.IsNullOrEmpty(file) ? _taskEnvironment.GetAbsolutePath(file) : file; + + if (!File.Exists(absoluteFilePath)) { throw new ReferenceAssemblyNotFoundException(file); } - using var peReader = new PEReader(File.OpenRead(file)); + using var peReader = new PEReader(File.OpenRead(absoluteFilePath)); if (!peReader.HasMetadata) { return Array.Empty(); // not a managed assembly diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs new file mode 100644 index 000000000000..cf8c78416c2c --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/FindAssembliesWithReferencesToMultiThreadingTest.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System.Reflection; +using Microsoft.AspNetCore.Razor.Tasks; +using Microsoft.Build.Framework; +using Moq; +using TaskItem = Microsoft.Build.Utilities.TaskItem; + +namespace Microsoft.NET.Sdk.Razor.Test +{ + public class FindAssembliesWithReferencesToMultiThreadingTest + { + [Fact] + public void GetReferences_RelativePath_ResolvesAgainstTaskEnvironmentProjectDirectory() + { + // The point of the migration: relative paths must be absolutized against + // TaskEnvironment.ProjectDirectory instead of the process CWD. The file is placed + // in a temp directory distinct from CWD so pre-migration code would not find it. + using var temp = new TempDirectory(); + const string fileName = "stub.dll"; + File.WriteAllBytes(Path.Combine(temp.Path, fileName), new byte[] { 0 }); + Directory.GetCurrentDirectory().Should().NotBe(temp.Path, + "test must run with CWD distinct from the temp dir so the migration is actually exercised"); + + var resolver = CreateExposingResolver(TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(temp.Path)); + + resolver.CallGetReferences(fileName).Should().BeEmpty( + "the file exists at TaskEnvironment.ProjectDirectory; PEReader's BadImageFormatException is swallowed and an empty list returned"); + } + + + [Fact] + public void Execute_PassesTaskEnvironmentToResolver_AndResolvesRelativeAssemblyItemSpecs() + { + // End-to-end wiring test: FindAssembliesWithReferencesTo must forward its + // TaskEnvironment to the ReferenceResolver so relative ItemSpecs on Assemblies + // are resolved against the project directory, not the process CWD. + using var temp = new TempDirectory(); + const string fileName = "candidate.dll"; + File.WriteAllBytes(Path.Combine(temp.Path, fileName), new byte[] { 0 }); + + var warnings = new List(); + var errors = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogWarningEvent(It.IsAny())) + .Callback(warnings.Add); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(errors.Add); + + var task = new FindAssembliesWithReferencesTo + { + BuildEngine = buildEngine.Object, + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(temp.Path), + TargetAssemblyNames = new ITaskItem[] { new TaskItem("Microsoft.AspNetCore.Mvc") }, + Assemblies = new ITaskItem[] + { + new TaskItem(fileName, new Dictionary + { + ["FusionName"] = "Candidate, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + }), + }, + }; + + task.Execute().Should().BeTrue(); + errors.Should().BeEmpty(); + warnings.Should().NotContain(w => w.Code == "RAZORSDK1007", + "the file is present under TaskEnvironment.ProjectDirectory; no not-found warning should be raised"); + } + + private static ExposingReferenceResolver CreateExposingResolver(TaskEnvironment env) => + new(Array.Empty(), Array.Empty(), env); + + private sealed class ExposingReferenceResolver : ReferenceResolver + { + public ExposingReferenceResolver( + IReadOnlyList targetAssemblies, + IReadOnlyList assemblyItems, + TaskEnvironment taskEnvironment) + : base(targetAssemblies, assemblyItems, taskEnvironment) + { + } + + public IReadOnlyList CallGetReferences(string file) => GetReferences(file); + } + + private sealed class TempDirectory : IDisposable + { + public TempDirectory() + { + Path = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + nameof(FindAssembliesWithReferencesToMultiThreadingTest), + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public void Dispose() + { + try { Directory.Delete(Path, recursive: true); } catch { /* best effort */ } + } + } + } +}