diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9b0ce007bc5..3532955597e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -39,6 +39,7 @@
+
diff --git a/eng/BuildTask.targets b/eng/BuildTask.targets
index 49ac256b029..09019d1a110 100644
--- a/eng/BuildTask.targets
+++ b/eng/BuildTask.targets
@@ -38,6 +38,19 @@
+
+
+
+
+
+
+
diff --git a/eng/MultiThreadableTaskAnalyzer.globalconfig b/eng/MultiThreadableTaskAnalyzer.globalconfig
new file mode 100644
index 00000000000..7a66c6d58fa
--- /dev/null
+++ b/eng/MultiThreadableTaskAnalyzer.globalconfig
@@ -0,0 +1,17 @@
+# Configuration for Microsoft.Build.TaskAuthoring.Analyzer (referenced from eng/BuildTask.targets).
+#
+# Arcade's tasks are being migrated to MSBuild's multithreaded execution model incrementally
+# (see https://github.com/dotnet/arcade/issues/17378). Scoping the analyzer to tasks that have
+# already opted in via [MSBuildMultiThreadableTask] keeps it as a regression guard for migrated
+# tasks without drowning the build in diagnostics for the ones still to be migrated.
+is_global = true
+
+msbuild_task_analyzer.scope = multithreadable_only
+
+# API-shape suggestions (typed path parameters, ITaskItem, constructor injection). Useful when
+# authoring a new task, but not worth churning the existing task surface over, and several of them
+# would be binary-breaking for tasks whose parameters are set from targets across the ecosystem.
+dotnet_diagnostic.MSBuildTask0006.severity = none
+dotnet_diagnostic.MSBuildTask0007.severity = none
+dotnet_diagnostic.MSBuildTask0008.severity = none
+dotnet_diagnostic.MSBuildTask0011.severity = none
diff --git a/eng/Version.Details.props b/eng/Version.Details.props
index c04c0dd321a..c607cfbbbca 100644
--- a/eng/Version.Details.props
+++ b/eng/Version.Details.props
@@ -20,10 +20,11 @@ This file should be imported by eng/Versions.props
1.1.0-beta.26407.1
- 17.12.50
- 17.12.50
- 17.12.50
- 17.12.50
+ 18.8.2
+ 18.8.2
+ 18.8.2
+ 18.8.2
+ 18.11.0-1.26420.118
4.8.0
4.8.0
@@ -81,6 +82,7 @@ This file should be imported by eng/Versions.props
$(MicrosoftBuildFrameworkPackageVersion)
$(MicrosoftBuildTasksCorePackageVersion)
$(MicrosoftBuildUtilitiesCorePackageVersion)
+ $(MicrosoftBuildTaskAuthoringAnalyzerPackageVersion)
$(MicrosoftCodeAnalysisCSharpPackageVersion)
$(MicrosoftNetCompilersToolsetPackageVersion)
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index e5f772e4356..edb63e62635 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -129,21 +129,25 @@
eb583d0664328fb0d370586c4d4e974212687cb0
-
+
https://github.com/dotnet/msbuild
- d1cce8d7cc03c23a4f1bad8e9240714fd9d199a3
+ ce25c01082c9c46cd02ad1ff3ff8f16fe5cc2f44
-
+
https://github.com/dotnet/msbuild
- d1cce8d7cc03c23a4f1bad8e9240714fd9d199a3
+ ce25c01082c9c46cd02ad1ff3ff8f16fe5cc2f44
-
+
https://github.com/dotnet/msbuild
- d1cce8d7cc03c23a4f1bad8e9240714fd9d199a3
+ ce25c01082c9c46cd02ad1ff3ff8f16fe5cc2f44
-
+
https://github.com/dotnet/msbuild
- d1cce8d7cc03c23a4f1bad8e9240714fd9d199a3
+ ce25c01082c9c46cd02ad1ff3ff8f16fe5cc2f44
+
+
+ https://github.com/dotnet/msbuild
+ ce25c01082c9c46cd02ad1ff3ff8f16fe5cc2f44
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs
index c323d13fe34..1de9084549e 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/HarvesterToolTask.cs
@@ -10,6 +10,12 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix
///
/// A tool task to invoke the WiX harvesting tool (heat.exe).
///
+ // Deliberately not annotated with [MSBuildMultiThreadableTask]. This type is never registered via
+ // UsingTask; it is constructed directly by MsiBase, so MSBuild neither routes it nor injects a
+ // TaskEnvironment, leaving WixToolTaskBase resolving against TaskEnvironment.Fallback (the process
+ // current directory). The attribute would therefore be inert while falsely marking part of the
+ // CreateVisualStudioWorkload helper chain as migrated. See dotnet/arcade#17378: the fix is to flow
+ // the owning task's TaskEnvironment into these instances once that chain takes AbsolutePath.
public class HarvesterToolTask : WixToolTaskBase
{
private static readonly Dictionary s_SuppressionArguments = new()
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTask.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTask.cs
index dbcb3bca411..f199c5b0b2d 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTask.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTask.cs
@@ -13,6 +13,12 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix
///
/// Tool task for invoking the WiX CLI (version 5 and above). This commands is responsible for compiling and linking.
///
+ // Deliberately not annotated with [MSBuildMultiThreadableTask]. This type is never registered via
+ // UsingTask; it is constructed directly by MsiBase, so MSBuild neither routes it nor injects a
+ // TaskEnvironment, leaving WixToolTaskBase resolving against TaskEnvironment.Fallback (the process
+ // current directory). The attribute would therefore be inert while falsely marking part of the
+ // CreateVisualStudioWorkload helper chain as migrated. See dotnet/arcade#17378: the fix is to flow
+ // the owning task's TaskEnvironment into these instances once that chain takes AbsolutePath.
public class WixToolTask : WixToolTaskBase
{
private List _sourceFiles = new();
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs
index a6219925652..ec10992c4ba 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/Wix/WixToolTaskBase.cs
@@ -39,7 +39,7 @@ protected WixToolTaskBase(IBuildEngine engine, string toolPath)
{
BuildEngine = engine ?? throw new ArgumentNullException(nameof(engine));
- if (!File.Exists(toolPath))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(toolPath)))
{
throw new FileNotFoundException("The specified tool executable was not found.", toolPath);
}
diff --git a/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj b/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj
index add99f50b33..de7f997fcd7 100644
--- a/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj
+++ b/src/Microsoft.DotNet.CMake.Sdk/Microsoft.DotNet.CMake.Sdk.csproj
@@ -14,8 +14,4 @@
-
-
-
-
diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs b/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs
index 56e6fe8abbc..d97465e849b 100644
--- a/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs
+++ b/src/Microsoft.DotNet.CMake.Sdk/src/CreateCMakeFileApiQuery.cs
@@ -3,7 +3,6 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
-using Microsoft.DotNet.Build.Tasks;
using System;
using System.IO;
@@ -12,8 +11,12 @@ namespace Microsoft.DotNet.CMake.Sdk
///
/// Creates a CMake File API query file to request codemodel information.
///
- public class CreateCMakeFileApiQuery : BuildTask
+ [MSBuildMultiThreadableTask]
+ public class CreateCMakeFileApiQuery : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// The CMake build output directory where the query should be created.
///
@@ -26,14 +29,14 @@ public override bool Execute()
{
// Create a client stateless query file with client name "Microsoft.DotNet.CMake.Sdk"
string queryDir = Path.Combine(CMakeOutputDir, ".cmake", "api", "v1", "query", "client-Microsoft.DotNet.CMake.Sdk");
- Directory.CreateDirectory(queryDir);
+ Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(queryDir));
string queryFile = Path.Combine(queryDir, "codemodel-v2");
// Create an empty file to request codemodel-v2 information
- File.WriteAllText(queryFile, string.Empty);
+ File.WriteAllText(TaskEnvironment.GetAbsolutePath(queryFile), string.Empty);
- Log.LogMessage(LogImportance.Low, "Created CMake File API query at: {0}", queryFile);
+ Log.LogMessage(MessageImportance.Low, "Created CMake File API query at: {0}", queryFile);
return true;
}
diff --git a/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs b/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs
index 8baeea06470..0442607b20a 100644
--- a/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs
+++ b/src/Microsoft.DotNet.CMake.Sdk/src/GetCMakeArtifactsFromFileApi.cs
@@ -3,7 +3,6 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
-using Microsoft.DotNet.Build.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
@@ -15,8 +14,12 @@ namespace Microsoft.DotNet.CMake.Sdk
///
/// Reads CMake File API response to find artifacts for a specific source directory.
///
- public class GetCMakeArtifactsFromFileApi : BuildTask
+ [MSBuildMultiThreadableTask]
+ public class GetCMakeArtifactsFromFileApi : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// The CMake build output directory containing the File API response.
///
@@ -47,14 +50,14 @@ public override bool Execute()
{
string replyDir = Path.Combine(CMakeOutputDir, ".cmake", "api", "v1", "reply");
- if (!Directory.Exists(replyDir))
+ if (!Directory.Exists(TaskEnvironment.GetAbsolutePath(replyDir)))
{
Log.LogError("CMake File API reply directory does not exist: {0}", replyDir);
return false;
}
// Find the latest index file
- var indexFiles = Directory.GetFiles(replyDir, "index-*.json");
+ var indexFiles = Directory.GetFiles(TaskEnvironment.GetAbsolutePath(replyDir), "index-*.json");
if (indexFiles.Length == 0)
{
Log.LogError("No CMake File API index files found.");
@@ -62,9 +65,9 @@ public override bool Execute()
}
string indexFile = indexFiles.OrderByDescending(f => f).First();
- Log.LogMessage(LogImportance.Low, "Reading CMake File API index: {0}", indexFile);
+ Log.LogMessage(MessageImportance.Low, "Reading CMake File API index: {0}", indexFile);
- string indexJson = File.ReadAllText(indexFile);
+ string indexJson = File.ReadAllText(TaskEnvironment.GetAbsolutePath(indexFile));
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
@@ -80,15 +83,15 @@ public override bool Execute()
}
string codeModelFile = Path.Combine(replyDir, index.Reply.ClientReply.CodemodelV2.JsonFile);
- if (!File.Exists(codeModelFile))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(codeModelFile)))
{
Log.LogError("Codemodel file not found: {0}", codeModelFile);
return false;
}
- Log.LogMessage(LogImportance.Low, "Reading codemodel: {0}", codeModelFile);
+ Log.LogMessage(MessageImportance.Low, "Reading codemodel: {0}", codeModelFile);
- string codeModelJson = File.ReadAllText(codeModelFile);
+ string codeModelJson = File.ReadAllText(TaskEnvironment.GetAbsolutePath(codeModelFile));
var codeModel = JsonSerializer.Deserialize(codeModelJson, options);
if (codeModel == null)
@@ -101,7 +104,7 @@ public override bool Execute()
string sourceRoot = codeModel.Paths?.Source?.Replace('\\', '/').TrimEnd('/') ?? "";
// Normalize source directory for comparison
- string normalizedSourceDir = Path.GetFullPath(SourceDirectory).Replace('\\', '/').TrimEnd('/');
+ string normalizedSourceDir = TaskEnvironment.GetAbsolutePath(SourceDirectory).Value.Replace('\\', '/').TrimEnd('/');
// Find the configuration using LINQ
var config = codeModel.Configurations?.FirstOrDefault(c =>
@@ -113,7 +116,7 @@ public override bool Execute()
return false;
}
- Log.LogMessage(LogImportance.Low, "Found configuration: {0}", Configuration);
+ Log.LogMessage(MessageImportance.Low, "Found configuration: {0}", Configuration);
if (config.Directories == null || config.Targets == null)
{
@@ -130,7 +133,7 @@ public override bool Execute()
if (!Path.IsPathRooted(dirSource))
{
dirSource = Path.Combine(sourceRoot, dirSource);
- dirSource = Path.GetFullPath(dirSource).Replace('\\', '/').TrimEnd('/');
+ dirSource = TaskEnvironment.GetAbsolutePath(dirSource).Value.Replace('\\', '/').TrimEnd('/');
}
return string.Equals(dirSource, normalizedSourceDir, StringComparison.OrdinalIgnoreCase);
@@ -142,7 +145,7 @@ public override bool Execute()
return false;
}
- Log.LogMessage(LogImportance.Low, "Found matching directory: {0}", SourceDirectory);
+ Log.LogMessage(MessageImportance.Low, "Found matching directory: {0}", SourceDirectory);
// Get artifacts
var artifacts = new List();
@@ -163,15 +166,15 @@ public override bool Execute()
}
string targetFile = Path.Combine(replyDir, target.JsonFile);
- if (!File.Exists(targetFile))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(targetFile)))
{
continue;
}
- Log.LogMessage(LogImportance.Low, "Reading target file: {0}", targetFile);
+ Log.LogMessage(MessageImportance.Low, "Reading target file: {0}", targetFile);
// Read target details
- string targetJson = File.ReadAllText(targetFile);
+ string targetJson = File.ReadAllText(TaskEnvironment.GetAbsolutePath(targetFile));
var targetDetails = JsonSerializer.Deserialize(targetJson, options);
// Get artifacts
@@ -182,12 +185,12 @@ public override bool Execute()
if (!string.IsNullOrEmpty(artifact.Path))
{
string fullPath = Path.Combine(CMakeOutputDir, artifact.Path);
- fullPath = Path.GetFullPath(fullPath);
+ fullPath = TaskEnvironment.GetAbsolutePath(fullPath);
var item = new TaskItem(fullPath);
artifacts.Add(item);
- Log.LogMessage(LogImportance.Low, "Found artifact: {0}", fullPath);
+ Log.LogMessage(MessageImportance.Low, "Found artifact: {0}", fullPath);
}
}
}
@@ -200,7 +203,7 @@ public override bool Execute()
}
Artifacts = artifacts.ToArray();
- Log.LogMessage(LogImportance.Normal, "Found {0} artifact(s) for source directory '{1}' in configuration '{2}'", Artifacts.Length, SourceDirectory, Configuration);
+ Log.LogMessage(MessageImportance.Normal, "Found {0} artifact(s) for source directory '{1}' in configuration '{2}'", Artifacts.Length, SourceDirectory, Configuration);
return true;
}
diff --git a/src/Microsoft.DotNet.Deployment.Tasks.Links/src/AkaMSLinksBase.cs b/src/Microsoft.DotNet.Deployment.Tasks.Links/src/AkaMSLinksBase.cs
index 704a968428d..7f2061faa4f 100644
--- a/src/Microsoft.DotNet.Deployment.Tasks.Links/src/AkaMSLinksBase.cs
+++ b/src/Microsoft.DotNet.Deployment.Tasks.Links/src/AkaMSLinksBase.cs
@@ -8,8 +8,11 @@
namespace Microsoft.DotNet.Deployment.Tasks.Links
{
- public abstract class AkaMSLinksBase : Microsoft.Build.Utilities.Task
+ public abstract class AkaMSLinksBase : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
// Authentication data
public string ClientId { get; set; }
@@ -24,7 +27,8 @@ protected AkaMSLinkManager CreateAkaMSLinksManager()
AkaMSLinkManager manager;
if (!string.IsNullOrEmpty(ClientCertificate))
{
- manager = new AkaMSLinkManager(ClientId, X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(File.ReadAllText(ClientCertificate)), password: null), Tenant, Log);
+ string certificatePath = TaskEnvironment.GetAbsolutePath(ClientCertificate);
+ manager = new AkaMSLinkManager(ClientId, X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(File.ReadAllText(certificatePath)), password: null), Tenant, Log);
}
else if (!string.IsNullOrEmpty(ClientSecret))
{
diff --git a/src/Microsoft.DotNet.Deployment.Tasks.Links/src/CreateAkaMSLinks.cs b/src/Microsoft.DotNet.Deployment.Tasks.Links/src/CreateAkaMSLinks.cs
index ebae7fc0958..ae5a7c68d24 100644
--- a/src/Microsoft.DotNet.Deployment.Tasks.Links/src/CreateAkaMSLinks.cs
+++ b/src/Microsoft.DotNet.Deployment.Tasks.Links/src/CreateAkaMSLinks.cs
@@ -12,6 +12,7 @@ namespace Microsoft.DotNet.Deployment.Tasks.Links
///
/// Creates or updates, in bulk, a set of aka.ms (redirection) links
///
+ [MSBuildMultiThreadableTask]
public class CreateAkaMSLinks : AkaMSLinksBase
{
///
diff --git a/src/Microsoft.DotNet.Deployment.Tasks.Links/src/DeleteAkaMSLinks.cs b/src/Microsoft.DotNet.Deployment.Tasks.Links/src/DeleteAkaMSLinks.cs
index b123f3326ab..67c0d95b032 100644
--- a/src/Microsoft.DotNet.Deployment.Tasks.Links/src/DeleteAkaMSLinks.cs
+++ b/src/Microsoft.DotNet.Deployment.Tasks.Links/src/DeleteAkaMSLinks.cs
@@ -8,6 +8,7 @@
namespace Microsoft.DotNet.Deployment.Tasks.Links
{
+ [MSBuildMultiThreadableTask]
public class DeleteAkaMSLinks : AkaMSLinksBase
{
///
diff --git a/src/Microsoft.DotNet.GenAPI/GenAPITask.cs b/src/Microsoft.DotNet.GenAPI/GenAPITask.cs
index 8df763b2cf6..d5278ff4b1f 100644
--- a/src/Microsoft.DotNet.GenAPI/GenAPITask.cs
+++ b/src/Microsoft.DotNet.GenAPI/GenAPITask.cs
@@ -7,7 +7,6 @@
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
-using Microsoft.DotNet.Build.Tasks;
using Microsoft.Cci;
using Microsoft.Cci.Extensions;
using Microsoft.Cci.Extensions.CSharp;
@@ -15,10 +14,17 @@
using Microsoft.Cci.Writers;
using Microsoft.Cci.Writers.CSharp;
using Microsoft.Cci.Writers.Syntax;
+using System.Text;
+using Microsoft.Build.Utilities;
namespace Microsoft.DotNet.GenAPI
{
- public class GenAPITask : BuildTask
+ // Deliberately not marked multithreadable: HostEnvironment resolves the raw LibPath and
+ // Assembly values below through Environment.ExpandEnvironmentVariables plus Directory.Exists/
+ // File.Exists (Microsoft.Cci.Extensions/HostEnvironment.cs:719-740), so relative inputs and
+ // per-project variables would bind to process-wide state in a shared node. Migrating requires
+ // expanding and resolving those paths through TaskEnvironment before they enter HostEnvironment.
+ public class GenAPITask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
private const string InternalsVisibleTypeName = "System.Runtime.CompilerServices.InternalsVisibleToAttribute";
private const string DefaultFileHeader =
@@ -36,6 +42,9 @@ public class GenAPITask : BuildTask
private SyntaxWriterType _syntaxWriterType;
private DocIdKinds _docIdKinds = Cci.Writers.DocIdKinds.All;
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// Path for an specific assembly or a directory to get all assemblies.
///
@@ -196,7 +205,7 @@ public override bool Execute()
}
string headerText = GetHeaderText(HeaderFile, _writerType, _syntaxWriterType);
- bool loopPerAssembly = Directory.Exists(OutputPath);
+ bool loopPerAssembly = !string.IsNullOrEmpty(OutputPath) && Directory.Exists(TaskEnvironment.GetAbsolutePath(OutputPath));
if (loopPerAssembly)
{
@@ -259,11 +268,11 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}
- private static string GetHeaderText(string headerFile, WriterType writerType, SyntaxWriterType syntaxWriterType)
+ private string GetHeaderText(string headerFile, WriterType writerType, SyntaxWriterType syntaxWriterType)
{
if (!string.IsNullOrEmpty(headerFile))
{
- return File.ReadAllText(headerFile);
+ return File.ReadAllText(TaskEnvironment.GetAbsolutePath(headerFile));
}
string defaultHeader = string.Empty;
@@ -279,18 +288,60 @@ private static string GetHeaderText(string headerFile, WriterType writerType, Sy
return defaultHeader;
}
- private static TextWriter GetOutput(string outFilePath, string filename = "")
+ private TextWriter GetOutput(string outFilePath, string filename = "")
{
- // If this is a null, empty, whitespace, or a directory use console
+ // If this is a null, empty, whitespace, or a directory write to the build log
if (string.IsNullOrWhiteSpace(outFilePath))
- return Console.Out;
+ return new LogTextWriter(Log);
+
+ if (Directory.Exists(TaskEnvironment.GetAbsolutePath(outFilePath)) && !string.IsNullOrEmpty(filename))
+ {
+ return File.CreateText(TaskEnvironment.GetAbsolutePath(Path.Combine(outFilePath, filename)));
+ }
+
+ return File.CreateText(TaskEnvironment.GetAbsolutePath(outFilePath));
+ }
+
+ ///
+ /// Forwards writes to the build log so that generated output is captured by MSBuild's
+ /// loggers rather than written to the process-wide console.
+ ///
+ private sealed class LogTextWriter : TextWriter
+ {
+ private readonly TaskLoggingHelper _log;
+ private readonly StringBuilder _line = new StringBuilder();
+
+ public LogTextWriter(TaskLoggingHelper log) => _log = log;
+
+ public override Encoding Encoding => Encoding.UTF8;
- if (Directory.Exists(outFilePath) && !string.IsNullOrEmpty(filename))
+ public override void Write(char value)
{
- return File.CreateText(Path.Combine(outFilePath, filename));
+ if (value == '\n')
+ {
+ Flush();
+ }
+ else if (value != '\r')
+ {
+ _line.Append(value);
+ }
+ }
+
+ public override void Flush()
+ {
+ _log.LogMessage(MessageImportance.High, _line.ToString());
+ _line.Clear();
}
- return File.CreateText(outFilePath);
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && _line.Length > 0)
+ {
+ Flush();
+ }
+
+ base.Dispose(disposing);
+ }
}
private static string GetFilename(IAssembly assembly, WriterType writer, SyntaxWriterType syntax)
diff --git a/src/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj b/src/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj
index 4cef61a930c..0efb0ad179a 100644
--- a/src/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj
+++ b/src/Microsoft.DotNet.GenAPI/Microsoft.DotNet.GenAPI.csproj
@@ -10,10 +10,6 @@
$(NoWarn);0436
-
-
-
-
diff --git a/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs b/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs
index 73fe72c35c4..a883a2cfc4b 100644
--- a/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs
+++ b/src/Microsoft.DotNet.GenFacades/ClearAssemblyReferenceVersions.cs
@@ -15,8 +15,12 @@ namespace Microsoft.DotNet.GenFacades
///
/// Rewrites an Assembly's references to be version 0.0.0.0.
///
- public class ClearAssemblyReferenceVersions : BuildTask
+ [MSBuildMultiThreadableTask]
+ public class ClearAssemblyReferenceVersions : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// Assembly to rewrite.
///
@@ -27,7 +31,7 @@ public override bool Execute()
{
try
{
- using (FileStream stream = File.Open(Assembly, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
+ using (FileStream stream = File.Open(TaskEnvironment.GetAbsolutePath(Assembly), FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
using (PEReader peReader = new PEReader(stream))
{
using (BinaryWriter writer = new BinaryWriter(stream))
diff --git a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
index a4b613890bb..2cf34465eed 100644
--- a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
+++ b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
@@ -9,8 +9,22 @@
namespace Microsoft.DotNet.GenFacades
{
- public class GenPartialFacadeSource : RoslynBuildTask
+ // TODO: Not opted into multithreading. RoslynBuildTask.Execute subscribes every instance to the
+ // process-wide AssemblyLoadContext.Resolving event, so with differing RoslynAssembliesPath values
+ // one instance can satisfy another instance's resolution. The TaskEnvironment below is still used
+ // for path resolution. Tracked by https://github.com/dotnet/arcade/issues/17378.
+ //
+ // Implementing IMultiThreadableTask without the attribute is deliberate. Routing is decided by
+ // the attribute alone (TaskRouter.NeedsTaskHostInMultiThreadedMode); it cannot key off the
+ // interface, because ToolTask implements it and that would opt in every ToolTask-derived task in
+ // the ecosystem. The interface only causes TaskEnvironment to be injected. Do not remove it to
+ // "make this safe" - that would revert the path resolution below to the process current
+ // directory while leaving the task exactly as unsafe as it is now.
+ public class GenPartialFacadeSource : RoslynBuildTask, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
public ITaskItem[] ReferencePaths { get; set; }
@@ -40,12 +54,12 @@ public override bool ExecuteCore()
try
{
result = GenPartialFacadeSourceGenerator.Execute(
- ReferencePaths?.Select(item => item.ItemSpec).ToArray(),
- ReferenceAssembly,
- CompileFiles?.Select(item => item.ItemSpec).ToArray(),
+ ReferencePaths?.Select(item => TaskEnvironment.GetAbsolutePath(item.ItemSpec)).ToArray(),
+ TaskEnvironment.GetAbsolutePath(ReferenceAssembly),
+ CompileFiles?.Select(item => TaskEnvironment.GetAbsolutePath(item.ItemSpec)).ToArray(),
DefineConstants,
LangVersion,
- OutputSourcePath,
+ TaskEnvironment.GetAbsolutePath(OutputSourcePath),
Log,
IgnoreMissingTypes,
IgnoreMissingTypesList,
diff --git a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs
index 1ec90b35153..069567a41c5 100644
--- a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs
+++ b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSourceGenerator.cs
@@ -17,13 +17,13 @@ namespace Microsoft.DotNet.GenFacades
public class GenPartialFacadeSourceGenerator
{
public static bool Execute(
- string[] seeds,
- string contractAssembly,
- string[] compileFiles,
+ Microsoft.Build.Framework.AbsolutePath[] seeds,
+ Microsoft.Build.Framework.AbsolutePath contractAssembly,
+ Microsoft.Build.Framework.AbsolutePath[] compileFiles,
string defineConstants,
string langVersion,
- string outputSourcePath,
- ILog logger,
+ Microsoft.Build.Framework.AbsolutePath outputSourcePath,
+ TaskLoggingHelper logger,
bool ignoreMissingTypes = false,
string[] ignoreMissingTypesList = null,
string[] OmitTypes = null,
@@ -34,8 +34,8 @@ public static bool Execute(
IEnumerable referenceTypes = GetPublicVisibleTypes(contractAssembly, includeTypeForwards: true);
// Normalizing and Removing Relative Segments from the seed paths.
- string[] distinctSeeds = seeds.Select(seed => Path.GetFullPath(seed)).Distinct().ToArray();
- string[] seedNames = distinctSeeds.Select(seed => Path.GetFileName(seed)).ToArray();
+ Microsoft.Build.Framework.AbsolutePath[] distinctSeeds = seeds.Distinct().ToArray();
+ string[] seedNames = distinctSeeds.Select(seed => Path.GetFileName(seed.Value)).ToArray();
if (distinctSeeds.Count() != seedNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count())
{
@@ -61,7 +61,7 @@ private static IEnumerable ParseDefineConstants(string defineConstants)
return defineConstants?.Split(';', ',').Where(t => !string.IsNullOrEmpty(t)).ToArray();
}
- private static Dictionary ParseSeedTypePreferences(ITaskItem[] preferences, ILog logger)
+ private static Dictionary ParseSeedTypePreferences(ITaskItem[] preferences, TaskLoggingHelper logger)
{
var dictionary = new Dictionary(StringComparer.Ordinal);
@@ -93,7 +93,7 @@ private static Dictionary ParseSeedTypePreferences(ITaskItem[] p
return dictionary;
}
- private static IEnumerable GetPublicVisibleTypes(string assembly, bool includeTypeForwards = false)
+ private static IEnumerable GetPublicVisibleTypes(Microsoft.Build.Framework.AbsolutePath assembly, bool includeTypeForwards = false)
{
using (var peReader = new PEReader(new FileStream(assembly, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read)))
{
@@ -140,15 +140,15 @@ private static bool IsPublic(TypeDefinition typeDefination)
return (typeDefination.Attributes & TypeAttributes.Public) != 0;
}
- private static IReadOnlyDictionary> GenerateTypeTable(IEnumerable seedAssemblies)
+ private static IReadOnlyDictionary> GenerateTypeTable(IEnumerable seedAssemblies)
{
var typeTable = new Dictionary>();
- foreach(string assembly in seedAssemblies)
+ foreach(Microsoft.Build.Framework.AbsolutePath assembly in seedAssemblies)
{
IEnumerable types = GetPublicVisibleTypes(assembly);
foreach (string type in types)
{
- AddTypeToTable(typeTable, type, Path.GetFileName(assembly));
+ AddTypeToTable(typeTable, type, Path.GetFileName(assembly.Value));
}
}
return typeTable;
diff --git a/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj b/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj
index 447848b9d93..2b7d3c57495 100644
--- a/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj
+++ b/src/Microsoft.DotNet.GenFacades/Microsoft.DotNet.GenFacades.csproj
@@ -12,8 +12,4 @@
-
-
-
-
diff --git a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
index 467858088e8..556fdd86f1c 100644
--- a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
+++ b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
@@ -16,8 +16,24 @@ namespace Microsoft.DotNet.GenFacades
///
/// The class generates an NotSupportedAssembly from the reference sources.
///
- public class NotSupportedAssemblyGenerator : RoslynBuildTask
+ ///
+ /// TODO: Not opted into multithreading. RoslynBuildTask.Execute subscribes every instance to the
+ /// process-wide AssemblyLoadContext.Resolving event, so with differing RoslynAssembliesPath values
+ /// one instance can satisfy another instance's resolution. The TaskEnvironment below is still used
+ /// for path resolution. Tracked by https://github.com/dotnet/arcade/issues/17378.
+ ///
+ /// Implementing IMultiThreadableTask without the attribute is deliberate. Routing is decided by
+ /// the attribute alone (TaskRouter.NeedsTaskHostInMultiThreadedMode); it cannot key off the
+ /// interface, because ToolTask implements it and that would opt in every ToolTask-derived task in
+ /// the ecosystem. The interface only causes TaskEnvironment to be injected. Do not remove it to
+ /// "make this safe" - that would revert the path resolution below to the process current
+ /// directory while leaving the task exactly as unsafe as it is now.
+ ///
+ public class NotSupportedAssemblyGenerator : RoslynBuildTask, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
public ITaskItem[] SourceFiles { get; set; }
@@ -44,9 +60,9 @@ public override bool ExecuteCore()
private void GenerateNotSupportedAssemblyFiles(IEnumerable sourceFiles)
{
string[] apiExclusions = null;
- if (!string.IsNullOrEmpty(ApiExclusionListPath) && File.Exists(ApiExclusionListPath))
+ if (!string.IsNullOrEmpty(ApiExclusionListPath) && File.Exists(TaskEnvironment.GetAbsolutePath(ApiExclusionListPath)))
{
- apiExclusions = File.ReadAllLines(ApiExclusionListPath);
+ apiExclusions = File.ReadAllLines(TaskEnvironment.GetAbsolutePath(ApiExclusionListPath));
}
foreach (ITaskItem item in sourceFiles)
@@ -54,7 +70,7 @@ private void GenerateNotSupportedAssemblyFiles(IEnumerable sourceFile
string sourceFile = item.ItemSpec;
string outputPath = item.GetMetadata("OutputPath");
- if (!File.Exists(sourceFile))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(sourceFile)))
{
Log.LogError($"File {sourceFile} was not found.");
continue;
@@ -76,7 +92,7 @@ private void GenerateNotSupportedAssemblyForSourceFile(string sourceFile, string
Log.LogError($"Invalid LangVersion value '{LangVersion}'");
return;
}
- syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(sourceFile), new CSharpParseOptions(languageVersion));
+ syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(TaskEnvironment.GetAbsolutePath(sourceFile)), new CSharpParseOptions(languageVersion));
}
catch(Exception ex)
{
@@ -87,7 +103,7 @@ private void GenerateNotSupportedAssemblyForSourceFile(string sourceFile, string
var rewriter = new NotSupportedAssemblyRewriter(Message, apiExclusions);
SyntaxNode root = rewriter.Visit(syntaxTree.GetRoot());
string text = root.GetText().ToString();
- File.WriteAllText(outputPath, text);
+ File.WriteAllText(TaskEnvironment.GetAbsolutePath(outputPath), text);
}
}
diff --git a/src/Microsoft.DotNet.GenFacades/RoslynBuildTask.cs b/src/Microsoft.DotNet.GenFacades/RoslynBuildTask.cs
index c96e5c37b26..d0acb736f8c 100644
--- a/src/Microsoft.DotNet.GenFacades/RoslynBuildTask.cs
+++ b/src/Microsoft.DotNet.GenFacades/RoslynBuildTask.cs
@@ -10,7 +10,7 @@
namespace Microsoft.DotNet.Build.Tasks
{
- public abstract partial class RoslynBuildTask : BuildTask
+ public abstract partial class RoslynBuildTask : Microsoft.Build.Utilities.Task
{
[Required]
public string RoslynAssembliesPath { get; set; }
diff --git a/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs b/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs
index d42f64b96fc..1c4e5e6599b 100644
--- a/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs
+++ b/src/Microsoft.DotNet.GenFacades/SourceGenerator.cs
@@ -15,17 +15,17 @@ internal class SourceGenerator
private readonly IReadOnlyDictionary _seedTypePreferences;
private readonly IEnumerable _referenceTypes;
private readonly IReadOnlyDictionary> _seedTypes;
- private readonly string _outputSourcePath;
+ private readonly Microsoft.Build.Framework.AbsolutePath _outputSourcePath;
private readonly HashSet _ignoreMissingTypesList = new HashSet();
- private readonly ILog _logger;
+ private readonly TaskLoggingHelper _logger;
public SourceGenerator(
IEnumerable referenceTypes,
IReadOnlyDictionary> seedTypes,
IReadOnlyDictionary seedTypePreferences,
- string outputSourcePath,
+ Microsoft.Build.Framework.AbsolutePath outputSourcePath,
string[] ignoreMissingTypesList,
- ILog logger
+ TaskLoggingHelper logger
)
{
_referenceTypes = referenceTypes;
@@ -39,7 +39,7 @@ ILog logger
}
public bool GenerateSource(
- IEnumerable compileFiles,
+ IEnumerable compileFiles,
IEnumerable constants,
string langVersion,
bool ignoreMissingTypes)
diff --git a/src/Microsoft.DotNet.GenFacades/TypeParser.cs b/src/Microsoft.DotNet.GenFacades/TypeParser.cs
index c6bd6426f24..0e1c4c55875 100644
--- a/src/Microsoft.DotNet.GenFacades/TypeParser.cs
+++ b/src/Microsoft.DotNet.GenFacades/TypeParser.cs
@@ -13,7 +13,7 @@ namespace Microsoft.DotNet.GenFacades
{
internal class TypeParser
{
- public static HashSet GetAllPublicTypes(IEnumerable files, IEnumerable constants, string langVersion)
+ public static HashSet GetAllPublicTypes(IEnumerable files, IEnumerable constants, string langVersion)
{
HashSet types = new HashSet();
@@ -130,13 +130,13 @@ private static string GetNamespaceName(NamespaceDeclarationSyntax namespaceSynta
return namespaceSyntax.Name.ToFullString().Trim();
}
- private static IEnumerable GetSourceTrees(IEnumerable sourceFiles, IEnumerable constants, LanguageVersion languageVersion)
+ private static IEnumerable GetSourceTrees(IEnumerable sourceFiles, IEnumerable constants, LanguageVersion languageVersion)
{
CSharpParseOptions options = new CSharpParseOptions(languageVersion: languageVersion, preprocessorSymbols: constants);
List result = new List();
- foreach (string sourceFile in sourceFiles)
+ foreach (Microsoft.Build.Framework.AbsolutePath sourceFile in sourceFiles)
{
- if (string.IsNullOrEmpty(sourceFile))
+ if (string.IsNullOrEmpty(sourceFile.Value))
{
continue;
}
diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAndroidWorkItemsTests.cs b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAndroidWorkItemsTests.cs
index 6880c3ee589..c1df1cf4f32 100644
--- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAndroidWorkItemsTests.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAndroidWorkItemsTests.cs
@@ -104,7 +104,7 @@ public void ArchivePayloadIsOverwritten()
CreateApk("apks/System.Bar.apk", "System.Bar"),
};
- _fileSystem.Files.Add("apks/xharness-payload-system.foo.zip", "archive");
+ _fileSystem.Files.Add(_task.TaskEnvironment.GetAbsolutePath("apks/xharness-payload-system.foo.zip"), "archive");
// Act
using var provider = collection.BuildServiceProvider();
@@ -245,7 +245,7 @@ private ITaskItem CreateApk(
mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAndroidWorkItems.MetadataNames.ApkPath)).Returns(apkPath);
}
- _fileSystem.WriteToFile(apkPath ?? itemSpec, "apk");
+ _fileSystem.WriteToFile(_task.TaskEnvironment.GetAbsolutePath(apkPath ?? itemSpec), "apk");
return mockBundle.Object;
}
diff --git a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs
index 7a4df6ffb1a..83dbfa6f893 100644
--- a/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk.Tests/Microsoft.DotNet.Helix.Sdk.Tests/CreateXHarnessAppleWorkItemsTests.cs
@@ -114,7 +114,7 @@ public void ArchivePayloadIsOverwritten()
CreateAppBundle("apps/System.Bar.app", "ios-simulator-64_13.5"),
};
- _fileSystem.Files.Add("apps/xharness-payload-system.foo.zip", "archive");
+ _fileSystem.Files.Add(_task.TaskEnvironment.GetAbsolutePath("apps/xharness-payload-system.foo.zip"), "archive");
// Act
using var provider = collection.BuildServiceProvider();
@@ -318,7 +318,7 @@ private ITaskItem CreateAppBundle(
mockBundle.Setup(x => x.GetMetadata(CreateXHarnessAppleWorkItems.MetadataNames.AppBundlePath)).Returns(appBundlePath);
}
- _fileSystem.CreateDirectory(appBundlePath ?? itemSpec);
+ _fileSystem.CreateDirectory(_task.TaskEnvironment.GetAbsolutePath(appBundlePath ?? itemSpec));
return mockBundle.Object;
}
diff --git a/src/Microsoft.DotNet.Helix/Sdk/AzureDevOpsTask.cs b/src/Microsoft.DotNet.Helix/Sdk/AzureDevOpsTask.cs
index e6e2b759a9c..64cb88e6ba0 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/AzureDevOpsTask.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/AzureDevOpsTask.cs
@@ -19,13 +19,16 @@
namespace Microsoft.DotNet.Helix.AzureDevOps
{
- public abstract class AzureDevOpsTask : BaseTask
+ public abstract class AzureDevOpsTask : BaseTask, IMultiThreadableTask
{
- private bool InAzurePipeline => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("BUILD_BUILDNUMBER"));
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
+ private bool InAzurePipeline => !string.IsNullOrEmpty(TaskEnvironment.GetEnvironmentVariable("BUILD_BUILDNUMBER"));
protected string GetEnvironmentVariable(string name)
{
- var result = Environment.GetEnvironmentVariable(name);
+ var result = TaskEnvironment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(result))
{
throw new InvalidOperationException($"Required environment variable {name} not set.");
@@ -194,8 +197,6 @@ protected async Task ParseResponseAsync(HttpRequestMessage req, HttpRes
return null;
}
- private static readonly Random s_rand = new Random();
-
public int RetryCount { get; set; } = 15;
public double RetryBackOffFactor { get; set; } = 1.3;
@@ -205,7 +206,7 @@ protected virtual int GetRetryDelay(int attempt)
var factor = RetryBackOffFactor;
var min = (int)(Math.Pow(factor, attempt) * 1000);
var max = (int)(Math.Pow(factor, attempt + 1) * 1000);
- return s_rand.Next(min, max);
+ return Random.Shared.Next(min, max);
}
public static bool IsRetryableHttpException(Exception ex)
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CancelHelixJob.cs b/src/Microsoft.DotNet.Helix/Sdk/CancelHelixJob.cs
index 2feb8f39c3d..9d10be18397 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CancelHelixJob.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CancelHelixJob.cs
@@ -10,6 +10,7 @@
namespace Microsoft.DotNet.Helix.Sdk
{
+ [MSBuildMultiThreadableTask]
public class CancelHelixJobs : HelixTask
{
///
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CheckAzurePipelinesTestResults.cs b/src/Microsoft.DotNet.Helix/Sdk/CheckAzurePipelinesTestResults.cs
index 07c145507e7..7011c5b02d1 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CheckAzurePipelinesTestResults.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CheckAzurePipelinesTestResults.cs
@@ -13,6 +13,7 @@
namespace Microsoft.DotNet.Helix.AzureDevOps
{
+ [MSBuildMultiThreadableTask]
public class CheckAzurePipelinesTestResults : AzureDevOpsTask
{
public int[] TestRunIds { get; set; }
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CheckHelixJobStatus.cs b/src/Microsoft.DotNet.Helix/Sdk/CheckHelixJobStatus.cs
index 2cc315a4899..868582dff4c 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CheckHelixJobStatus.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CheckHelixJobStatus.cs
@@ -10,6 +10,7 @@
namespace Microsoft.DotNet.Helix.Sdk
{
+ [MSBuildMultiThreadableTask]
public class CheckHelixJobStatus : HelixTask
{
///
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateFailedTestsForFailedWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateFailedTestsForFailedWorkItems.cs
index 1fa4d85411c..2a820d46665 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateFailedTestsForFailedWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateFailedTestsForFailedWorkItems.cs
@@ -12,6 +12,7 @@
namespace Microsoft.DotNet.Helix.Sdk
{
+ [MSBuildMultiThreadableTask]
public class CreateTestsForWorkItems : AzureDevOpsTask
{
[Required]
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateMTPWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateMTPWorkItems.cs
index 132c35f314b..58bdd6febd9 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateMTPWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateMTPWorkItems.cs
@@ -17,6 +17,7 @@ namespace Microsoft.DotNet.Helix.Sdk
/// run directly with 'dotnet exec'. This applies to MSTest 4.x, xUnit v3 with MTP,
/// NUnit with MTP, TUnit, and any custom MTP-based test framework.
///
+ [MSBuildMultiThreadableTask]
public class CreateMTPWorkItems : BaseTask
{
///
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
index e8f00390894..742e90147da 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
@@ -15,6 +15,7 @@ namespace Microsoft.DotNet.Helix.Sdk
///
/// MSBuild custom task to create HelixWorkItems for provided Android application packages.
///
+ [MSBuildMultiThreadableTask]
public class CreateXHarnessAndroidWorkItems : XHarnessTaskBase
{
public static class MetadataNames
@@ -73,6 +74,11 @@ private async Task PrepareWorkItem(IZipArchiveManager zipArchiveManag
{
var (workItemName, apkPath) = GetNameAndPath(appPackage, MetadataNames.ApkPath, fileSystem);
+ // The APK path is documented as relative (see tools/xharness-runner/Readme.md), so it has to be
+ // resolved against the project directory before it reaches IFileSystem/ZipArchiveManager, which
+ // use raw File/Directory APIs and would otherwise bind to the shared node's current directory.
+ apkPath = TaskEnvironment.GetAbsolutePath(apkPath);
+
if (!fileSystem.FileExists(apkPath))
{
Log.LogError($"App package not found in {apkPath}");
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
index a1d7c943d8c..1e3ee06c70e 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
@@ -15,6 +15,7 @@ namespace Microsoft.DotNet.Helix.Sdk
///
/// MSBuild custom task to create HelixWorkItems for provided iOS app bundle paths.
///
+ [MSBuildMultiThreadableTask]
public class CreateXHarnessAppleWorkItems : XHarnessTaskBase
{
public const string iOSTargetName = "ios-device";
@@ -63,7 +64,9 @@ public static class MetadataNames
public override void ConfigureServices(IServiceCollection collection)
{
- collection.TryAddProvisioningProfileProvider(ProvisioningProfileUrl, TmpDir);
+ // TmpDir is optional and flows into the provisioning profile provider, which does raw file IO.
+ string tmpDir = string.IsNullOrEmpty(TmpDir) ? TmpDir : TaskEnvironment.GetAbsolutePath(TmpDir);
+ collection.TryAddProvisioningProfileProvider(ProvisioningProfileUrl, tmpDir);
collection.TryAddTransient();
collection.TryAddTransient();
collection.TryAddSingleton(Log);
@@ -101,6 +104,12 @@ private async Task PrepareWorkItem(
appFolderPath = appFolderPath.TrimEnd(Path.DirectorySeparatorChar);
+ // The app bundle path is documented as relative (see tools/xharness-runner/Readme.md), so it has to be
+ // resolved against the project directory before it reaches IFileSystem/ZipArchiveManager, which use raw
+ // File/Directory APIs and would otherwise bind to the shared node's current directory. Trim first so the
+ // trailing separator does not turn a bundle directory into a different resolved path.
+ appFolderPath = TaskEnvironment.GetAbsolutePath(appFolderPath);
+
bool isAlreadyArchived = appFolderPath.EndsWith(".zip");
if (isAlreadyArchived && workItemName.EndsWith(".app"))
{
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXUnitWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXUnitWorkItems.cs
index d41471235fb..286b3bd8eca 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateXUnitWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXUnitWorkItems.cs
@@ -15,6 +15,7 @@ namespace Microsoft.DotNet.Helix.Sdk
///
/// MSBuild custom task to create HelixWorkItems given xUnit project publish information
///
+ [MSBuildMultiThreadableTask]
public class CreateXUnitWorkItems : BaseTask
{
///
diff --git a/src/Microsoft.DotNet.Helix/Sdk/DownloadFromResultsContainer.cs b/src/Microsoft.DotNet.Helix/Sdk/DownloadFromResultsContainer.cs
index 13ac6caac6b..8319f53417c 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/DownloadFromResultsContainer.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/DownloadFromResultsContainer.cs
@@ -12,8 +12,12 @@
namespace Microsoft.DotNet.Helix.Sdk
{
- public class DownloadFromResultsContainer : HelixTask, ICancelableTask
+ [MSBuildMultiThreadableTask]
+ public class DownloadFromResultsContainer : HelixTask, ICancelableTask, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
public ITaskItem[] WorkItems { get; set; }
@@ -49,7 +53,7 @@ protected override async Task ExecuteCore(CancellationToken cancellationToken)
Log.LogMessage(MessageImportance.High, $"Downloading result files for job {JobId}");
- DirectoryInfo directory = Directory.CreateDirectory(Path.Combine(OutputDirectory, JobId));
+ DirectoryInfo directory = Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, JobId)));
using (FileStream stream = File.Open(Path.Combine(directory.FullName, MetadataFile), FileMode.Create, FileAccess.Write))
using (var writer = new StreamWriter(stream))
{
@@ -74,7 +78,7 @@ private async Task DownloadFilesForWorkItem(ITaskItem workItem, string directory
var allAvailableFiles = await HelixApi.WorkItem.ListFilesAsync(workItemName, JobId, true, ct);
var resultsUri = await HelixApi.Job.ResultsAsync(JobId, ct);
- DirectoryInfo destinationDir = Directory.CreateDirectory(Path.Combine(directoryPath, workItemName));
+ DirectoryInfo destinationDir = Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.Combine(directoryPath, workItemName)));
foreach (string file in filesToDownload)
{
try
diff --git a/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs b/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs
index 59723ce43c2..bd41f95c294 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/FindDotNetCliPackage.cs
@@ -17,6 +17,7 @@
namespace Microsoft.DotNet.Helix.Sdk
{
+ [MSBuildMultiThreadableTask]
public class FindDotNetCliPackage : MSBuildTaskBase
{
// Use lots of retries since an Http Client failure here means failure to send to Helix
@@ -55,7 +56,7 @@ public class FindDotNetCliPackage : MSBuildTaskBase
[Output]
public string PackageUri { get; set; }
- private static HttpClient _client;
+ private HttpClient _client;
private HttpMessageHandler _httpMessageHandler;
public override void ConfigureServices(IServiceCollection collection)
diff --git a/src/Microsoft.DotNet.Helix/Sdk/GetHelixWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/GetHelixWorkItems.cs
index d8d25d70c19..fc6aac62896 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/GetHelixWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/GetHelixWorkItems.cs
@@ -16,6 +16,7 @@
namespace Microsoft.DotNet.Helix.Sdk
{
+ [MSBuildMultiThreadableTask]
public class GetHelixWorkItems : HelixTask
{
public const int DelayBetweenHelixApiCallsInMs = 500;
diff --git a/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs b/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
index 5e636785c46..d49f6ff36aa 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
@@ -15,8 +15,24 @@ namespace Microsoft.DotNet.Helix.Sdk
/// Task that installs a .NET tool in a given folder.
/// Handles parallel builds that install the same tool.
///
- public class InstallDotNetTool : MSBuildTaskBase
+ // TODO: https://github.com/dotnet/arcade/issues/17378 - not yet annotated with
+ // [MSBuildMultiThreadableTask]. Paths are resolved through TaskEnvironment below, but the child
+ // `dotnet tool install` process is spawned through Microsoft.Arcade.Common's ICommandFactory,
+ // which builds its ProcessStartInfo from the ambient process environment rather than from an
+ // injected TaskEnvironment. Per-project environment variables would therefore leak between
+ // projects sharing a node, so MSBuild keeps routing this task through the out-of-proc TaskHost.
+ //
+ // Implementing IMultiThreadableTask without the attribute is deliberate. Routing is decided by
+ // the attribute alone (TaskRouter.NeedsTaskHostInMultiThreadedMode); it cannot key off the
+ // interface, because ToolTask implements it and that would opt in every ToolTask-derived task in
+ // the ecosystem. The interface only causes TaskEnvironment to be injected. Do not remove it to
+ // "make this safe" - that would revert the path resolution below to the process current
+ // directory while leaving the task exactly as unsafe as it is now.
+ public class InstallDotNetTool : MSBuildTaskBase, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// The name of the tool to install (same as the NuGet package name, e.g. Microsoft.DotNet.XHarness.CLI)
///
@@ -96,11 +112,12 @@ public bool ExecuteTask(ICommandFactory commandFactory, IFileSystem fileSystem,
// We install the tool in [dest]/[name]/[version] because if we tried to install 2 versions in the same dir,
// `dotnet tool install` would fail.
var version = Version.ToLowerInvariant();
- ToolPath = Path.Combine(DestinationPath, Name, Version);
+ string absoluteDestinationPath = TaskEnvironment.GetAbsolutePath(DestinationPath);
+ ToolPath = Path.Combine(absoluteDestinationPath, Name, Version);
if (!fileSystem.DirectoryExists(ToolPath))
{
- fileSystem.CreateDirectory(DestinationPath);
+ fileSystem.CreateDirectory(absoluteDestinationPath);
}
string versionInstallPath = Path.Combine(ToolPath, ".store", Name.ToLowerInvariant(), version);
@@ -156,14 +173,16 @@ private bool InstallTool(ICommandFactory commandFactory)
args.Add(Name);
- var executable = string.IsNullOrEmpty(DotnetPath) ? "dotnet" : DotnetPath;
+ // A bare "dotnet" is resolved through PATH by the process launcher and must stay
+ // unresolved; only an explicitly supplied path is made absolute.
+ var executable = string.IsNullOrEmpty(DotnetPath) ? "dotnet" : (string)TaskEnvironment.GetAbsolutePath(DotnetPath);
Log.LogMessage($"Executing {DotnetPath} {string.Join(" ", args)}");
ICommand command = commandFactory.Create(executable, args);
if (!string.IsNullOrEmpty(WorkingDirectory))
{
- command.WorkingDirectory(WorkingDirectory);
+ command.WorkingDirectory(TaskEnvironment.GetAbsolutePath(WorkingDirectory));
}
CommandResult result = command.Execute();
diff --git a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
index e2bacc45c52..a4e0f7a7753 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
@@ -16,7 +16,12 @@
namespace Microsoft.DotNet.Helix.Sdk
{
- public class SendHelixJob : HelixTask
+ // Deliberately not marked multithreadable: SendAsync reaches JobDefinition, which reads
+ // BUILD_REPOSITORY_NAME, BUILD_SOURCEBRANCH, SYSTEM_TEAMPROJECT and BUILD_REASON straight from
+ // Environment (JobSender/JobDefinition.cs:209-223,402-423). In a shared node those reads see
+ // process-wide values rather than the project's, so a job could be tagged with another project's
+ // source metadata. Migrating requires threading TaskEnvironment into JobDefinition.
+ public class SendHelixJob : HelixTask, IMultiThreadableTask
{
public static class MetadataNames
{
@@ -40,6 +45,9 @@ public static class MetadataNames
public const string AsArchive = "AsArchive";
}
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// The 'type' value reported to Helix
///
@@ -307,7 +315,7 @@ private IJobDefinition AddBuildVariableProperty(IJobDefinition def, string key,
{
string envName = FromAzdoVariableNameToEnvironmentVariableName(azdoVariableName);
- var value = Environment.GetEnvironmentVariable(envName);
+ var value = TaskEnvironment.GetEnvironmentVariable(envName);
if (string.IsNullOrEmpty(value))
{
return def;
@@ -548,7 +556,7 @@ private IJobDefinition AddCorrelationPayload(IJobDefinition def, ITaskItem corre
}
}
- if (Directory.Exists(path))
+ if (Directory.Exists(TaskEnvironment.GetAbsolutePath(path)))
{
string includeDirectoryNameStr = correlationPayload.GetMetadata(MetadataNames.IncludeDirectoryName);
if (!bool.TryParse(includeDirectoryNameStr, out bool includeDirectoryName))
@@ -564,7 +572,7 @@ private IJobDefinition AddCorrelationPayload(IJobDefinition def, ITaskItem corre
}
- if (File.Exists(path))
+ if (File.Exists(TaskEnvironment.GetAbsolutePath(path)))
{
string asArchiveStr = correlationPayload.GetMetadata(MetadataNames.AsArchive);
if (!bool.TryParse(asArchiveStr, out bool asArchive))
diff --git a/src/Microsoft.DotNet.Helix/Sdk/StartAzurePipelinesTestRun.cs b/src/Microsoft.DotNet.Helix/Sdk/StartAzurePipelinesTestRun.cs
index ef838479e70..c94e88fbe6c 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/StartAzurePipelinesTestRun.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/StartAzurePipelinesTestRun.cs
@@ -10,6 +10,7 @@
namespace Microsoft.DotNet.Helix.AzureDevOps
{
+ [MSBuildMultiThreadableTask]
public class StartAzurePipelinesTestRun : AzureDevOpsTask
{
[Required]
diff --git a/src/Microsoft.DotNet.Helix/Sdk/StopAzurePipelinesTestRun.cs b/src/Microsoft.DotNet.Helix/Sdk/StopAzurePipelinesTestRun.cs
index 2c2e1ce1c92..7f3f4b2acc2 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/StopAzurePipelinesTestRun.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/StopAzurePipelinesTestRun.cs
@@ -10,6 +10,7 @@
namespace Microsoft.DotNet.Helix.AzureDevOps
{
+ [MSBuildMultiThreadableTask]
public class StopAzurePipelinesTestRun : AzureDevOpsTask
{
[Required]
diff --git a/src/Microsoft.DotNet.Helix/Sdk/WaitForHelixJobCompletion.cs b/src/Microsoft.DotNet.Helix/Sdk/WaitForHelixJobCompletion.cs
index ba2a1b1d225..f6775f7dfb6 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/WaitForHelixJobCompletion.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/WaitForHelixJobCompletion.cs
@@ -12,6 +12,7 @@
namespace Microsoft.DotNet.Helix.Sdk
{
+ [MSBuildMultiThreadableTask]
public class WaitForHelixJobCompletion : HelixTask
{
internal const string HelixControllerWorkQueueingWorkItemName = "HelixController Work Queueing";
diff --git a/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs b/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs
index 4a2abd0c937..106512d8118 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs
@@ -13,12 +13,15 @@ namespace Microsoft.DotNet.Helix.Sdk
///
/// MSBuild custom task to create HelixWorkItems for provided Android application packages.
///
- public abstract class XHarnessTaskBase : MSBuildTaskBase
+ public abstract class XHarnessTaskBase : MSBuildTaskBase, IMultiThreadableTask
{
private static readonly TimeSpan s_defaultWorkItemTimeout = TimeSpan.FromMinutes(20);
private static readonly TimeSpan s_defaultTestTimeout = TimeSpan.FromMinutes(12);
private static readonly TimeSpan s_telemetryBuffer = TimeSpan.FromMinutes(2); // extra time to send the XHarness telemetry
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
public class MetadataName
{
public const string TestTimeout = "TestTimeout";
diff --git a/src/Microsoft.DotNet.NuGetRepack/tasks/src/NuGetVersionUpdater.cs b/src/Microsoft.DotNet.NuGetRepack/tasks/src/NuGetVersionUpdater.cs
index bd92f54d76d..80bab0f82f4 100644
--- a/src/Microsoft.DotNet.NuGetRepack/tasks/src/NuGetVersionUpdater.cs
+++ b/src/Microsoft.DotNet.NuGetRepack/tasks/src/NuGetVersionUpdater.cs
@@ -54,17 +54,21 @@ public PackageInfo(
}
public static void Run(
- IEnumerable packagePaths,
- string outDirectoryOpt,
+ IEnumerable packagePaths,
+ Microsoft.Build.Framework.AbsolutePath? outDirectoryOpt,
VersionTranslation translation,
bool exactVersions,
Func allowPreReleaseDependency = null)
{
- string tempDirectoryOpt;
+ Microsoft.Build.Framework.AbsolutePath? tempDirectoryOpt;
if (outDirectoryOpt != null)
{
- tempDirectoryOpt = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
- Directory.CreateDirectory(tempDirectoryOpt);
+ // MSBuildTask0002: the temp root is only used as the parent of a freshly generated unique
+ // directory/file name, so it is never shared between concurrently running tasks.
+ #pragma warning disable MSBuildTask0002
+ tempDirectoryOpt = new Microsoft.Build.Framework.AbsolutePath(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
+ #pragma warning restore MSBuildTask0002
+ Directory.CreateDirectory(tempDirectoryOpt.Value);
}
else
{
@@ -79,7 +83,7 @@ public static void Run(
if (outDirectoryOpt != null)
{
- SavePackages(packages, outDirectoryOpt);
+ SavePackages(packages, outDirectoryOpt.Value);
}
}
finally
@@ -92,12 +96,12 @@ public static void Run(
if (tempDirectoryOpt != null)
{
- Directory.Delete(tempDirectoryOpt, recursive: true);
+ Directory.Delete(tempDirectoryOpt.Value, recursive: true);
}
}
}
- private static void LoadPackages(IEnumerable packagePaths, Dictionary packages, string tempDirectoryOpt, VersionTranslation translation)
+ private static void LoadPackages(IEnumerable packagePaths, Dictionary packages, Microsoft.Build.Framework.AbsolutePath? tempDirectoryOpt, VersionTranslation translation)
{
bool readOnly = tempDirectoryOpt == null;
@@ -113,8 +117,8 @@ private static void LoadPackages(IEnumerable packagePaths, Dictionary packagePaths, Dictionary packages,
ThrowExceptions(errors);
}
- private static void SavePackages(Dictionary packages, string outDirectory)
+ private static void SavePackages(Dictionary packages, Microsoft.Build.Framework.AbsolutePath outDirectory)
{
Directory.CreateDirectory(outDirectory);
@@ -371,7 +375,7 @@ private static void SavePackages(Dictionary packages, strin
try
{
- File.Copy(package.TempPathOpt, finalPath, overwrite: true);
+ File.Copy(new Microsoft.Build.Framework.AbsolutePath(package.TempPathOpt), new Microsoft.Build.Framework.AbsolutePath(finalPath), overwrite: true);
}
catch (Exception e)
{
diff --git a/src/Microsoft.DotNet.NuGetRepack/tasks/src/ReplacePackageParts.cs b/src/Microsoft.DotNet.NuGetRepack/tasks/src/ReplacePackageParts.cs
index e715f80a5d4..01e22a3bca1 100644
--- a/src/Microsoft.DotNet.NuGetRepack/tasks/src/ReplacePackageParts.cs
+++ b/src/Microsoft.DotNet.NuGetRepack/tasks/src/ReplacePackageParts.cs
@@ -17,8 +17,12 @@ namespace Microsoft.DotNet.Tools
///
/// Replaces content of files in specified package with new content and updates version of the package.
///
- public sealed class ReplacePackageParts : Microsoft.Build.Utilities.Task
+ [MSBuildMultiThreadableTask]
+ public sealed class ReplacePackageParts : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
///
/// Full path to the package to process.
///
@@ -107,10 +111,14 @@ private void ExecuteImpl()
string packageId = null;
SemanticVersion packageVersion = null;
+ // MSBuildTask0002: the temp root is only used as the parent of a freshly generated unique
+ // directory/file name, so it is never shared between concurrently running tasks.
+ #pragma warning disable MSBuildTask0002
string tempPackagePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+ #pragma warning restore MSBuildTask0002
try
{
- File.Copy(SourcePackage, tempPackagePath);
+ File.Copy(TaskEnvironment.GetAbsolutePath(SourcePackage), TaskEnvironment.GetAbsolutePath(tempPackagePath));
using (var package = Package.Open(tempPackagePath, FileMode.Open, FileAccess.ReadWrite))
{
@@ -178,7 +186,7 @@ private void ExecuteImpl()
Stream replacementStream;
try
{
- replacementStream = File.OpenRead(replacementFilePath);
+ replacementStream = File.OpenRead(TaskEnvironment.GetAbsolutePath(replacementFilePath));
}
catch (Exception e)
{
@@ -216,19 +224,19 @@ private void ExecuteImpl()
}
// remove signature if present (the signature part is not accessible thru Package API):
- using (var archive = new ZipArchive(File.Open(tempPackagePath, FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update))
+ using (var archive = new ZipArchive(File.Open(TaskEnvironment.GetAbsolutePath(tempPackagePath), FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update))
{
archive.Entries.FirstOrDefault(e => e.FullName == NuGetUtils.SignaturePartUri)?.Delete();
}
NewPackage = Path.Combine(DestinationFolder, packageId + "." + packageVersion + ".nupkg");
- Directory.CreateDirectory(DestinationFolder);
- File.Copy(tempPackagePath, NewPackage, overwrite: true);
+ Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(DestinationFolder));
+ File.Copy(TaskEnvironment.GetAbsolutePath(tempPackagePath), TaskEnvironment.GetAbsolutePath(NewPackage), overwrite: true);
}
finally
{
- File.Delete(tempPackagePath);
+ File.Delete(TaskEnvironment.GetAbsolutePath(tempPackagePath));
}
}
diff --git a/src/Microsoft.DotNet.NuGetRepack/tasks/src/UpdatePackageVersionTask.cs b/src/Microsoft.DotNet.NuGetRepack/tasks/src/UpdatePackageVersionTask.cs
index 751c56330df..309f690375b 100644
--- a/src/Microsoft.DotNet.NuGetRepack/tasks/src/UpdatePackageVersionTask.cs
+++ b/src/Microsoft.DotNet.NuGetRepack/tasks/src/UpdatePackageVersionTask.cs
@@ -10,8 +10,12 @@
namespace Microsoft.DotNet.Tools
{
- public class UpdatePackageVersionTask : Microsoft.Build.Utilities.Task
+ [MSBuildMultiThreadableTask]
+ public class UpdatePackageVersionTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
public string VersionKind { get; set; }
[Required]
@@ -24,7 +28,12 @@ public class UpdatePackageVersionTask : Microsoft.Build.Utilities.Task
public bool AllowPreReleaseDependencies { get; set; }
+ // MSBuildTask0005: the only remaining unsafe call in this chain is Path.GetTempPath(), which
+ // is used purely as the parent of a freshly generated GUID directory, so it is never shared
+ // between concurrently running tasks.
+ #pragma warning disable MSBuildTask0005
public override bool Execute()
+ #pragma warning restore MSBuildTask0005
{
try
{
@@ -61,7 +70,7 @@ private void ExecuteImpl()
try
{
- NuGetVersionUpdater.Run(Packages, OutputDirectory, translation, ExactVersions, allowPreReleaseDependency: (packageId, dependencyId, dependencyVersion) =>
+ NuGetVersionUpdater.Run(Packages.Select(TaskEnvironment.GetAbsolutePath), string.IsNullOrEmpty(OutputDirectory) ? null : TaskEnvironment.GetAbsolutePath(OutputDirectory), translation, ExactVersions, allowPreReleaseDependency: (packageId, dependencyId, dependencyVersion) =>
{
if (AllowPreReleaseDependencies)
{
@@ -75,7 +84,7 @@ private void ExecuteImpl()
if (translation == VersionTranslation.Release)
{
- File.WriteAllLines(Path.Combine(OutputDirectory, "PreReleaseDependencies.txt"), preReleaseDependencies.Distinct());
+ File.WriteAllLines(TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, "PreReleaseDependencies.txt")), preReleaseDependencies.Distinct());
}
}
catch (AggregateException e)
diff --git a/src/Microsoft.DotNet.NuGetRepack/tests/VersionUpdaterTests.cs b/src/Microsoft.DotNet.NuGetRepack/tests/VersionUpdaterTests.cs
index d4d76da2883..7e240addedd 100644
--- a/src/Microsoft.DotNet.NuGetRepack/tests/VersionUpdaterTests.cs
+++ b/src/Microsoft.DotNet.NuGetRepack/tests/VersionUpdaterTests.cs
@@ -8,6 +8,7 @@
using System.Xml.Linq;
using Microsoft.DotNet.Tools.Tests.Utilities;
using Xunit;
+using Microsoft.Build.Framework;
namespace Microsoft.DotNet.Tools.Tests
{
@@ -78,8 +79,8 @@ public void TestPackagesSemVer1()
var d_rel = Path.Combine(dir, TestResources.ReleasePackages.NameD);
var g_rel = Path.Combine(dir, TestResources.ReleasePackages.NameG);
- NuGetVersionUpdater.Run(new[] { a_daily, b_daily, c_daily, d_daily, g_daily }, dir, VersionTranslation.Release, exactVersions: false);
- NuGetVersionUpdater.Run(new[] { a_daily, b_daily, c_daily, d_daily, g_daily }, dir, VersionTranslation.PreRelease, exactVersions: false);
+ NuGetVersionUpdater.Run(new[] { a_daily, b_daily, c_daily, d_daily, g_daily }.Select(p => new AbsolutePath(p)), new AbsolutePath(dir), VersionTranslation.Release, exactVersions: false);
+ NuGetVersionUpdater.Run(new[] { a_daily, b_daily, c_daily, d_daily, g_daily }.Select(p => new AbsolutePath(p)), new AbsolutePath(dir), VersionTranslation.PreRelease, exactVersions: false);
AssertPackagesEqual(TestResources.ReleasePackages.TestPackageA, File.ReadAllBytes(a_rel));
AssertPackagesEqual(TestResources.ReleasePackages.TestPackageB, File.ReadAllBytes(b_rel));
@@ -112,8 +113,8 @@ public void TestPackagesSemVer2()
var e_rel = Path.Combine(dir, TestResources.ReleasePackages.NameE);
var f_rel = Path.Combine(dir, TestResources.ReleasePackages.NameF);
- NuGetVersionUpdater.Run(new[] { e_daily, f_daily }, dir, VersionTranslation.Release, exactVersions: true);
- NuGetVersionUpdater.Run(new[] { e_daily, f_daily }, dir, VersionTranslation.PreRelease, exactVersions: true);
+ NuGetVersionUpdater.Run(new[] { e_daily, f_daily }.Select(p => new AbsolutePath(p)), new AbsolutePath(dir), VersionTranslation.Release, exactVersions: true);
+ NuGetVersionUpdater.Run(new[] { e_daily, f_daily }.Select(p => new AbsolutePath(p)), new AbsolutePath(dir), VersionTranslation.PreRelease, exactVersions: true);
AssertPackagesEqual(TestResources.ReleasePackages.TestPackageE, File.ReadAllBytes(e_rel));
AssertPackagesEqual(TestResources.ReleasePackages.TestPackageF, File.ReadAllBytes(f_rel));
@@ -135,10 +136,10 @@ public void TestValidation()
File.WriteAllBytes(b_daily = Path.Combine(dir, TestResources.DailyBuildPackages.NameB), TestResources.DailyBuildPackages.TestPackageB);
File.WriteAllBytes(c_daily = Path.Combine(dir, TestResources.DailyBuildPackages.NameC), TestResources.DailyBuildPackages.TestPackageC);
- var e1 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { c_daily }, outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
+ var e1 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { c_daily }.Select(p => new AbsolutePath(p)), outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
AssertEx.AreEqual("Package 'TestPackageC' depends on a pre-release package 'TestPackageB, [1.0.0-beta-12345-01]'", e1.Message);
- var e2 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { a_daily }, outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
+ var e2 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { a_daily }.Select(p => new AbsolutePath(p)), outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
AssertEx.Equal(new[]
{
"System.InvalidOperationException: Package 'TestPackageA' depends on a pre-release package 'TestPackageB, 1.0.0-beta-12345-01'",
@@ -146,14 +147,14 @@ public void TestValidation()
"System.InvalidOperationException: Package 'TestPackageA' depends on a pre-release package 'TestPackageC, 1.0.0-beta-12345-01'"
}, e2.InnerExceptions.Select(i => i.ToString()));
- var e3 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { a_daily, b_daily }, outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
+ var e3 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { a_daily, b_daily }.Select(p => new AbsolutePath(p)), outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
AssertEx.Equal(new[]
{
"System.InvalidOperationException: Package 'TestPackageA' depends on a pre-release package 'TestPackageC, (, 1.0.0-beta-12345-01]'",
"System.InvalidOperationException: Package 'TestPackageA' depends on a pre-release package 'TestPackageC, 1.0.0-beta-12345-01'"
}, e3.InnerExceptions.Select(i => i.ToString()));
- var e4 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { a_daily, c_daily }, outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
+ var e4 = Assert.Throws(() => NuGetVersionUpdater.Run(new[] { a_daily, c_daily }.Select(p => new AbsolutePath(p)), outDirectoryOpt: null, VersionTranslation.Release, exactVersions: false));
AssertEx.Equal(new[]
{
"System.InvalidOperationException: Package 'TestPackageA' depends on a pre-release package 'TestPackageB, 1.0.0-beta-12345-01'",
@@ -175,7 +176,7 @@ public void TestDotnetToolValidation()
string normal_package_b_daily;
File.WriteAllBytes(normal_package_b_daily = Path.Combine(dir, TestResources.DailyBuildPackages.NameB), TestResources.DailyBuildPackages.TestPackageB);
- NuGetVersionUpdater.Run(new[] { dotnet_tool, normal_package_b_daily }, outDirectoryOpt: outputDir, VersionTranslation.Release, exactVersions: false);
+ NuGetVersionUpdater.Run(new[] { dotnet_tool, normal_package_b_daily }.Select(p => new AbsolutePath(p)), outDirectoryOpt: new AbsolutePath(outputDir), VersionTranslation.Release, exactVersions: false);
// Only contain normal package. dotnet tool package is skipped
Assert.Single(Directory.EnumerateFiles(outputDir), fullPath => Path.GetFileNameWithoutExtension(fullPath) == "TestPackageB.1.0.0");
diff --git a/src/Microsoft.DotNet.PackageTesting.Tests/GetCompatibilePackageTargetFrameworksTests.cs b/src/Microsoft.DotNet.PackageTesting.Tests/GetCompatibilePackageTargetFrameworksTests.cs
index 1fbcc5ff995..0f585d2ca6f 100644
--- a/src/Microsoft.DotNet.PackageTesting.Tests/GetCompatibilePackageTargetFrameworksTests.cs
+++ b/src/Microsoft.DotNet.PackageTesting.Tests/GetCompatibilePackageTargetFrameworksTests.cs
@@ -10,9 +10,11 @@ namespace Microsoft.DotNet.PackageTesting.Tests
{
public class GetCompatibilePackageTargetFrameworksTests
{
+ private readonly GetCompatiblePackageTargetFrameworks _task = new();
+
public GetCompatibilePackageTargetFrameworksTests()
{
- GetCompatiblePackageTargetFrameworks.Initialize("netcoreapp3.1;net5.0;net6.0;net461;net462;net471;net472;netstandard2.0;netstandard2.1");
+ _task.Initialize("netcoreapp3.1;net5.0;net6.0;net461;net462;net471;net472;netstandard2.0;netstandard2.1");
}
public static IEnumerable