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()