diff --git a/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs b/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs index a8ebc64a225..d9459be0678 100644 --- a/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs +++ b/src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -35,7 +35,7 @@ public void ManifestPopulationCheck(string? manifestName, bool expectedResult) { AddToWin32Manifest task = new AddToWin32Manifest() { - BuildEngine = new MockEngine(_testOutput) + BuildEngine = new MockEngine(_testOutput), }; using (TestEnvironment env = TestEnvironment.Create()) diff --git a/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs b/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs index 0d6ed50ad59..f3547a4babd 100644 --- a/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs +++ b/src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs @@ -773,9 +773,9 @@ class MyForm /// File mode /// Access type /// The Stream - private Stream CreateFileStream(string path, FileMode mode, FileAccess access) + private Stream CreateFileStream(AbsolutePath path, FileMode mode, FileAccess access) { - if (String.Equals(path, "SR1.strings", StringComparison.OrdinalIgnoreCase)) + if (String.Equals(Path.GetFileName(path.Value), "SR1.strings", StringComparison.OrdinalIgnoreCase)) { return StreamHelpers.StringToStream("namespace MyStuff.Namespace { class Class {} }"); } diff --git a/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs b/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs index 28c15eb8a90..12c684cbb79 100644 --- a/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs +++ b/src/Tasks.UnitTests/CreateVisualBasicManifestResourceName_Tests.cs @@ -410,9 +410,9 @@ End Class /// File mode /// Access type /// The Stream - private Stream CreateFileStream(string path, FileMode mode, FileAccess access) + private Stream CreateFileStream(AbsolutePath path, FileMode mode, FileAccess access) { - if (String.Equals(path, "SR1.strings", StringComparison.OrdinalIgnoreCase)) + if (String.Equals(Path.GetFileName(path.Value), "SR1.strings", StringComparison.OrdinalIgnoreCase)) { return StreamHelpers.StringToStream( @" diff --git a/src/Tasks.UnitTests/ManifestTaskEnvironmentTests.cs b/src/Tasks.UnitTests/ManifestTaskEnvironmentTests.cs new file mode 100644 index 00000000000..fafaed3e411 --- /dev/null +++ b/src/Tasks.UnitTests/ManifestTaskEnvironmentTests.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.UnitTests; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Tasks.UnitTests +{ + /// + /// Tests verifying TaskEnvironment migration compatibility for manifest tasks. + /// These tests focus on path handling changes from the migration. + /// + public class ManifestTaskEnvironmentTests + { + private readonly ITestOutputHelper _output; + + public ManifestTaskEnvironmentTests(ITestOutputHelper output) => _output = output; + // Test 1: Empty ItemSpec - verifies exception handling matches pre-migration behavior + // GetAbsolutePath throws on empty, but this flows through existing exception handling + [Fact] + public void CreateManifestResourceName_EmptyItemSpec_ShouldFail() + { + var engine = new MockEngine(_output); + var task = new CreateCSharpManifestResourceName + { + BuildEngine = engine, + ResourceFiles = new ITaskItem[] { new TaskItem("") }, + RootNamespace = "Test" + }; + + // On .NET Framework: returns false with logged error + // On .NET Core+: throws ArgumentNullException (pre-existing behavior in Path.GetDirectoryName) +#if NETFRAMEWORK + bool result = task.Execute(); + result.ShouldBeFalse(); +#else + Should.Throw(() => task.Execute()); +#endif + } + + // Test 2: Path with .. segments - critical test for canonicalization + // GetAbsolutePath does NOT canonicalize, so we wrap with Path.GetFullPath where needed + [Fact] + public void CreateManifestResourceName_PathWithDotDot_ShouldResolve() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + var subFolder = Path.Combine(folder.Path, "sub"); + Directory.CreateDirectory(subFolder); + + var resxPath = Path.Combine(subFolder, "Test.resx"); + File.WriteAllText(resxPath, ""); + + var csPath = Path.Combine(subFolder, "Test.cs"); + File.WriteAllText(csPath, "namespace Test { class Test { } }"); + + // Use path with .. segments - tests canonicalization + var pathWithDotDot = Path.Combine(folder.Path, "sub", "..", "sub", "Test.resx"); + + var task = new CreateCSharpManifestResourceName + { + BuildEngine = new MockEngine(_output), + ResourceFiles = new ITaskItem[] { new TaskItem(pathWithDotDot) }, + RootNamespace = "Test", + UseDependentUponConvention = true + }; + + bool result = task.Execute(); + result.ShouldBeTrue(); + } + + // Test 3: Forward slashes - tests path normalization + [Fact] + public void CreateManifestResourceName_ForwardSlashes_ShouldWork() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + var subFolder = Path.Combine(folder.Path, "Resources"); + Directory.CreateDirectory(subFolder); + + var resxPath = Path.Combine(subFolder, "Strings.resx"); + File.WriteAllText(resxPath, ""); + + // Replace backslashes with forward slashes + var pathWithForwardSlashes = resxPath.Replace('\\', '/'); + + var task = new CreateCSharpManifestResourceName + { + BuildEngine = new MockEngine(_output), + ResourceFiles = new ITaskItem[] { new TaskItem(pathWithForwardSlashes) }, + RootNamespace = "Test" + }; + + bool result = task.Execute(); + result.ShouldBeTrue(); + } + + // Test 4: Mixed slashes - tests path normalization handles both + [Fact] + public void CreateManifestResourceName_MixedSlashes_ShouldWork() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + var subFolder = Path.Combine(folder.Path, "Sub", "Folder"); + Directory.CreateDirectory(subFolder); + + var resxPath = Path.Combine(subFolder, "Test.resx"); + File.WriteAllText(resxPath, ""); + + // Mix forward and back slashes + var mixedPath = folder.Path + "/Sub\\Folder/Test.resx"; + + var task = new CreateCSharpManifestResourceName + { + BuildEngine = new MockEngine(_output), + ResourceFiles = new ITaskItem[] { new TaskItem(mixedPath) }, + RootNamespace = "Test" + }; + + bool result = task.Execute(); + result.ShouldBeTrue(); + } + + // Test 5: AddToWin32Manifest with null ApplicationManifest - tests graceful handling + [Fact] + public void AddToWin32Manifest_NullApplicationManifest_HandledGracefully() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + + var task = new AddToWin32Manifest + { + BuildEngine = new MockEngine(_output), + ApplicationManifest = null, + OutputDirectory = folder.Path, + SupportedArchitectures = "amd64" + }; + + // Null is treated as "no manifest" - should generate new one + bool result = task.Execute(); + result.ShouldBeTrue(); + } + + // Test 6: Batch processing - one error should not abort remaining items + [Fact] + public void CreateManifestResourceName_BatchProcessing_ContinuesAfterError() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + + // Create one valid resource + var validPath = Path.Combine(folder.Path, "Valid.resx"); + File.WriteAllText(validPath, ""); + + // Create another valid resource + var valid2Path = Path.Combine(folder.Path, "Valid2.resx"); + File.WriteAllText(valid2Path, ""); + + // Invalid: DependentUpon points to non-existent file + var invalidItem = new TaskItem(validPath); + invalidItem.SetMetadata("DependentUpon", "NonExistent.cs"); + + var validItem = new TaskItem(valid2Path); + + var task = new CreateCSharpManifestResourceName + { + BuildEngine = new MockEngine(_output), + ResourceFiles = new ITaskItem[] { invalidItem, validItem }, + RootNamespace = "Test" + }; + + // Should return false due to error, but should still process both items + bool result = task.Execute(); + result.ShouldBeFalse(); + // The valid item should still have been processed successfully + task.ManifestResourceNames[1].ShouldNotBeNull(); + task.ManifestResourceNames[1].ItemSpec.ShouldNotBeNullOrEmpty(); + } + + // Test 7: Deeply nested folder - tests path handling with many segments + [Fact] + public void CreateManifestResourceName_DeepNesting_ShouldWork() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + var deepFolder = Path.Combine(folder.Path, "a", "b", "c", "d", "e"); + Directory.CreateDirectory(deepFolder); + + var resxPath = Path.Combine(deepFolder, "Test.resx"); + File.WriteAllText(resxPath, ""); + + var task = new CreateCSharpManifestResourceName + { + BuildEngine = new MockEngine(_output), + ResourceFiles = new ITaskItem[] { new TaskItem(resxPath) }, + RootNamespace = "Test" + }; + + bool result = task.Execute(); + result.ShouldBeTrue(); + } + + // Test 8: Path with spaces - tests no issues with space handling + [Fact] + public void CreateManifestResourceName_PathWithSpaces_ShouldWork() + { + using var env = TestEnvironment.Create(_output); + var folder = env.CreateFolder(); + var spaceFolder = Path.Combine(folder.Path, "My Resources"); + Directory.CreateDirectory(spaceFolder); + + var resxPath = Path.Combine(spaceFolder, "My Strings.resx"); + File.WriteAllText(resxPath, ""); + + var task = new CreateCSharpManifestResourceName + { + BuildEngine = new MockEngine(_output), + ResourceFiles = new ITaskItem[] { new TaskItem(resxPath) }, + RootNamespace = "Test" + }; + + bool result = task.Execute(); + result.ShouldBeTrue(); + } + } +} diff --git a/src/Tasks/AddToWin32Manifest.cs b/src/Tasks/AddToWin32Manifest.cs index 968172b0d6f..53cc63a28f0 100644 --- a/src/Tasks/AddToWin32Manifest.cs +++ b/src/Tasks/AddToWin32Manifest.cs @@ -17,7 +17,8 @@ namespace Microsoft.Build.Tasks /// /// Generates an application manifest or adds an entry to the existing one when PreferNativeArm64 property is true. /// - public sealed class AddToWin32Manifest : TaskExtension + [MSBuildMultiThreadableTask] + public sealed class AddToWin32Manifest : TaskExtension, IMultiThreadableTask { private const string supportedArchitectures = "supportedArchitectures"; private const string windowsSettings = "windowsSettings"; @@ -86,43 +87,54 @@ public string ManifestPath private set => _generatedManifestFullPath = value; } - private string? GetManifestPath() + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + + private AbsolutePath? GetManifestPath() { if (ApplicationManifest != null) { - if (string.IsNullOrEmpty(ApplicationManifest.ItemSpec) || !FileSystems.Default.FileExists(ApplicationManifest?.ItemSpec)) + if (string.IsNullOrEmpty(ApplicationManifest.ItemSpec)) + { + Log.LogErrorWithCodeFromResources(null, ApplicationManifest.ItemSpec, 0, 0, 0, 0, "AddToWin32Manifest.SpecifiedApplicationManifestCanNotBeFound"); + return null; + } + + AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(ApplicationManifest.ItemSpec); + if (!FileSystems.Default.FileExists(absolutePath)) { - Log.LogErrorWithCodeFromResources(null, ApplicationManifest?.ItemSpec, 0, 0, 0, 0, "AddToWin32Manifest.SpecifiedApplicationManifestCanNotBeFound"); + Log.LogErrorWithCodeFromResources(null, ApplicationManifest.ItemSpec, 0, 0, 0, 0, "AddToWin32Manifest.SpecifiedApplicationManifestCanNotBeFound"); return null; } - return ApplicationManifest!.ItemSpec; + return absolutePath; } string? defaultManifestPath = ToolLocationHelper.GetPathToDotNetFrameworkFile(DefaultManifestName, TargetDotNetFrameworkVersion.Version46); - return defaultManifestPath; + return defaultManifestPath is null ? null : new AbsolutePath(defaultManifestPath); } - private Stream? GetManifestStream(string? path) + private Stream? GetManifestStream(AbsolutePath? path) { // The logic for getting default manifest is similar to the one from Roslyn: // If Roslyn logic returns null, we fall back to reading embedded manifest. return path is null ? typeof(AddToWin32Manifest).Assembly.GetManifestResourceStream($"Microsoft.Build.Tasks.Resources.{DefaultManifestName}") - : File.OpenRead(path); + : File.OpenRead(path.Value); } public override bool Execute() { - string? manifestPath = GetManifestPath(); + AbsolutePath? manifestPath = GetManifestPath(); + string? manifestDisplayPath = manifestPath?.OriginalValue; try { using Stream? stream = GetManifestStream(manifestPath); if (stream is null) { - Log.LogErrorWithCodeFromResources(null, manifestPath, 0, 0, 0, 0, "AddToWin32Manifest.ManifestCanNotBeOpened"); + Log.LogErrorWithCodeFromResources(null, manifestDisplayPath, 0, 0, 0, 0, "AddToWin32Manifest.ManifestCanNotBeOpened"); return !Log.HasLoggedErrors; } @@ -130,7 +142,7 @@ public override bool Execute() XmlDocument document = LoadManifest(stream); XmlNamespaceManager xmlNamespaceManager = XmlNamespaces.GetNamespaceManager(document.NameTable); - ManifestValidationResult validationResult = ValidateManifest(manifestPath, document, xmlNamespaceManager); + ManifestValidationResult validationResult = ValidateManifest(manifestDisplayPath, document, xmlNamespaceManager); switch (validationResult) { @@ -148,7 +160,7 @@ public override bool Execute() } catch (Exception ex) { - Log.LogErrorWithCodeFromResources(null, manifestPath, 0, 0, 0, 0, "AddToWin32Manifest.ManifestCanNotBeOpenedWithException", ex.Message); + Log.LogErrorWithCodeFromResources(null, manifestDisplayPath, 0, 0, 0, 0, "AddToWin32Manifest.ManifestCanNotBeOpenedWithException", ex.Message); return !Log.HasLoggedErrors; } @@ -168,8 +180,10 @@ private XmlDocument LoadManifest(Stream stream) private void SaveManifest(XmlDocument document, string manifestName) { - ManifestPath = Path.Combine(OutputDirectory, manifestName); - using (var xmlWriter = new XmlTextWriter(ManifestPath, Encoding.UTF8)) + string originalPath = Path.Combine(OutputDirectory, manifestName); + AbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath); + ManifestPath = originalPath; + using (var xmlWriter = new XmlTextWriter(outputPath, Encoding.UTF8)) { xmlWriter.Formatting = Formatting.Indented; xmlWriter.Indentation = 4; diff --git a/src/Tasks/CreateCSharpManifestResourceName.cs b/src/Tasks/CreateCSharpManifestResourceName.cs index c7f838b16ef..a674351db1c 100644 --- a/src/Tasks/CreateCSharpManifestResourceName.cs +++ b/src/Tasks/CreateCSharpManifestResourceName.cs @@ -16,6 +16,7 @@ namespace Microsoft.Build.Tasks /// Base class for task that determines the appropriate manifest resource name to /// assign to a given resx or other resource. /// + [MSBuildMultiThreadableTask] public class CreateCSharpManifestResourceName : CreateManifestResourceName { protected override string SourceFileExtension => ".cs"; diff --git a/src/Tasks/CreateManifestResourceName.cs b/src/Tasks/CreateManifestResourceName.cs index 99e71de2b07..5dca659f7c6 100644 --- a/src/Tasks/CreateManifestResourceName.cs +++ b/src/Tasks/CreateManifestResourceName.cs @@ -20,7 +20,7 @@ namespace Microsoft.Build.Tasks /// Base class for task that determines the appropriate manifest resource name to /// assign to a given resx or other resource. /// - public abstract class CreateManifestResourceName : TaskExtension + public abstract class CreateManifestResourceName : TaskExtension, IMultiThreadableTask { #region Properties internal const string resxFileExtension = ".resx"; @@ -86,6 +86,9 @@ public bool EnableCustomCulture [Output] public ITaskItem[] ResourceFilesWithManifestResourceNames { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + #endregion /// @@ -119,7 +122,7 @@ protected abstract string CreateManifestName( /// File mode /// Access type /// The FileStream - private static Stream CreateFileStreamOverNewFileStream(string path, FileMode mode, FileAccess access) + private static Stream CreateFileStreamOverNewFileStream(AbsolutePath path, FileMode mode, FileAccess access) { return new FileStream(path, mode, access); } @@ -190,7 +193,8 @@ internal bool Execute( } } - if (FileSystems.Default.FileExists(Path.Combine(Path.GetDirectoryName(fileName), conventionDependentUpon))) + AbsolutePath dependentPath = TaskEnvironment.GetAbsolutePath(Path.Combine(Path.GetDirectoryName(fileName), conventionDependentUpon)); + if (FileSystems.Default.FileExists(dependentPath)) { dependentUpon = conventionDependentUpon; } @@ -214,7 +218,7 @@ internal bool Execute( if (isDependentOnSourceFile) { - string pathToDependent = Path.Combine(Path.GetDirectoryName(fileName), dependentUpon); + AbsolutePath pathToDependent = TaskEnvironment.GetAbsolutePath(Path.Combine(Path.GetDirectoryName(fileName), dependentUpon)); binaryStream = createFileStream(pathToDependent, FileMode.Open, FileAccess.Read); } diff --git a/src/Tasks/CreateVisualBasicManifestResourceName.cs b/src/Tasks/CreateVisualBasicManifestResourceName.cs index d2cf7f405ef..40c0224dc8c 100644 --- a/src/Tasks/CreateVisualBasicManifestResourceName.cs +++ b/src/Tasks/CreateVisualBasicManifestResourceName.cs @@ -15,6 +15,7 @@ namespace Microsoft.Build.Tasks /// Base class for task that determines the appropriate manifest resource name to /// assign to a given resx or other resource. /// + [MSBuildMultiThreadableTask] public class CreateVisualBasicManifestResourceName : CreateManifestResourceName { protected override string SourceFileExtension => ".vb"; diff --git a/src/Tasks/Delegate.cs b/src/Tasks/Delegate.cs index 48711172264..bd4ccb8ed8e 100644 --- a/src/Tasks/Delegate.cs +++ b/src/Tasks/Delegate.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Runtime.Versioning; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Tasks.AssemblyDependency; @@ -117,7 +118,7 @@ internal delegate void GetAssemblyMetadata( /// File mode /// Access type /// The Stream - internal delegate Stream CreateFileStream(string path, FileMode mode, FileAccess access); + internal delegate Stream CreateFileStream(AbsolutePath path, FileMode mode, FileAccess access); /// /// Delegate for System.IO.File.GetLastWriteTime diff --git a/src/Tasks/GenerateApplicationManifest.cs b/src/Tasks/GenerateApplicationManifest.cs index dae5d27d4cf..a1e1c7c39d3 100644 --- a/src/Tasks/GenerateApplicationManifest.cs +++ b/src/Tasks/GenerateApplicationManifest.cs @@ -19,6 +19,7 @@ namespace Microsoft.Build.Tasks /// Generates an application manifest for ClickOnce projects. /// [SupportedOSPlatform("windows")] + [MSBuildMultiThreadableTask] public sealed class GenerateApplicationManifest : GenerateManifestBase { private enum _ManifestType @@ -41,7 +42,7 @@ private enum _ManifestType public ITaskItem[] Dependencies { get => _dependencies; - set => _dependencies = Util.SortItems(value); + set => _dependencies = value; } public string ErrorReportUrl { get; set; } @@ -63,7 +64,7 @@ public ITaskItem[] FileAssociations public ITaskItem[] Files { get => _files; - set => _files = Util.SortItems(value); + set => _files = value; } public bool HostInBrowser { get; set; } @@ -73,7 +74,7 @@ public ITaskItem[] Files public ITaskItem[] IsolatedComReferences { get => _isolatedComReferences; - set => _isolatedComReferences = Util.SortItems(value); + set => _isolatedComReferences = value; } public string ManifestType { get; set; } @@ -142,6 +143,10 @@ protected override bool OnManifestResolved(Manifest manifest) private bool BuildApplicationManifest(ApplicationManifest manifest) { + _dependencies = Util.SortItems(_dependencies, TaskEnvironment); + _files = Util.SortItems(_files, TaskEnvironment); + _isolatedComReferences = Util.SortItems(_isolatedComReferences, TaskEnvironment); + if (Dependencies != null) { foreach (ITaskItem item in Dependencies) @@ -234,7 +239,7 @@ private bool AddIsolatedComReferences(ApplicationManifest manifest) name = Path.GetFileName(item.ItemSpec); } FileReference file = AddFileFromItem(item); - if (!file.ImportComComponent(item.ItemSpec, manifest.OutputMessages, name)) + if (!file.ImportComComponent(TaskEnvironment.GetAbsolutePath(item.ItemSpec), manifest.OutputMessages, name)) { success = false; } @@ -281,8 +286,9 @@ private bool AddClickOnceFiles(ApplicationManifest manifest) if (!String.IsNullOrEmpty(TrustInfoFile?.ItemSpec)) { + AbsolutePath trustInfoPath = TaskEnvironment.GetAbsolutePath(TrustInfoFile.ItemSpec); manifest.TrustInfo = new TrustInfo(); - manifest.TrustInfo.Read(TrustInfoFile.ItemSpec); + manifest.TrustInfo.Read(trustInfoPath); } if (manifest.TrustInfo == null) @@ -449,7 +455,8 @@ private bool GetRequestedExecutionLevel(out string requestedExecutionLevel) try { - using (Stream s = File.Open(InputManifest.ItemSpec, FileMode.Open, FileAccess.Read, FileShare.Read)) + AbsolutePath inputManifestPath = TaskEnvironment.GetAbsolutePath(InputManifest.ItemSpec); + using (Stream s = File.Open(inputManifestPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { var document = new XmlDocument(); var xrSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }; diff --git a/src/Tasks/GenerateDeploymentManifest.cs b/src/Tasks/GenerateDeploymentManifest.cs index 897ba49d15f..782cb8003bc 100644 --- a/src/Tasks/GenerateDeploymentManifest.cs +++ b/src/Tasks/GenerateDeploymentManifest.cs @@ -5,7 +5,9 @@ using System.Diagnostics; using System.IO; using System.Runtime.Versioning; +using Microsoft.Build.Framework; using Microsoft.Build.Tasks.Deployment.ManifestUtilities; +using Constants = Microsoft.Build.Tasks.Deployment.ManifestUtilities.Constants; #nullable disable @@ -15,6 +17,7 @@ namespace Microsoft.Build.Tasks /// Generates a deploy manifest for ClickOnce projects. /// [SupportedOSPlatform("windows")] + [MSBuildMultiThreadableTask] public sealed class GenerateDeploymentManifest : GenerateManifestBase { private bool? _createDesktopShortcut; diff --git a/src/Tasks/GenerateManifestBase.cs b/src/Tasks/GenerateManifestBase.cs index 195e2593111..ac63db6cbef 100644 --- a/src/Tasks/GenerateManifestBase.cs +++ b/src/Tasks/GenerateManifestBase.cs @@ -16,7 +16,7 @@ namespace Microsoft.Build.Tasks /// /// Base class for all manifest generation tasks. /// - public abstract class GenerateManifestBase : Task + public abstract class GenerateManifestBase : Task, IMultiThreadableTask { private enum AssemblyType { @@ -81,6 +81,9 @@ public string TargetFrameworkVersion public string TargetFrameworkMoniker { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + protected internal AssemblyReference AddAssemblyNameFromItem(ITaskItem item, AssemblyReferenceType referenceType) { var assembly = new AssemblyReference @@ -452,7 +455,8 @@ private bool InitializeManifest(Type manifestType) { try { - _manifest = ManifestReader.ReadManifest(manifestType.Name, InputManifest.ItemSpec, true); + AbsolutePath inputManifestPath = TaskEnvironment.GetAbsolutePath(InputManifest.ItemSpec); + _manifest = ManifestReader.ReadManifest(manifestType.Name, inputManifestPath, true); } catch (Exception ex) { @@ -511,7 +515,7 @@ private bool ResolveFiles() { int t1 = Environment.TickCount; - string[] searchPaths = { Directory.GetCurrentDirectory() }; + string[] searchPaths = { TaskEnvironment.ProjectDirectory }; _manifest.ResolveFiles(searchPaths); _manifest.UpdateFileInfo(TargetFrameworkVersion); if (_manifest.OutputMessages.ErrorCount > 0) @@ -613,13 +617,14 @@ private bool WriteManifest() } int t1 = Environment.TickCount; + AbsolutePath outputManifestPath = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec); try { - ManifestWriter.WriteManifest(_manifest, OutputManifest.ItemSpec, TargetFrameworkVersion); + ManifestWriter.WriteManifest(_manifest, outputManifestPath, TargetFrameworkVersion); } catch (Exception ex) { - string lockedFileMessage = LockCheck.GetLockedFileMessage(OutputManifest.ItemSpec); + string lockedFileMessage = LockCheck.GetLockedFileMessage(outputManifestPath); Log.LogErrorWithCodeFromResources("GenerateManifest.WriteOutputManifestFailed", OutputManifest.ItemSpec, ex.Message, lockedFileMessage); return false; diff --git a/src/Tasks/ManifestUtil/Manifest.cs b/src/Tasks/ManifestUtil/Manifest.cs index 60b55b7cb1e..deea2b0b14c 100644 --- a/src/Tasks/ManifestUtil/Manifest.cs +++ b/src/Tasks/ManifestUtil/Manifest.cs @@ -207,6 +207,11 @@ private static bool ResolveFile(BaseReference f, string[] searchPaths) /// The location of each referenced assembly and file is required for hash computation and assembly identity resolution. /// Any resulting errors or warnings are reported in the OutputMessages collection. /// + /// + /// WARNING: This overload uses to resolve relative paths, + /// which is not safe in multithreaded (-mt) MSBuild mode. Use the overload + /// with explicit search paths (e.g., from TaskEnvironment.ProjectDirectory) instead. + /// public void ResolveFiles() { string defaultDir = String.Empty; diff --git a/src/Tasks/ManifestUtil/Util.cs b/src/Tasks/ManifestUtil/Util.cs index 22d48fab01c..cc19e892305 100644 --- a/src/Tasks/ManifestUtil/Util.cs +++ b/src/Tasks/ManifestUtil/Util.cs @@ -397,7 +397,7 @@ public static string PlatformToProcessorArchitecture(string platform) return null; } - private static ITaskItem[] RemoveDuplicateItems(ITaskItem[] items) + private static ITaskItem[] RemoveDuplicateItems(ITaskItem[] items, TaskEnvironment taskEnvironment) { if (items == null) { @@ -425,7 +425,7 @@ private static ITaskItem[] RemoveDuplicateItems(ITaskItem[] items) } else { - key = Path.GetFullPath(item.ItemSpec).ToUpperInvariant(); + key = ((string)taskEnvironment.GetAbsolutePath(item.ItemSpec).GetCanonicalForm()).ToUpperInvariant(); } if (!list.ContainsKey(key)) @@ -437,9 +437,9 @@ private static ITaskItem[] RemoveDuplicateItems(ITaskItem[] items) return list.Values.ToArray(); } - public static ITaskItem[] SortItems(ITaskItem[] items) + public static ITaskItem[] SortItems(ITaskItem[] items, TaskEnvironment taskEnvironment) { - ITaskItem[] outputItems = RemoveDuplicateItems(items); + ITaskItem[] outputItems = RemoveDuplicateItems(items, taskEnvironment); if (outputItems != null) { Array.Sort(outputItems, s_itemComparer); diff --git a/src/Tasks/ResolveManifestFiles.cs b/src/Tasks/ResolveManifestFiles.cs index 16254ed9cfd..f664357e63b 100644 --- a/src/Tasks/ResolveManifestFiles.cs +++ b/src/Tasks/ResolveManifestFiles.cs @@ -37,7 +37,8 @@ namespace Microsoft.Build.Tasks /// Apply Group and Optional attributes from PublishFile items to built items /// (8) Insure all output items have a TargetPath, and if in a Group that IsOptional is set /// - public sealed class ResolveManifestFiles : TaskExtension + [MSBuildMultiThreadableTask] + public sealed class ResolveManifestFiles : TaskExtension, IMultiThreadableTask { #region Fields @@ -56,7 +57,7 @@ public sealed class ResolveManifestFiles : TaskExtension private bool _canPublish; private Dictionary _runtimePackAssets; // map of satellite assemblies that are included in References - private SatelliteRefAssemblyMap _satelliteAssembliesPassedAsReferences = new SatelliteRefAssemblyMap(); + private SatelliteRefAssemblyMap _satelliteAssembliesPassedAsReferences; #endregion #region Properties @@ -68,25 +69,25 @@ public sealed class ResolveManifestFiles : TaskExtension public ITaskItem[] ExtraFiles { get => _extraFiles; - set => _extraFiles = Util.SortItems(value); + set => _extraFiles = value; } public ITaskItem[] Files { get => _files; - set => _files = Util.SortItems(value); + set => _files = value; } public ITaskItem[] ManagedAssemblies { get => _managedAssemblies; - set => _managedAssemblies = Util.SortItems(value); + set => _managedAssemblies = value; } public ITaskItem[] NativeAssemblies { get => _nativeAssemblies; - set => _nativeAssemblies = Util.SortItems(value); + set => _nativeAssemblies = value; } // Runtime assets for self-contained deployment from .NETCore runtime pack @@ -113,13 +114,13 @@ public ITaskItem[] NativeAssemblies public ITaskItem[] PublishFiles { get => _publishFiles; - set => _publishFiles = Util.SortItems(value); + set => _publishFiles = value; } public ITaskItem[] SatelliteAssemblies { get => _satelliteAssemblies; - set => _satelliteAssemblies = Util.SortItems(value); + set => _satelliteAssemblies = value; } public string TargetCulture { get; set; } @@ -156,10 +157,21 @@ public string TargetFrameworkIdentifier set => _targetFrameworkIdentifier = value; } + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + #endregion public override bool Execute() { + _satelliteAssembliesPassedAsReferences = new SatelliteRefAssemblyMap(TaskEnvironment); + _extraFiles = Util.SortItems(_extraFiles, TaskEnvironment); + _files = Util.SortItems(_files, TaskEnvironment); + _managedAssemblies = Util.SortItems(_managedAssemblies, TaskEnvironment); + _nativeAssemblies = Util.SortItems(_nativeAssemblies, TaskEnvironment); + _publishFiles = Util.SortItems(_publishFiles, TaskEnvironment); + _satelliteAssemblies = Util.SortItems(_satelliteAssemblies, TaskEnvironment); + if (!ValidateInputs()) { return false; @@ -402,7 +414,9 @@ private void GetOutputAssemblies(List publishInfos, List } // Apply the culture publishing rules to include or exclude satellite assemblies - AssemblyIdentity identity = AssemblyIdentity.FromManagedAssembly(item.ItemSpec); + AssemblyIdentity identity = String.IsNullOrEmpty(item.ItemSpec) + ? null + : AssemblyIdentity.FromManagedAssembly(TaskEnvironment.GetAbsolutePath(item.ItemSpec)); if (identity != null && !String.Equals(identity.Culture, "neutral", StringComparison.Ordinal)) { CultureInfo satelliteCulture = new CultureInfo(identity.Culture); @@ -508,7 +522,8 @@ private ITaskItem[] GetOutputFiles(List publishInfos, IEnumerable Path.GetFullPath(p.ItemSpec), StringComparer.OrdinalIgnoreCase); + // Use GetCanonicalForm to canonicalize paths (resolve .., normalize slashes) for reliable matching + var outputAssembliesMap = outputAssemblies.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(), StringComparer.OrdinalIgnoreCase); // Add all input Files to the FileMap, flagging them to be published by default... if (Files != null) @@ -520,7 +535,7 @@ private ITaskItem[] GetOutputFiles(List publishInfos, IEnumerable _dictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly TaskEnvironment _taskEnvironment; + + public SatelliteRefAssemblyMap(TaskEnvironment taskEnvironment) + { + _taskEnvironment = taskEnvironment; + } public MapEntry this[string fusionName] { @@ -911,7 +934,9 @@ public MapEntry this[string fusionName] public bool ContainsItem(ITaskItem item) { - AssemblyIdentity identity = AssemblyIdentity.FromManagedAssembly(item.ItemSpec); + AssemblyIdentity identity = string.IsNullOrEmpty(item.ItemSpec) + ? null + : AssemblyIdentity.FromManagedAssembly(_taskEnvironment.GetAbsolutePath(item.ItemSpec)); if (identity != null) { return _dictionary.ContainsKey(identity.ToString()); @@ -922,8 +947,10 @@ public bool ContainsItem(ITaskItem item) public void Add(ITaskItem item) { var entry = new MapEntry(item, true); - AssemblyIdentity identity = AssemblyIdentity.FromManagedAssembly(item.ItemSpec); - if (identity != null && !String.Equals(identity.Culture, "neutral", StringComparison.Ordinal)) + AssemblyIdentity identity = string.IsNullOrEmpty(item.ItemSpec) + ? null + : AssemblyIdentity.FromManagedAssembly(_taskEnvironment.GetAbsolutePath(item.ItemSpec)); + if (identity != null && !string.Equals(identity.Culture, "neutral", StringComparison.Ordinal)) { // Use satellite assembly strong name signature as key string key = identity.ToString(); diff --git a/src/Tasks/UpdateManifest.cs b/src/Tasks/UpdateManifest.cs index 109f7ee732c..79c23caa2f0 100644 --- a/src/Tasks/UpdateManifest.cs +++ b/src/Tasks/UpdateManifest.cs @@ -17,7 +17,8 @@ namespace Microsoft.Build.Tasks /// /// Updates selected properties in a manifest and resigns. /// - public class UpdateManifest : Task, IUpdateManifestTaskContract + [MSBuildMultiThreadableTask] + public class UpdateManifest : Task, IUpdateManifestTaskContract, IMultiThreadableTask { [Required] public string ApplicationPath { get; set; } @@ -33,9 +34,16 @@ public class UpdateManifest : Task, IUpdateManifestTaskContract [Output] public ITaskItem OutputManifest { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + public override bool Execute() { - Manifest.UpdateEntryPoint(InputManifest.ItemSpec, OutputManifest.ItemSpec, ApplicationPath, ApplicationManifest.ItemSpec, TargetFrameworkVersion); + AbsolutePath inputManifestPath = TaskEnvironment.GetAbsolutePath(InputManifest.ItemSpec); + AbsolutePath outputManifestPath = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec); + AbsolutePath applicationManifestPath = TaskEnvironment.GetAbsolutePath(ApplicationManifest.ItemSpec); + + Manifest.UpdateEntryPoint(inputManifestPath, outputManifestPath, ApplicationPath, applicationManifestPath, TargetFrameworkVersion); return true; } @@ -43,6 +51,7 @@ public override bool Execute() #else + [MSBuildMultiThreadableTask] public sealed class UpdateManifest : TaskRequiresFramework, IUpdateManifestTaskContract { public UpdateManifest()