diff --git a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs index cb9e4b35fdd2..b41598f528f3 100644 --- a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs +++ b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs @@ -103,6 +103,7 @@ public ProcessStartInfo GetProcessStartInfo() var startInfo = new ProcessStartInfo { WorkingDirectory = _projectDirectory.Value, + UseShellExecute = false, }; // Populate environment from the scoped environment dictionary diff --git a/src/Tasks/Common/TaskEnvironmentDefaults.cs b/src/Tasks/Common/TaskEnvironmentDefaults.cs new file mode 100644 index 000000000000..7ef5666a4175 --- /dev/null +++ b/src/Tasks/Common/TaskEnvironmentDefaults.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Provides a default TaskEnvironment for single-threaded MSBuild execution. +// When MSBuild supports IMultiThreadableTask, it sets TaskEnvironment directly. +// This fallback ensures tasks work with older MSBuild versions that do not set it. + +#if NETFRAMEWORK + +using System; + +namespace Microsoft.Build.Framework +{ + internal static class TaskEnvironmentDefaults + { + /// + /// Creates a default TaskEnvironment backed by the current process environment. + /// Uses Environment.CurrentDirectory as the project directory, which in single-threaded + /// MSBuild is set to the project directory before task execution. + /// + internal static TaskEnvironment Create() => + new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory)); + } +} + +#endif diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentAssetsMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentAssetsMultiThreading.cs new file mode 100644 index 000000000000..b706b7749088 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentAssetsMultiThreading.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAProduceContentAssetsMultiThreading + { + [Fact] + public void ContentPreprocessorOutputDirectory_IsResolvedRelativeToProjectDirectory() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"produce-mt-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + try + { + // Create a relative output directory under projectDir + var ppOutputDir = Path.Combine(projectDir, "obj", "pp"); + Directory.CreateDirectory(ppOutputDir); + + var contentFile = new MockTaskItem("path/to/content.cs", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Compile" }, + { "CodeLanguage", "any" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + ContentFileDependencies = new ITaskItem[] { contentFile }, + ContentPreprocessorOutputDirectory = Path.Combine("obj", "pp"), + ProjectLanguage = "C#", + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + }; + + var result = task.Execute(); + + result.Should().BeTrue("task should succeed with relative ContentPreprocessorOutputDirectory resolved via TaskEnvironment"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + [Fact] + public void ItProducesSameResultsRegardlessOfCwd() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"produce-parity-{Guid.NewGuid():N}")); + var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"produce-decoy-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + try + { + var contentFile1 = new MockTaskItem("path/to/content.cs", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Compile" }, + { "CodeLanguage", "any" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + var contentFile2 = new MockTaskItem("path/to/content.cs", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Compile" }, + { "CodeLanguage", "any" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + + var taskEnv = TaskEnvironmentHelper.CreateForTest(projectDir); + + // --- Multiprocess mode: CWD == projectDir --- + Directory.SetCurrentDirectory(projectDir); + var engine1 = new MockBuildEngine(); + var task1 = new ProduceContentAssets + { + BuildEngine = engine1, + ContentFileDependencies = new ITaskItem[] { contentFile1 }, + ProjectLanguage = "C#", + TaskEnvironment = taskEnv, + }; + var result1 = task1.Execute(); + + // --- Multithreaded mode: CWD == otherDir --- + Directory.SetCurrentDirectory(otherDir); + var engine2 = new MockBuildEngine(); + var task2 = new ProduceContentAssets + { + BuildEngine = engine2, + ContentFileDependencies = new ITaskItem[] { contentFile2 }, + ProjectLanguage = "C#", + TaskEnvironment = taskEnv, + }; + var result2 = task2.Execute(); + + result1.Should().Be(result2, + "task should return the same result regardless of CWD"); + engine1.Errors.Count.Should().Be(engine2.Errors.Count, + "error count should be the same in both environments"); + task1.ProcessedContentItems.Length.Should().Be(task2.ProcessedContentItems.Length, + "ProcessedContentItems count should be identical regardless of CWD"); + task1.CopyLocalItems.Length.Should().Be(task2.CopyLocalItems.Length, + "CopyLocalItems count should be identical regardless of CWD"); + task1.FileWrites.Length.Should().Be(task2.FileWrites.Length, + "FileWrites count should be identical regardless of CWD"); + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentsAssetsTask.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentsAssetsTask.cs index bfbbde77794b..6b3785b11a80 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentsAssetsTask.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentsAssetsTask.cs @@ -46,6 +46,8 @@ public void ItProcessesContentFiles() ContentPreprocessorOutputDirectory = ContentOutputDirectory, ProjectLanguage = null, }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -99,6 +101,8 @@ public void ItOutputsFileWritesForProcessedContent() ContentPreprocessorOutputDirectory = ContentOutputDirectory, ProjectLanguage = null, }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -159,6 +163,8 @@ public void ItOutputsCopyLocalItems() ContentPreprocessorOutputDirectory = ContentOutputDirectory, ProjectLanguage = null, }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -235,6 +241,8 @@ public void ItOutputsContentItemsWithActiveBuildAction() ContentPreprocessorOutputDirectory = ContentOutputDirectory, ProjectLanguage = null, }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -318,6 +326,8 @@ public void ItCanOutputOnlyPreprocessedItems() ProduceOnlyPreprocessorFiles = true, ProjectLanguage = null, }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -373,6 +383,8 @@ public void ItIgnoresProjectLanguageIfCodeLanguageIsOnlyAny() ContentPreprocessorOutputDirectory = null, ProjectLanguage = "C#", }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -415,6 +427,8 @@ public void ItProcessesOnlyProjectLanguageIfPresent() ContentPreprocessorOutputDirectory = null, ProjectLanguage = "C#", }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -459,6 +473,8 @@ public void ItProcessesOnlyAnyItemsIfProjectLanguageNotPresent() ContentPreprocessorOutputDirectory = null, ProjectLanguage = "C#", }; + task.BuildEngine = new MockBuildEngine(); + task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Directory.GetCurrentDirectory()); task.Execute().Should().BeTrue(); // Asserts @@ -482,8 +498,8 @@ public void ItProcessesOnlyAnyItemsIfProjectLanguageNotPresent() #region Sample Test Data - private static readonly string ContentOutputDirectory = Path.Combine("bin", "obj"); - private static readonly string PackageRootDirectory = Path.Combine("root", "packages"); + private static readonly string ContentOutputDirectory = Path.GetFullPath(Path.Combine("bin", "obj")); + private static readonly string PackageRootDirectory = Path.GetFullPath(Path.Combine("root", "packages")); private static ITaskItem[] GetPreprocessorValueItems(Dictionary values) => values.Select(kvp => new MockTaskItem( diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveCopyLocalAssetsMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveCopyLocalAssetsMultiThreading.cs new file mode 100644 index 000000000000..30aa7b2878e0 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveCopyLocalAssetsMultiThreading.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAResolveCopyLocalAssetsMultiThreading + { + private const string AssetsJson = """ + { + "version": 3, + "targets": { ".NETCoreApp,Version=v8.0": {} }, + "libraries": {}, + "packageFolders": {}, + "projectFileDependencyGroups": { ".NETCoreApp,Version=v8.0": [] }, + "project": { + "version": "1.0.0", + "frameworks": { "net8.0": {} } + } + } + """; + + [Fact] + public void AssetsFilePath_IsResolvedRelativeToProjectDirectory() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"copyloc-mt-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + try + { + var objDir = Path.Combine(projectDir, "obj"); + Directory.CreateDirectory(objDir); + File.WriteAllText(Path.Combine(objDir, "project.assets.json"), AssetsJson); + + var task = new ResolveCopyLocalAssets + { + BuildEngine = new MockBuildEngine(), + AssetsFilePath = Path.Combine("obj", "project.assets.json"), + TargetFramework = ".NETCoreApp,Version=v8.0", + RuntimeIdentifier = "", + IsSelfContained = false, + ResolveRuntimeTargets = false, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + }; + + var result = task.Execute(); + + result.Should().BeTrue("task should succeed when assets file is found via TaskEnvironment"); + task.ResolvedAssets.Should().BeEmpty("no packages in the lock file means no assets"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + [Fact] + public void ItProducesSameResultsInMultiProcessAndMultiThreadedEnvironments() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"copyloc-parity-{Guid.NewGuid():N}")); + var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"copyloc-decoy-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + try + { + var objDir = Path.Combine(projectDir, "obj"); + Directory.CreateDirectory(objDir); + File.WriteAllText(Path.Combine(objDir, "project.assets.json"), AssetsJson); + + var assetsRelPath = Path.Combine("obj", "project.assets.json"); + var taskEnv = TaskEnvironmentHelper.CreateForTest(projectDir); + + // --- Multiprocess mode: CWD == projectDir --- + Directory.SetCurrentDirectory(projectDir); + var engine1 = new MockBuildEngine(); + var task1 = new ResolveCopyLocalAssets + { + BuildEngine = engine1, + AssetsFilePath = assetsRelPath, + TargetFramework = ".NETCoreApp,Version=v8.0", + RuntimeIdentifier = "", + IsSelfContained = false, + ResolveRuntimeTargets = false, + TaskEnvironment = taskEnv, + }; + var result1 = task1.Execute(); + + // --- Multithreaded mode: CWD == otherDir --- + Directory.SetCurrentDirectory(otherDir); + var engine2 = new MockBuildEngine(); + var task2 = new ResolveCopyLocalAssets + { + BuildEngine = engine2, + AssetsFilePath = assetsRelPath, + TargetFramework = ".NETCoreApp,Version=v8.0", + RuntimeIdentifier = "", + IsSelfContained = false, + ResolveRuntimeTargets = false, + TaskEnvironment = taskEnv, + }; + var result2 = task2.Execute(); + + result1.Should().Be(result2, + "task should return the same result regardless of CWD"); + engine1.Errors.Count.Should().Be(engine2.Errors.Count, + "error count should be identical in both environments"); + task1.ResolvedAssets.Length.Should().Be(task2.ResolvedAssets.Length, + "ResolvedAssets count should be identical regardless of CWD"); + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup6.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup6.cs new file mode 100644 index 000000000000..0fe452b0d8cb --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup6.cs @@ -0,0 +1,708 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + /// + /// Behavioral and attribute-presence tests for tasks in merge-group-6. + /// GetDefaultPlatformTargetForNetFramework, GetEmbeddedApphostPaths, and + /// GetNuGetShortFolderName are attribute-only. ProduceContentAssets and + /// ResolveCopyLocalAssets were migrated to Pattern B (IMultiThreadableTask). + /// + public class GivenAttributeOnlyTasksGroup6 + { + #region GetDefaultPlatformTargetForNetFramework + + [Fact] + public void GetDefaultPlatformTargetForNetFramework_ReturnsAnyCPU_WhenNoNativeAssets() + { + var task = new GetDefaultPlatformTargetForNetFramework + { + BuildEngine = new MockBuildEngine(), + PackageDependencies = Array.Empty(), + NativeCopyLocalItems = Array.Empty() + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.DefaultPlatformTarget.Should().Be("AnyCPU"); + } + + [Fact] + public void GetDefaultPlatformTargetForNetFramework_ReturnsAnyCPU_WhenNullNativeAssets() + { + var task = new GetDefaultPlatformTargetForNetFramework + { + BuildEngine = new MockBuildEngine(), + PackageDependencies = null, + NativeCopyLocalItems = null + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.DefaultPlatformTarget.Should().Be("AnyCPU"); + } + + [Fact] + public void GetDefaultPlatformTargetForNetFramework_ReturnsX86_WhenPlatformsPackagePresent() + { + var platformsPkg = new TaskItem("Microsoft.NETCore.Platforms"); + var nativeItem = new MockTaskItem("native.dll", new Dictionary + { + { "PathInPackage", "runtimes/win-x64/native/native.dll" } + }); + + var task = new GetDefaultPlatformTargetForNetFramework + { + BuildEngine = new MockBuildEngine(), + PackageDependencies = new ITaskItem[] { platformsPkg }, + NativeCopyLocalItems = new ITaskItem[] { nativeItem } + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.DefaultPlatformTarget.Should().Be("x86", + "presence of NETCore.Platforms package with native assets implies x86"); + } + + [Fact] + public void GetDefaultPlatformTargetForNetFramework_ReturnsX86_WhenWin7X86NativeAsset() + { + var nativeItem = new MockTaskItem("native.dll", new Dictionary + { + { "PathInPackage", "runtimes/win7-x86/native/native.dll" } + }); + + var task = new GetDefaultPlatformTargetForNetFramework + { + BuildEngine = new MockBuildEngine(), + PackageDependencies = Array.Empty(), + NativeCopyLocalItems = new ITaskItem[] { nativeItem } + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.DefaultPlatformTarget.Should().Be("x86", + "win7-x86 native assets without Platforms package should still return x86"); + } + + [Fact] + public void GetDefaultPlatformTargetForNetFramework_ReturnsAnyCPU_WhenNonX86NativeAsset() + { + var nativeItem = new MockTaskItem("native.dll", new Dictionary + { + { "PathInPackage", "runtimes/win-x64/native/native.dll" } + }); + + var task = new GetDefaultPlatformTargetForNetFramework + { + BuildEngine = new MockBuildEngine(), + PackageDependencies = Array.Empty(), + NativeCopyLocalItems = new ITaskItem[] { nativeItem } + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.DefaultPlatformTarget.Should().Be("AnyCPU", + "non-win7-x86 native assets without Platforms package should return AnyCPU"); + } + + #endregion + + #region GetEmbeddedApphostPaths + + [Fact] + public void GetEmbeddedApphostPaths_ProducesPathsForEachRid() + { + var rids = new[] + { + new TaskItem("win-x64"), + new TaskItem("linux-x64"), + new TaskItem("osx-x64") + }; + + var task = new GetEmbeddedApphostPaths + { + BuildEngine = new MockBuildEngine(), + ToolCommandName = "mytool", + PackagedShimOutputDirectory = "shims", + ShimRuntimeIdentifiers = rids + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.EmbeddedApphostPaths.Should().HaveCount(3); + } + + [Fact] + public void GetEmbeddedApphostPaths_WindowsRid_HasExeExtension() + { + var rids = new[] { new TaskItem("win-x64") }; + + var task = new GetEmbeddedApphostPaths + { + BuildEngine = new MockBuildEngine(), + ToolCommandName = "mytool", + PackagedShimOutputDirectory = "shims", + ShimRuntimeIdentifiers = rids + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.EmbeddedApphostPaths.Should().HaveCount(1); + task.EmbeddedApphostPaths[0].ItemSpec.Should().EndWith(".exe"); + task.EmbeddedApphostPaths[0].GetMetadata("ShimRuntimeIdentifier").Should().Be("win-x64"); + } + + [Fact] + public void GetEmbeddedApphostPaths_LinuxRid_HasNoExtension() + { + var rids = new[] { new TaskItem("linux-x64") }; + + var task = new GetEmbeddedApphostPaths + { + BuildEngine = new MockBuildEngine(), + ToolCommandName = "mytool", + PackagedShimOutputDirectory = "shims", + ShimRuntimeIdentifiers = rids + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.EmbeddedApphostPaths[0].ItemSpec.Should().NotEndWith(".exe"); + task.EmbeddedApphostPaths[0].ItemSpec.Should().EndWith("mytool"); + } + + [Fact] + public void GetEmbeddedApphostPaths_CombinesOutputDirRidAndToolName() + { + var rids = new[] { new TaskItem("win-x64") }; + + var task = new GetEmbeddedApphostPaths + { + BuildEngine = new MockBuildEngine(), + ToolCommandName = "mytool", + PackagedShimOutputDirectory = "output", + ShimRuntimeIdentifiers = rids + }; + + task.Execute(); + + var path = task.EmbeddedApphostPaths[0].ItemSpec; + path.Should().Contain("output"); + path.Should().Contain("win-x64"); + path.Should().Contain("mytool"); + } + + #endregion + + #region GetNuGetShortFolderName + + [Fact] + public void GetNuGetShortFolderName_ReturnsNet80() + { + var task = new GetNuGetShortFolderName + { + BuildEngine = new MockBuildEngine(), + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.NuGetShortFolderName.Should().Be("net8.0"); + } + + [Fact] + public void GetNuGetShortFolderName_ReturnsNetStandard20() + { + var task = new GetNuGetShortFolderName + { + BuildEngine = new MockBuildEngine(), + TargetFrameworkMoniker = ".NETStandard,Version=v2.0" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.NuGetShortFolderName.Should().Be("netstandard2.0"); + } + + [Fact] + public void GetNuGetShortFolderName_ReturnsNet472() + { + var task = new GetNuGetShortFolderName + { + BuildEngine = new MockBuildEngine(), + TargetFrameworkMoniker = ".NETFramework,Version=v4.7.2" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.NuGetShortFolderName.Should().Be("net472"); + } + + [Fact] + public void GetNuGetShortFolderName_WithPlatformMoniker_IncludesPlatform() + { + var task = new GetNuGetShortFolderName + { + BuildEngine = new MockBuildEngine(), + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0", + TargetPlatformMoniker = "Windows,Version=10.0.19041.0" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.NuGetShortFolderName.Should().Contain("windows"); + } + + #endregion + + #region ProduceContentAssets + + [Fact] + public void ProduceContentAssets_WithCompileAsset_ProducesContentItem() + { + var contentFile = new MockTaskItem("path/to/content.cs", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Compile" }, + { "CodeLanguage", "any" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + ContentFileDependencies = new ITaskItem[] { contentFile }, + ProjectLanguage = "C#" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.ProcessedContentItems.Should().HaveCount(1); + task.ProcessedContentItems[0].GetMetadata("ProcessedItemType").Should().Be("Compile"); + } + + [Fact] + public void ProduceContentAssets_WithNoneBuildAction_ProducesNoContentItem() + { + var contentFile = new MockTaskItem("path/to/readme.txt", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "None" }, + { "CodeLanguage", "any" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + ContentFileDependencies = new ITaskItem[] { contentFile }, + ProjectLanguage = "C#" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.ProcessedContentItems.Should().BeEmpty( + "items with BuildAction=None should not produce content items"); + } + + [Fact] + public void ProduceContentAssets_WithCopyToOutput_ProducesCopyLocalItem() + { + var contentFile = new MockTaskItem("path/to/data.json", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Content" }, + { "CodeLanguage", "any" }, + { "CopyToOutput", "true" }, + { "PPOutputPath", "" }, + { "OutputPath", "data.json" } + }); + + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + ContentFileDependencies = new ITaskItem[] { contentFile }, + ProjectLanguage = "C#" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.CopyLocalItems.Should().HaveCount(1); + task.CopyLocalItems[0].GetMetadata("TargetPath").Should().Be("data.json"); + } + + [Fact] + public void ProduceContentAssets_FiltersLanguageSpecificAssets() + { + var csharpFile = new MockTaskItem("path/to/code.cs", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Compile" }, + { "CodeLanguage", "cs" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + + var vbFile = new MockTaskItem("path/to/code.vb", new Dictionary + { + { "NuGetPackageId", "MyPackage" }, + { "NuGetPackageVersion", "1.0.0" }, + { "BuildAction", "Compile" }, + { "CodeLanguage", "vb" }, + { "CopyToOutput", "false" }, + { "PPOutputPath", "" }, + { "OutputPath", "" } + }); + + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + ContentFileDependencies = new ITaskItem[] { csharpFile, vbFile }, + ProjectLanguage = "C#" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.ProcessedContentItems.Should().HaveCount(1, + "only C# language-specific content should be included for a C# project"); + } + + [Fact] + public void ProduceContentAssets_EmptyDependencies_Succeeds() + { + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + ContentFileDependencies = Array.Empty(), + ProjectLanguage = "C#", + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.ProcessedContentItems.Should().BeEmpty(); + task.CopyLocalItems.Should().BeEmpty(); + task.FileWrites.Should().BeEmpty(); + } + + #endregion + + #region ResolveCopyLocalAssets + + [Fact] + public void ResolveCopyLocalAssets_WithMinimalAssetsFile_Succeeds() + { + var projectDir = Path.Combine(Path.GetTempPath(), $"resolve-copy-{Guid.NewGuid():N}"); + Directory.CreateDirectory(projectDir); + try + { + var assetsContent = @"{ + ""version"": 3, + ""targets"": { "".NETCoreApp,Version=v8.0"": {} }, + ""libraries"": {}, + ""packageFolders"": {}, + ""projectFileDependencyGroups"": { "".NETCoreApp,Version=v8.0"": [] }, + ""project"": { ""version"": ""1.0.0"", ""frameworks"": { ""net8.0"": {} } } + }"; + var assetsPath = Path.Combine(projectDir, "project.assets.json"); + File.WriteAllText(assetsPath, assetsContent); + + var task = new ResolveCopyLocalAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + AssetsFilePath = assetsPath, + TargetFramework = ".NETCoreApp,Version=v8.0", + RuntimeIdentifier = "", + IsSelfContained = false, + ResolveRuntimeTargets = false + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.ResolvedAssets.Should().BeEmpty("no packages in the lock file means no assets"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + #endregion + + #region Concurrent Execution + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task GetDefaultPlatformTargetForNetFramework_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new GetDefaultPlatformTargetForNetFramework + { + BuildEngine = new MockBuildEngine(), + PackageDependencies = Array.Empty(), + NativeCopyLocalItems = Array.Empty() + }; + + startGate.Wait(); + task.Execute(); + + if (task.DefaultPlatformTarget != "AnyCPU") + { + errors.Add($"Thread {idx}: Expected 'AnyCPU' but got '{task.DefaultPlatformTarget}'"); + } + } + catch (Exception ex) + { + errors.Add($"Thread {idx}: {ex.Message}"); + } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task GetEmbeddedApphostPaths_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new GetEmbeddedApphostPaths + { + BuildEngine = new MockBuildEngine(), + ToolCommandName = "mytool", + PackagedShimOutputDirectory = "shims", + ShimRuntimeIdentifiers = new ITaskItem[] { new TaskItem("win-x64") } + }; + + startGate.Wait(); + task.Execute(); + + if (task.EmbeddedApphostPaths == null || task.EmbeddedApphostPaths.Length != 1) + { + errors.Add($"Thread {idx}: Expected 1 path but got {task.EmbeddedApphostPaths?.Length}"); + } + } + catch (Exception ex) + { + errors.Add($"Thread {idx}: {ex.Message}"); + } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task GetNuGetShortFolderName_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new GetNuGetShortFolderName + { + BuildEngine = new MockBuildEngine(), + TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0" + }; + + startGate.Wait(); + task.Execute(); + + if (task.NuGetShortFolderName != "net8.0") + { + errors.Add($"Thread {idx}: Expected 'net8.0' but got '{task.NuGetShortFolderName}'"); + } + } + catch (Exception ex) + { + errors.Add($"Thread {idx}: {ex.Message}"); + } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ProduceContentAssets_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ProduceContentAssets + { + BuildEngine = new MockBuildEngine(), + ContentFileDependencies = Array.Empty(), + ProjectLanguage = "C#", + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() + }; + + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) + { + errors.Add($"Thread {idx}: {ex.Message}"); + } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ResolveCopyLocalAssets_ConcurrentExecution(int parallelism) + { + var projectDir = Path.Combine(Path.GetTempPath(), $"resolve-copy-concurrent-{Guid.NewGuid():N}"); + Directory.CreateDirectory(projectDir); + try + { + var assetsContent = @"{ + ""version"": 3, + ""targets"": { "".NETCoreApp,Version=v8.0"": {} }, + ""libraries"": {}, + ""packageFolders"": {}, + ""projectFileDependencyGroups"": { "".NETCoreApp,Version=v8.0"": [] }, + ""project"": { ""version"": ""1.0.0"", ""frameworks"": { ""net8.0"": {} } } + }"; + var assetsPath = Path.Combine(projectDir, "project.assets.json"); + File.WriteAllText(assetsPath, assetsContent); + + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ResolveCopyLocalAssets + { + BuildEngine = new MockBuildEngine(), + AssetsFilePath = assetsPath, + TargetFramework = ".NETCoreApp,Version=v8.0", + RuntimeIdentifier = "", + IsSelfContained = false, + ResolveRuntimeTargets = false, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir) + }; + + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) + { + errors.Add($"Thread {idx}: {ex.Message}"); + } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + #endregion + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProduceContentAssets.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProduceContentAssets.cs index 387b5e836c16..ada31abb422d 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProduceContentAssets.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProduceContentAssets.cs @@ -14,13 +14,25 @@ namespace Microsoft.NET.Build.Tasks /// other filtering on content assets, including whether they match the active /// project language. /// - public sealed class ProduceContentAssets : TaskBase + [MSBuildMultiThreadableTask] + public sealed class ProduceContentAssets : TaskBase, IMultiThreadableTask { private readonly List _contentItems = new(); private readonly List _fileWrites = new(); private readonly List _copyLocalItems = new(); private IContentAssetPreprocessor _assetPreprocessor; +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } +#endif + #region Output Items /// @@ -141,7 +153,7 @@ protected override void ExecuteCore() Log.LogWarning(Strings.DuplicatePreprocessorToken, duplicatedPreprocessorKey, preprocessorValues[duplicatedPreprocessorKey]); } - AssetPreprocessor.ConfigurePreprocessor(ContentPreprocessorOutputDirectory, preprocessorValues); + AssetPreprocessor.ConfigurePreprocessor(TaskEnvironment.GetAbsolutePath(ContentPreprocessorOutputDirectory), preprocessorValues); } var contentFileDeps = ContentFileDependencies ?? Enumerable.Empty(); @@ -193,7 +205,7 @@ private bool IsPreprocessorFile(ITaskItem contentFile) => private void ProduceContentAsset(ITaskItem contentFile) { - string resolvedPath = contentFile.ItemSpec; + string resolvedPath = TaskEnvironment.GetAbsolutePath(contentFile.ItemSpec); string pathToFinalAsset = resolvedPath; string ppOutputPath = contentFile.GetMetadata(MetadataKeys.PPOutputPath); string packageName = contentFile.GetMetadata(MetadataKeys.NuGetPackageId); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveCopyLocalAssets.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveCopyLocalAssets.cs index 2533f79e9bf8..e1c14660cbd8 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveCopyLocalAssets.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveCopyLocalAssets.cs @@ -13,10 +13,22 @@ namespace Microsoft.NET.Build.Tasks /// /// Resolves the assets from the package dependencies that should be copied to output/publish directories. /// - public class ResolveCopyLocalAssets : TaskBase + [MSBuildMultiThreadableTask] + public class ResolveCopyLocalAssets : TaskBase, IMultiThreadableTask { private readonly List _resolvedAssets = new(); +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } +#endif + public string AssetsFilePath { get; set; } [Required] @@ -44,7 +56,7 @@ public class ResolveCopyLocalAssets : TaskBase protected override void ExecuteCore() { var lockFileCache = new LockFileCache(this); - LockFile lockFile = lockFileCache.GetLockFile(AssetsFilePath); + LockFile lockFile = lockFileCache.GetLockFile(TaskEnvironment.GetAbsolutePath(AssetsFilePath)); HashSet packagestoBeFiltered = null; if (RuntimeStorePackages != null && RuntimeStorePackages.Length > 0)