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 PackageTfmData => new List @@ -137,7 +139,7 @@ public GetCompatibilePackageTargetFrameworksTests() public void GetCompatibleFrameworks(List filePaths, List expectedTestFrameworks) { Package package = new("TestPackage", "1.0.0", filePaths, Enumerable.Empty()); - IEnumerable actualTestFrameworks = GetCompatiblePackageTargetFrameworks.GetTestFrameworks(package, "netcoreapp3.1"); + IEnumerable actualTestFrameworks = _task.GetTestFrameworks(package, "netcoreapp3.1"); CollectionsEqual(expectedTestFrameworks, actualTestFrameworks); } @@ -153,7 +155,7 @@ public void GetCompatibleFrameworksFromDependencies() NuGetFramework.Parse("net6.0"), }; Package package = new("TestPackage", "1.0.0", Enumerable.Empty(), dependencyFrameworks); - IEnumerable actualTestFrameworks = GetCompatiblePackageTargetFrameworks.GetTestFrameworks(package, "netcoreapp3.1"); + IEnumerable actualTestFrameworks = _task.GetTestFrameworks(package, "netcoreapp3.1"); var expectedTestFrameworks = new[] { diff --git a/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs b/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs index 4fa94bfe4c8..350ac34ca19 100644 --- a/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs +++ b/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs @@ -3,7 +3,6 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.DotNet.Build.Tasks; using NuGet.Frameworks; using System; using System.Collections.Generic; @@ -11,10 +10,14 @@ namespace Microsoft.DotNet.PackageTesting { - public class GetCompatiblePackageTargetFrameworks : BuildTask + [MSBuildMultiThreadableTask] + public class GetCompatiblePackageTargetFrameworks : Microsoft.Build.Utilities.Task, IMultiThreadableTask { - private static List allTargetFrameworks = new(); - private static Dictionary> packageTfmMapping = new(); + private readonly List allTargetFrameworks = new(); + private readonly Dictionary> packageTfmMapping = new(); + + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; [Required] public string[] PackagePaths { get; set; } @@ -39,7 +42,7 @@ public override bool Execute() foreach (var packagePath in PackagePaths) { - Package package = NupkgParser.CreatePackageObject(packagePath); + Package package = NupkgParser.CreatePackageObject(TaskEnvironment.GetAbsolutePath(packagePath)); IEnumerable testFrameworks = GetTestFrameworks(package, minDotnetTargetFramework); testProjects.AddRange(CreateItemFromTestFramework(package.PackageId, package.Version, testFrameworks)); @@ -56,7 +59,7 @@ public override bool Execute() return !Log.HasLoggedErrors; } - public static IEnumerable GetTestFrameworks(Package package, string minDotnetTargetFramework) + public IEnumerable GetTestFrameworks(Package package, string minDotnetTargetFramework) { List frameworksToTest= new(); IEnumerable packageTargetFrameworks = package.FrameworksInPackage; @@ -81,7 +84,7 @@ public static IEnumerable GetTestFrameworks(Package package, str return frameworksToTest.Where(tfm => allTargetFrameworks.Contains(tfm)).Distinct(); } - public static void Initialize(string targetFrameworks) + public void Initialize(string targetFrameworks) { // Defining the set of known frameworks that we care to test foreach (var tfm in targetFrameworks.Split(';')) diff --git a/src/Microsoft.DotNet.PackageTesting/Microsoft.DotNet.PackageTesting.csproj b/src/Microsoft.DotNet.PackageTesting/Microsoft.DotNet.PackageTesting.csproj index 15099f9f9a2..9bdf43d4520 100644 --- a/src/Microsoft.DotNet.PackageTesting/Microsoft.DotNet.PackageTesting.csproj +++ b/src/Microsoft.DotNet.PackageTesting/Microsoft.DotNet.PackageTesting.csproj @@ -13,8 +13,4 @@ - - - - diff --git a/src/Microsoft.DotNet.PackageTesting/VerifyClosure.cs b/src/Microsoft.DotNet.PackageTesting/VerifyClosure.cs index f64f2bf9347..a0477aadd09 100644 --- a/src/Microsoft.DotNet.PackageTesting/VerifyClosure.cs +++ b/src/Microsoft.DotNet.PackageTesting/VerifyClosure.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; -using Microsoft.DotNet.Build.Tasks; using System; using System.Collections.Generic; using System.IO; @@ -18,9 +17,13 @@ namespace Microsoft.DotNet.PackageTesting /// /// Verifies the closure of a set of DLLs, making sure all files are present and no cycles exist /// - public class VerifyClosure : BuildTask + [MSBuildMultiThreadableTask] + public class VerifyClosure : 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; + /// /// Sources to scan. Items can be directories or files. /// @@ -93,7 +96,7 @@ private void LoadSources() private void AddSourceFile(string file) { - var assemblyInfo = AssemblyInfo.GetAssemblyInfo(file); + var assemblyInfo = AssemblyInfo.GetAssemblyInfo(TaskEnvironment.GetAbsolutePath(file)); if (assemblyInfo == null) { @@ -244,7 +247,7 @@ void CheckDependencies(Stack depStack) if (assm.State == CheckState.Unchecked) { - Log.LogMessage(LogImportance.Low, $"Checked {assm.Path}"); + Log.LogMessage(MessageImportance.Low, $"Checked {assm.Path}"); assm.State = CheckState.Checked; } @@ -340,7 +343,7 @@ private string PrintCycle(AssemblyInfo[] cycleStack) } private static XNamespace s_dgmlns = @"http://schemas.microsoft.com/vs/2009/dgml"; - private static void WriteDependencyGraph(string dependencyGraphFilePath, IEnumerable assemblies) + private void WriteDependencyGraph(string dependencyGraphFilePath, IEnumerable assemblies) { var doc = new XDocument(new XElement(s_dgmlns + "DirectedGraph")); @@ -387,7 +390,7 @@ private static void WriteDependencyGraph(string dependencyGraphFilePath, IEnumer new XAttribute("Background", "Green") )); - using (var file = File.Create(dependencyGraphFilePath)) + using (var file = File.Create(TaskEnvironment.GetAbsolutePath(dependencyGraphFilePath))) { doc.Save(file); } @@ -424,7 +427,7 @@ public AssemblyInfo(string path, string name, Version version, AssemblyReference public string[] ModuleReferences { get; } public CheckState State { get; set; } - public static AssemblyInfo GetAssemblyInfo(string path) + public static AssemblyInfo GetAssemblyInfo(Microsoft.Build.Framework.AbsolutePath path) { try { diff --git a/src/Microsoft.DotNet.PackageTesting/VerifyTypes.cs b/src/Microsoft.DotNet.PackageTesting/VerifyTypes.cs index 772a766b275..561eb4af33f 100644 --- a/src/Microsoft.DotNet.PackageTesting/VerifyTypes.cs +++ b/src/Microsoft.DotNet.PackageTesting/VerifyTypes.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; -using Microsoft.DotNet.Build.Tasks; using System; using System.Collections.Generic; using System.IO; @@ -15,8 +14,12 @@ namespace Microsoft.DotNet.PackageTesting /// /// Verifies no type overlap in a set of DLLs /// - public class VerifyTypes : BuildTask + [MSBuildMultiThreadableTask] + public class VerifyTypes : 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; + /// /// Sources to scan. Items can be directories or files. /// @@ -95,7 +98,7 @@ private void LoadSources() private void AddSourceFile(string file) { - var assemblyInfo = AssemblyInfo.GetAssemblyInfo(file); + var assemblyInfo = AssemblyInfo.GetAssemblyInfo(TaskEnvironment.GetAbsolutePath(file)); if (assemblyInfo != null) { @@ -145,7 +148,7 @@ public AssemblyInfo(string path, string name, string[] types) public string Name { get; } public string[] Types { get; } - public static AssemblyInfo GetAssemblyInfo(string path) + public static AssemblyInfo GetAssemblyInfo(Microsoft.Build.Framework.AbsolutePath path) { try { diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/Microsoft.DotNet.SharedFramework.Sdk.csproj b/src/Microsoft.DotNet.SharedFramework.Sdk/Microsoft.DotNet.SharedFramework.Sdk.csproj index c424ec15114..ee457e9896b 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/Microsoft.DotNet.SharedFramework.Sdk.csproj +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/Microsoft.DotNet.SharedFramework.Sdk.csproj @@ -26,7 +26,6 @@ - diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/CreateFrameworkListFile.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/CreateFrameworkListFile.cs index 68a1af12c73..09c34e0f178 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/CreateFrameworkListFile.cs +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/CreateFrameworkListFile.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; -using Microsoft.DotNet.Build.Tasks; using System; using System.Collections.Generic; using System.Globalization; @@ -13,8 +12,12 @@ namespace Microsoft.DotNet.SharedFramework.Sdk { - public class CreateFrameworkListFile : BuildTask + [MSBuildMultiThreadableTask] + public class CreateFrameworkListFile : 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; + /// /// Files to extract basic information from and include in the list. /// @@ -83,8 +86,8 @@ public override bool Execute() Item = item, Filename = Path.GetFileName(item.ItemSpec), TargetPath = item.GetMetadata("TargetPath"), - AssemblyName = FileUtilities.GetAssemblyName(item.ItemSpec), - FileVersion = FileUtilities.GetFileVersion(item.ItemSpec), + AssemblyName = FileUtilities.GetAssemblyName(TaskEnvironment.GetAbsolutePath(item.ItemSpec)), + FileVersion = FileUtilities.GetFileVersion(TaskEnvironment.GetAbsolutePath(item.ItemSpec)), IsNative = item.GetMetadata("IsNative") == "true", IsSymbolFile = item.GetMetadata("IsSymbolFile") == "true", IsPgoData = item.GetMetadata("IsPgoData") == "true", @@ -257,8 +260,8 @@ public override bool Execute() Log.LogError($"Classification matches no files: {unused}"); } - Directory.CreateDirectory(Path.GetDirectoryName(TargetFile)); - File.WriteAllText(TargetFile, frameworkManifest.ToString()); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(TargetFile))); + File.WriteAllText(TaskEnvironment.GetAbsolutePath(TargetFile), frameworkManifest.ToString()); return !Log.HasLoggedErrors; } diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/FileUtilities.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/FileUtilities.cs index b8d9348bf98..c36c15a06f3 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/FileUtilities.cs +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/FileUtilities.cs @@ -17,7 +17,7 @@ internal static partial class FileUtilities new[] { ".dll", ".exe" }, StringComparer.OrdinalIgnoreCase); - public static Version GetFileVersion(string sourcePath) + public static Version GetFileVersion(Microsoft.Build.Framework.AbsolutePath sourcePath) { var fvi = FileVersionInfo.GetVersionInfo(sourcePath); @@ -29,7 +29,7 @@ public static Version GetFileVersion(string sourcePath) return null; } - public static AssemblyName GetAssemblyName(string path) + public static AssemblyName GetAssemblyName(Microsoft.Build.Framework.AbsolutePath path) { if (!s_assemblyExtensions.Contains(Path.GetExtension(path))) { diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromFileList.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromFileList.cs index 2e4ba8dd5a9..ab1b41920f7 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromFileList.cs +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromFileList.cs @@ -3,7 +3,6 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.DotNet.Build.Tasks; using Microsoft.Extensions.DependencyModel; using System; using System.Collections.Generic; @@ -12,8 +11,12 @@ namespace Microsoft.DotNet.SharedFramework.Sdk { - public class GeneratePlatformManifestEntriesFromFileList : BuildTask + [MSBuildMultiThreadableTask] + public class GeneratePlatformManifestEntriesFromFileList : 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] public ITaskItem[] Files { get; set; } @@ -28,8 +31,8 @@ public override bool Execute() entries.Add(new PlatformManifestEntry { Name = file.ItemSpec, - AssemblyVersion = FileUtilities.GetAssemblyName(file.GetMetadata("OriginalFilePath"))?.Version.ToString() ?? string.Empty, - FileVersion = FileUtilities.GetFileVersion(file.GetMetadata("OriginalFilePath"))?.ToString() ?? string.Empty + AssemblyVersion = FileUtilities.GetAssemblyName(TaskEnvironment.GetAbsolutePath(file.GetMetadata("OriginalFilePath")))?.Version.ToString() ?? string.Empty, + FileVersion = FileUtilities.GetFileVersion(TaskEnvironment.GetAbsolutePath(file.GetMetadata("OriginalFilePath")))?.ToString() ?? string.Empty }); } diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromTemplate.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromTemplate.cs index 73f032a2d39..ffd8f3069dd 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromTemplate.cs +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GeneratePlatformManifestEntriesFromTemplate.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; @@ -11,8 +10,12 @@ namespace Microsoft.DotNet.SharedFramework.Sdk { - public class GeneratePlatformManifestEntriesFromTemplate : BuildTask + [MSBuildMultiThreadableTask] + public class GeneratePlatformManifestEntriesFromTemplate : 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] public ITaskItem[] PlatformManifestEntryTemplates { get; set; } @@ -39,8 +42,8 @@ public override bool Execute() entries.Add(new PlatformManifestEntry { Name = entryTemplate.ItemSpec, - AssemblyVersion = FileUtilities.GetAssemblyName(existingFile.ItemSpec)?.Version.ToString() ?? string.Empty, - FileVersion = FileUtilities.GetFileVersion(existingFile.ItemSpec)?.ToString() ?? string.Empty + AssemblyVersion = FileUtilities.GetAssemblyName(TaskEnvironment.GetAbsolutePath(existingFile.ItemSpec))?.Version.ToString() ?? string.Empty, + FileVersion = FileUtilities.GetFileVersion(TaskEnvironment.GetAbsolutePath(existingFile.ItemSpec))?.ToString() ?? string.Empty }); } else diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs index 76b1c22d9de..02e893ef4fd 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs @@ -3,7 +3,6 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Microsoft.DotNet.Build.Tasks; using Microsoft.Extensions.DependencyModel; using NuGet.RuntimeModel; using System; @@ -13,8 +12,12 @@ namespace Microsoft.DotNet.SharedFramework.Sdk { - public class GenerateSharedFrameworkDepsFile : BuildTask + [MSBuildMultiThreadableTask] + public class GenerateSharedFrameworkDepsFile : 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] public string TargetFrameworkMoniker { get; set; } @@ -60,8 +63,8 @@ public override bool Execute() } string filePath = file.ItemSpec; string fileName = Path.GetFileName(filePath); - string fileVersion = FileUtilities.GetFileVersion(filePath)?.ToString() ?? string.Empty; - Version assemblyVersion = FileUtilities.GetAssemblyName(filePath)?.Version; + string fileVersion = FileUtilities.GetFileVersion(TaskEnvironment.GetAbsolutePath(filePath))?.ToString() ?? string.Empty; + Version assemblyVersion = FileUtilities.GetAssemblyName(TaskEnvironment.GetAbsolutePath(filePath))?.Version; string cultureMaybe = file.GetMetadata("Culture"); if (!string.IsNullOrEmpty(cultureMaybe)) { @@ -97,7 +100,12 @@ public override bool Execute() if (IncludeFallbacksInDepsFile) { - RuntimeGraph runtimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(RuntimeIdentifierGraph); + // RuntimeIdentifierGraph is optional; only resolve it when set so an unset value keeps + // producing the existing error from ReadRuntimeGraph rather than throwing from GetAbsolutePath. + string runtimeIdentifierGraphPath = string.IsNullOrEmpty(RuntimeIdentifierGraph) + ? RuntimeIdentifierGraph + : TaskEnvironment.GetAbsolutePath(RuntimeIdentifierGraph); + RuntimeGraph runtimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(runtimeIdentifierGraphPath); runtimeFallbackGraph = runtimeGraph.Runtimes .Select(runtimeDict => runtimeGraph.ExpandRuntime(runtimeDict.Key)) .Where(expansion => expansion.Contains(RuntimeIdentifier)) @@ -115,16 +123,16 @@ public override bool Execute() var depsFilePath = Path.Combine(IntermediateOutputPath, depsFileName); try { - using var depsStream = File.Create(depsFilePath); + using var depsStream = File.Create(TaskEnvironment.GetAbsolutePath(depsFilePath)); new DependencyContextWriter().Write(context, depsStream); GeneratedDepsFile = new TaskItem(depsFilePath); } catch (Exception ex) { // If there is a problem, ensure we don't write a partially complete version to disk. - if (File.Exists(depsFilePath)) + if (File.Exists(TaskEnvironment.GetAbsolutePath(depsFilePath))) { - File.Delete(depsFilePath); + File.Delete(TaskEnvironment.GetAbsolutePath(depsFilePath)); } Log.LogErrorFromException(ex, false); return false; diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/ValidateFileVersions.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/ValidateFileVersions.cs index 79bf170071f..e18441fc75f 100644 --- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/ValidateFileVersions.cs +++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/ValidateFileVersions.cs @@ -6,14 +6,17 @@ using System.IO; using System.Linq; using Microsoft.Build.Framework; -using Microsoft.DotNet.Build.Tasks; namespace Microsoft.DotNet.SharedFramework.Sdk { - public class ValidateFileVersions : BuildTask + [MSBuildMultiThreadableTask] + public class ValidateFileVersions : Microsoft.Build.Utilities.Task, IMultiThreadableTask { private static readonly Version ZeroVersion = new Version(0, 0, 0, 0); + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + [Required] public ITaskItem[] Files { get; set; } @@ -97,8 +100,8 @@ FileVersionData GetFileVersionData(ITaskItem file) { return new FileVersionData() { - AssemblyVersion = FileUtilities.GetAssemblyName(filePath)?.Version, - FileVersion = FileUtilities.GetFileVersion(filePath), + AssemblyVersion = FileUtilities.GetAssemblyName(TaskEnvironment.GetAbsolutePath(filePath))?.Version, + FileVersion = FileUtilities.GetFileVersion(TaskEnvironment.GetAbsolutePath(filePath)), File = file }; } diff --git a/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs b/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs index 1765b2e21d3..82c27839b80 100644 --- a/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs +++ b/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs @@ -8,12 +8,25 @@ using Microsoft.Build.Framework; using Microsoft.SignCheck; using Microsoft.SignCheck.Logging; -using BuildTask = Microsoft.Build.Utilities.Task; namespace SignCheckTask { - public class SignCheckTask : BuildTask + // TODO: Not opted into multithreading. SignCheckRunner builds a SignatureVerificationManager whose + // static _fileVerifiers dictionary is populated by every constructor via AddFileVerifier, so + // concurrent or repeated instances race and can throw on duplicate keys. 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 SignCheckTask : 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 bool EnableJarSignatureVerification { get; set; } public bool EnableXmlSignatureVerification { get; set; } @@ -74,7 +87,7 @@ private bool ExecuteImpl() List inputFiles = new List(); if (InputFiles != null) { - ArtifactFolder = ArtifactFolder ?? Environment.CurrentDirectory; + ArtifactFolder = ArtifactFolder ?? TaskEnvironment.ProjectDirectory; SearchOption fileSearchOptions = Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; foreach (var checkFile in InputFiles.Select(s => s.ItemSpec).ToArray()) @@ -85,7 +98,7 @@ private bool ExecuteImpl() } else { - var matchedFiles = Directory.GetFiles(ArtifactFolder, checkFile, fileSearchOptions); + var matchedFiles = Directory.GetFiles(TaskEnvironment.GetAbsolutePath(ArtifactFolder), checkFile, fileSearchOptions); if (matchedFiles.Length == 1) { diff --git a/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs b/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs index 7669d9f4245..6718691b035 100644 --- a/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs +++ b/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Build.Framework; -using BuildTask = Microsoft.Build.Utilities.Task; using System; using System.Collections.Generic; using System.IO; @@ -12,8 +11,23 @@ namespace Microsoft.DotNet.SignTool { - public class SignToolTask : BuildTask + // TODO: https://github.com/dotnet/arcade/issues/17378 - this task is not yet annotated with + // [MSBuildMultiThreadableTask] because signing reads and writes files through a deep helper + // chain (BatchSignUtil, ZipData, Configuration, VerifySignatures) that still resolves paths + // against the process-wide current directory. Until those helpers take AbsolutePath, MSBuild + // keeps routing this task through the out-of-proc TaskHost in multi-threaded mode. + // + // 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 SignToolTask : 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; + /// /// Perform validation but do not actually send signing request to the server. /// @@ -163,7 +177,11 @@ public class SignToolTask : BuildTask // This property can be removed if https://github.com/dotnet/arcade/issues/6747 is implemented internal BatchSignInput ParsedSigningInput { get; private set; } + // MSBuildTask0005 is suppressed while this task is still routed through the TaskHost. + // See the TODO on the class declaration and https://github.com/dotnet/arcade/issues/17378. + #pragma warning disable MSBuildTask0005 public override bool Execute() + #pragma warning restore MSBuildTask0005 { try { @@ -193,7 +211,7 @@ public void ExecuteImpl() if (!DryRun) { - if (!File.Exists(DotNetPath)) + if (string.IsNullOrEmpty(DotNetPath) || !File.Exists(TaskEnvironment.GetAbsolutePath(DotNetPath))) { Log.LogError($"DotNet was not found at this path: '{DotNetPath}'."); return; @@ -209,11 +227,11 @@ public void ExecuteImpl() Log.LogError($"PkgToolPath ('{PkgToolPath}') does not exist & is required for unpacking, repacking, and notarizing .pkg files and .app bundles on MacOS."); } } - if(!string.IsNullOrEmpty(Wix3ToolsPath) && !Directory.Exists(Wix3ToolsPath)) + if(!string.IsNullOrEmpty(Wix3ToolsPath) && !Directory.Exists(TaskEnvironment.GetAbsolutePath(Wix3ToolsPath))) { Log.LogError($"Wix3ToolsPath ('{Wix3ToolsPath}') does not exist."); } - if(!string.IsNullOrEmpty(WixToolsPath) && !Directory.Exists(WixToolsPath)) + if(!string.IsNullOrEmpty(WixToolsPath) && !Directory.Exists(TaskEnvironment.GetAbsolutePath(WixToolsPath))) { Log.LogError($"WixToolsPath ('{WixToolsPath}') does not exist."); } @@ -374,7 +392,7 @@ private string GetEnclosingDirectoryOfItemsToSign() continue; } - var directoryParts = Path.GetFullPath(Path.GetDirectoryName(itemToSign.ItemSpec)).Split(separators); + var directoryParts = TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(itemToSign.ItemSpec)).Value.Split(separators); if (result == null) { result = directoryParts; diff --git a/src/Microsoft.DotNet.SourceBuild/tasks/src/ReadNuGetPackageInfos.cs b/src/Microsoft.DotNet.SourceBuild/tasks/src/ReadNuGetPackageInfos.cs index bcb95bf3191..ce4981117b0 100644 --- a/src/Microsoft.DotNet.SourceBuild/tasks/src/ReadNuGetPackageInfos.cs +++ b/src/Microsoft.DotNet.SourceBuild/tasks/src/ReadNuGetPackageInfos.cs @@ -10,8 +10,12 @@ namespace Microsoft.DotNet.SourceBuild.Tasks { - public class ReadNuGetPackageInfos : Microsoft.Build.Utilities.Task + [MSBuildMultiThreadableTask] + public class ReadNuGetPackageInfos : 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] public string[] PackagePaths { get; set; } @@ -28,7 +32,8 @@ public override bool Execute() PackageInfoItems = PackagePaths .Select(p => { - PackageIdentity identity = ReadIdentity(p); + // Read through the resolved path, but keep the original spec as the item identity. + PackageIdentity identity = ReadIdentity(TaskEnvironment.GetAbsolutePath(p)); return new TaskItem( p, new Dictionary diff --git a/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs b/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs index caf7252a163..7973cf4f9b1 100644 --- a/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs +++ b/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs @@ -17,8 +17,12 @@ namespace Microsoft.DotNet.SourceBuild.Tasks.UsageReport { - public class WritePackageUsageData : Microsoft.Build.Utilities.Task + [MSBuildMultiThreadableTask] + public class WritePackageUsageData : 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[] RestoredPackageFiles { get; set; } public string[] TarballPrebuiltPackageFiles { get; set; } public string[] ReferencePackageFiles { get; set; } @@ -86,8 +90,10 @@ public override bool Execute() DateTime startTime = DateTime.Now; Log.LogMessage(MessageImportance.High, "Writing package usage data..."); + // Compare resolved paths on both sides; GetPathRelativeToRoot below resolves too, so a + // raw comparison here would disagree with it whenever RootDir is relative. string[] projectDirectoriesOutsideRoot = ProjectDirectories.NullAsEmpty() - .Where(dir => !dir.StartsWith(RootDir, StringComparison.Ordinal)) + .Where(dir => !TaskEnvironment.GetAbsolutePath(dir).Value.StartsWith(AbsoluteRootDir, StringComparison.Ordinal)) .ToArray(); if (projectDirectoriesOutsideRoot.Any()) @@ -107,22 +113,22 @@ public override bool Execute() Log.LogMessage(MessageImportance.Low, "Reading package identities..."); PackageIdentity[] restored = RestoredPackageFiles.NullAsEmpty() - .Select(ReadNuGetPackageInfos.ReadIdentity) + .Select(ReadIdentityFromResolvedPath) .Distinct() .ToArray(); PackageIdentity[] tarballPrebuilt = TarballPrebuiltPackageFiles.NullAsEmpty() - .Select(ReadNuGetPackageInfos.ReadIdentity) + .Select(ReadIdentityFromResolvedPath) .Distinct() .ToArray(); PackageIdentity[] referencePackages = ReferencePackageFiles.NullAsEmpty() - .Select(ReadNuGetPackageInfos.ReadIdentity) + .Select(ReadIdentityFromResolvedPath) .Distinct() .ToArray(); PackageIdentity[] sourceBuilt = SourceBuiltPackageFiles.NullAsEmpty() - .Select(ReadNuGetPackageInfos.ReadIdentity) + .Select(ReadIdentityFromResolvedPath) .Distinct() .ToArray(); @@ -138,7 +144,7 @@ public override bool Execute() Log.LogMessage(MessageImportance.Low, "Finding project.assets.json files..."); string[] assetFiles = Directory - .GetFiles(RootDir, "project.assets.json", SearchOption.AllDirectories) + .GetFiles(TaskEnvironment.GetAbsolutePath(AbsoluteRootDir), "project.assets.json", SearchOption.AllDirectories) .Select(GetPathRelativeToRoot) .Except(IgnoredProjectAssetsJsonFiles.NullAsEmpty().Select(GetPathRelativeToRoot)) .ToArray(); @@ -147,11 +153,11 @@ public override bool Execute() { Log.LogMessage(MessageImportance.Low, "Archiving project.assets.json files..."); - Directory.CreateDirectory(Path.GetDirectoryName(ProjectAssetsJsonArchiveFile)); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(ProjectAssetsJsonArchiveFile))); using (var projectAssetArchive = new ZipArchive( - File.Open( - ProjectAssetsJsonArchiveFile, +File.Open(TaskEnvironment.GetAbsolutePath( + ProjectAssetsJsonArchiveFile), FileMode.Create, FileAccess.ReadWrite), ZipArchiveMode.Create)) @@ -160,7 +166,7 @@ public override bool Execute() // ForEach later. foreach (var relativePath in assetFiles) { - using (var stream = File.OpenRead(Path.Combine(RootDir, relativePath))) + using (var stream = File.OpenRead(TaskEnvironment.GetAbsolutePath(Path.Combine(AbsoluteRootDir, relativePath)))) using (Stream entryWriter = projectAssetArchive .CreateEntry(relativePath, CompressionLevel.Optimal) .Open()) @@ -181,7 +187,7 @@ public override bool Execute() { JObject jObj; - using (var file = File.OpenRead(Path.Combine(RootDir, assetFile))) + using (var file = File.OpenRead(TaskEnvironment.GetAbsolutePath(Path.Combine(AbsoluteRootDir, assetFile)))) using (var reader = new StreamReader(file)) using (var jsonReader = new JsonTextReader(reader)) { @@ -251,8 +257,8 @@ public override bool Execute() .ToArray() }; - Directory.CreateDirectory(Path.GetDirectoryName(DataFile)); - File.WriteAllText(DataFile, data.ToXml().ToString()); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(DataFile))); + File.WriteAllText(TaskEnvironment.GetAbsolutePath(DataFile), data.ToXml().ToString()); Log.LogMessage( MessageImportance.High, @@ -261,23 +267,62 @@ public override bool Execute() return !Log.HasLoggedErrors; } + private string _absoluteRootDir; + + /// + /// resolved against the project directory. Any trailing separator is + /// preserved, because strips exactly this prefix and its + /// callers rely on the result staying relative. + /// + private string AbsoluteRootDir + { + get + { + if (_absoluteRootDir == null) + { + string resolved = TaskEnvironment.GetAbsolutePath(RootDir); + + if (EndsWithDirectorySeparator(RootDir) && !EndsWithDirectorySeparator(resolved)) + { + resolved += Path.DirectorySeparatorChar; + } + + _absoluteRootDir = resolved; + } + + return _absoluteRootDir; + } + } + + private static bool EndsWithDirectorySeparator(string path) => + !string.IsNullOrEmpty(path) && + (path[path.Length - 1] == Path.DirectorySeparatorChar || + path[path.Length - 1] == Path.AltDirectorySeparatorChar); + private string GetPathRelativeToRoot(string path) { - if (path.StartsWith(RootDir)) + // Compare against the same resolved root that was used to enumerate these paths, + // otherwise a relative RootDir never matches the absolute results. + string absolutePath = TaskEnvironment.GetAbsolutePath(path); + + if (absolutePath.StartsWith(AbsoluteRootDir)) { - return path.Substring(RootDir.Length).Replace(Path.DirectorySeparatorChar, '/'); + return absolutePath.Substring(AbsoluteRootDir.Length).Replace(Path.DirectorySeparatorChar, '/'); } throw new ArgumentException($"Path '{path}' is not within RootDir '{RootDir}'"); } - private static string[] ReadRidsFromRuntimeJson(string path) + private string[] ReadRidsFromRuntimeJson(string path) { - var root = JObject.Parse(File.ReadAllText(path)); + var root = JObject.Parse(File.ReadAllText(TaskEnvironment.GetAbsolutePath(path))); return root["runtimes"] .Values() .Select(o => o.Name) .ToArray(); } + + private PackageIdentity ReadIdentityFromResolvedPath(string nupkgFile) => + ReadNuGetPackageInfos.ReadIdentity(TaskEnvironment.GetAbsolutePath(nupkgFile)); } } diff --git a/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WriteUsageReports.cs b/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WriteUsageReports.cs index eb86e4e5b3c..8bb30b78524 100644 --- a/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WriteUsageReports.cs +++ b/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WriteUsageReports.cs @@ -12,11 +12,15 @@ namespace Microsoft.DotNet.SourceBuild.Tasks.UsageReport { - public class WriteUsageReports : Microsoft.Build.Utilities.Task + [MSBuildMultiThreadableTask] + public class WriteUsageReports : Microsoft.Build.Utilities.Task, IMultiThreadableTask { private const string SnapshotPrefix = "PackageVersions.props.pre."; private const string SnapshotSuffix = ".xml"; + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + /// /// Source usage data JSON file. /// @@ -60,7 +64,7 @@ public class WriteUsageReports : Microsoft.Build.Utilities.Task public override bool Execute() { - UsageData data = UsageData.Parse(XElement.Parse(File.ReadAllText(DataFile))); + UsageData data = UsageData.Parse(XElement.Parse(File.ReadAllText(TaskEnvironment.GetAbsolutePath(DataFile)))); IEnumerable sourceBuildRepoOutputs = GetSourceBuildRepoOutputs(); @@ -76,9 +80,9 @@ public override bool Execute() item.GetMetadata("OriginBuildName")); } - if (File.Exists(ProdConBuildManifestFile)) + if (!string.IsNullOrEmpty(ProdConBuildManifestFile) && File.Exists(TaskEnvironment.GetAbsolutePath(ProdConBuildManifestFile))) { - var xml = XElement.Parse(File.ReadAllText(ProdConBuildManifestFile)); + var xml = XElement.Parse(File.ReadAllText(TaskEnvironment.GetAbsolutePath(ProdConBuildManifestFile))); foreach (var x in xml.Descendants("Package")) { AddProdConPackage( @@ -90,9 +94,9 @@ public override bool Execute() var poisonNupkgFilenames = new HashSet(StringComparer.OrdinalIgnoreCase); - if (File.Exists(PoisonedReportFile)) + if (!string.IsNullOrEmpty(PoisonedReportFile) && File.Exists(TaskEnvironment.GetAbsolutePath(PoisonedReportFile))) { - foreach (string line in File.ReadAllLines(PoisonedReportFile)) + foreach (string line in File.ReadAllLines(TaskEnvironment.GetAbsolutePath(PoisonedReportFile))) { string[] segments = line.Split('\\'); if (segments.Length > 2 && @@ -156,10 +160,10 @@ public override bool Execute() report.Add(annotatedUsages.Select(u => u.ToXml())); - Directory.CreateDirectory(OutputDirectory); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(OutputDirectory)); File.WriteAllText( - Path.Combine(OutputDirectory, "annotated-usage.xml"), +TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, "annotated-usage.xml")), report.ToString()); return !Log.HasLoggedErrors; @@ -170,7 +174,7 @@ private RepoOutput[] GetSourceBuildRepoOutputs() var pvpSnapshotFiles = PackageVersionPropsSnapshots.NullAsEmpty() .Select(item => { - var content = File.ReadAllText(item.ItemSpec); + var content = File.ReadAllText(TaskEnvironment.GetAbsolutePath(item.ItemSpec)); return new { Path = item.ItemSpec, diff --git a/src/Microsoft.DotNet.SourceBuild/tasks/src/WriteBuildOutputProps.cs b/src/Microsoft.DotNet.SourceBuild/tasks/src/WriteBuildOutputProps.cs index 1b32c5f1112..00caa67cd01 100644 --- a/src/Microsoft.DotNet.SourceBuild/tasks/src/WriteBuildOutputProps.cs +++ b/src/Microsoft.DotNet.SourceBuild/tasks/src/WriteBuildOutputProps.cs @@ -13,12 +13,16 @@ namespace Microsoft.DotNet.SourceBuild.Tasks { - public class WriteBuildOutputProps : Microsoft.Build.Utilities.Task + [MSBuildMultiThreadableTask] + public class WriteBuildOutputProps : Microsoft.Build.Utilities.Task, IMultiThreadableTask { private static readonly Regex InvalidElementNameCharRegex = new Regex(@"(^|[^A-Za-z0-9])(?.)"); public const string CreationTimePropertyName = "BuildOutputPropsCreationTime"; + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + [Required] public ITaskItem[] NuGetPackages { get; set; } @@ -65,16 +69,18 @@ public override bool Execute() .ToArray(); var additionalAssets = (AdditionalAssetDirs ?? new string[0]) - .Where(Directory.Exists) + .Where(dir => !string.IsNullOrEmpty(dir)) + .Select(dir => TaskEnvironment.GetAbsolutePath(dir)) + .Where(dir => Directory.Exists(dir)) .Where(dir => Directory.GetDirectories(dir).Count() > 0) .Select(dir => new { Name = new DirectoryInfo(dir).Name + "Version", - Version = new DirectoryInfo(Directory.EnumerateDirectories(dir).OrderBy(s => s).Last()).Name + Version = new DirectoryInfo(TaskEnvironment.GetAbsolutePath(Directory.EnumerateDirectories(dir).OrderBy(s => s).Last())).Name }).ToArray(); - Directory.CreateDirectory(Path.GetDirectoryName(OutputPath)); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(OutputPath))); - using (var outStream = File.Open(OutputPath, FileMode.Create)) + using (var outStream = File.Open(TaskEnvironment.GetAbsolutePath(OutputPath), FileMode.Create)) using (var sw = new StreamWriter(outStream, new UTF8Encoding(false))) { sw.WriteLine(@""); diff --git a/src/Microsoft.DotNet.SwaggerGenerator/Microsoft.DotNet.SwaggerGenerator.MSBuild/GenerateSwaggerCode.cs b/src/Microsoft.DotNet.SwaggerGenerator/Microsoft.DotNet.SwaggerGenerator.MSBuild/GenerateSwaggerCode.cs index fe67d428640..8bb4503c1e1 100644 --- a/src/Microsoft.DotNet.SwaggerGenerator/Microsoft.DotNet.SwaggerGenerator.MSBuild/GenerateSwaggerCode.cs +++ b/src/Microsoft.DotNet.SwaggerGenerator/Microsoft.DotNet.SwaggerGenerator.MSBuild/GenerateSwaggerCode.cs @@ -16,8 +16,12 @@ namespace Microsoft.DotNet.SwaggerGenerator.MSBuild { - public class GenerateSwaggerCode : Microsoft.Build.Utilities.Task + [MSBuildMultiThreadableTask] + public class GenerateSwaggerCode : 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] public string SwaggerDocumentUri { get; set; } @@ -76,7 +80,7 @@ private async System.Threading.Tasks.Task ExecuteAsync() List code = codeFactory.GenerateCode(model, options); Log.LogMessage(MessageImportance.High, $"Generating {SwaggerDocumentUri} -> {OutputDirectory}"); - var outputDirectory = new DirectoryInfo(OutputDirectory); + var outputDirectory = new DirectoryInfo(TaskEnvironment.GetAbsolutePath(OutputDirectory)); outputDirectory.Create(); var generatedFiles = new List(); diff --git a/src/Microsoft.DotNet.XliffTasks/Model/Document.cs b/src/Microsoft.DotNet.XliffTasks/Model/Document.cs index ff73f0e0c34..da08d27ae42 100644 --- a/src/Microsoft.DotNet.XliffTasks/Model/Document.cs +++ b/src/Microsoft.DotNet.XliffTasks/Model/Document.cs @@ -26,7 +26,7 @@ internal abstract class Document /// /// Loads (or reloads) the document content from the given file path. /// - public void Load(string path) + public void Load(Microsoft.Build.Framework.AbsolutePath path) { using FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read); Load(stream); @@ -56,7 +56,7 @@ public void Load(Stream stream) /// /// Saves the document's content to the given file path. /// - public void Save(string path) + public void Save(Microsoft.Build.Framework.AbsolutePath path) { //On Windows: // Readers will prevent the file from being overwritten due to FileShare.Read. @@ -67,7 +67,7 @@ public void Save(string path) // reading is happening, each reader will see file before or after overwrite, not in between EnsureContent(); - string tempPath = Path.Combine(Path.GetDirectoryName(path), Path.GetRandomFileName()); + Microsoft.Build.Framework.AbsolutePath tempPath = new(Path.Combine(Path.GetDirectoryName(path), Path.GetRandomFileName())); using (FileStream stream = File.Open(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) { diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/EnsureAllResourcesTranslated.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/EnsureAllResourcesTranslated.cs index 181c8f013df..81a969fb11a 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/EnsureAllResourcesTranslated.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/EnsureAllResourcesTranslated.cs @@ -9,6 +9,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class EnsureAllResourcesTranslated : XlfTask { [Required] @@ -39,7 +40,7 @@ protected override void ExecuteCore() try { - xlfDocument = XlfTask.LoadXlfDocument(xlfPath); + xlfDocument = XlfTask.LoadXlfDocument(TaskEnvironment.GetAbsolutePath(xlfPath)); } catch (FileNotFoundException) { diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/GatherTranslatedSource.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/GatherTranslatedSource.cs index 67d3a1543d9..2d71e120a6e 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/GatherTranslatedSource.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/GatherTranslatedSource.cs @@ -8,6 +8,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class GatherTranslatedSource : XlfTask { [Required] @@ -103,13 +104,13 @@ private static void AdjustLogicalName(ITaskItem xlf, ITaskItem output, string la } } - private static void AdjustDependentUpon(ITaskItem xlf, ITaskItem output) + private void AdjustDependentUpon(ITaskItem xlf, ITaskItem output) { string dependentUpon = xlf.GetMetadata(MetadataKey.DependentUpon); if (!string.IsNullOrEmpty(dependentUpon)) { string sourceDirectory = Path.GetDirectoryName(xlf.GetMetadataOrThrow(MetadataKey.XlfSource)); - dependentUpon = Path.GetFullPath(Path.Combine(sourceDirectory, dependentUpon)); + dependentUpon = TaskEnvironment.GetAbsolutePath(Path.Combine(sourceDirectory, dependentUpon)); output.SetMetadata(MetadataKey.DependentUpon, dependentUpon); } } diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/GatherXlf.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/GatherXlf.cs index 5c0665792ad..e651c13e66f 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/GatherXlf.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/GatherXlf.cs @@ -9,6 +9,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class GatherXlf : XlfTask { [Required] diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/SortXlf.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/SortXlf.cs index 825f6071ae0..d88242cb635 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/SortXlf.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/SortXlf.cs @@ -7,6 +7,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class SortXlf : XlfTask { [Required] @@ -28,7 +29,7 @@ protected override void ExecuteCore() try { - xlfDocument = XlfTask.LoadXlfDocument(xlfPath); + xlfDocument = XlfTask.LoadXlfDocument(TaskEnvironment.GetAbsolutePath(xlfPath)); } catch (FileNotFoundException) { @@ -42,8 +43,8 @@ protected override void ExecuteCore() continue; // no changes } - Directory.CreateDirectory(Path.GetDirectoryName(xlfPath)); - xlfDocument.Save(xlfPath); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(xlfPath))); + xlfDocument.Save(TaskEnvironment.GetAbsolutePath(xlfPath)); } } } diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/TransformTemplates.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/TransformTemplates.cs index 37d1f9c611e..0b08d5bb7c2 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/TransformTemplates.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/TransformTemplates.cs @@ -12,6 +12,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class TransformTemplates : XlfTask { [Required] @@ -75,15 +76,15 @@ private ITaskItem TransformTemplate(ITaskItem template, string language, IDictio string localizedTemplateDirectory = transformingDefaultTemplate ? Path.Combine(TranslatedOutputDirectory, $"{templateName}.default.1033") : Path.Combine(TranslatedOutputDirectory, $"{templateName}.{language}"); - Directory.CreateDirectory(localizedTemplateDirectory); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(localizedTemplateDirectory)); string cultureSpecificTemplateFile = Path.Combine(localizedTemplateDirectory, Path.GetFileName(template.ItemSpec)); - File.Copy(templatePath, cultureSpecificTemplateFile, overwrite: true); + File.Copy(TaskEnvironment.GetAbsolutePath(templatePath), TaskEnvironment.GetAbsolutePath(cultureSpecificTemplateFile), overwrite: true); // copy the template project files foreach (XElement projectNode in templateXml.Descendants().Where(d => d.Name.LocalName == "Project")) { string projectFileFullPath = Path.Combine(templateDirectory, projectNode.Attribute("File").Value); - File.Copy(projectFileFullPath, Path.Combine(localizedTemplateDirectory, Path.GetFileName(projectNode.Attribute("File").Value)), overwrite: true); + File.Copy(TaskEnvironment.GetAbsolutePath(projectFileFullPath), TaskEnvironment.GetAbsolutePath(Path.Combine(localizedTemplateDirectory, Path.GetFileName(projectNode.Attribute("File").Value))), overwrite: true); } // copy the template project items @@ -95,10 +96,10 @@ private ITaskItem TransformTemplate(ITaskItem template, string language, IDictio { // if not localizing anything, simply strip out the translation markers UnstructuredDocument document = new(); - document.Load(templateItemFullPath); + document.Load(TaskEnvironment.GetAbsolutePath(templateItemFullPath)); Dictionary defaultTranslation = document.Nodes.ToDictionary(node => node.Id, node => node.Source); document.Translate(defaultTranslation); - document.Save(templateItemDestinationPath); + document.Save(TaskEnvironment.GetAbsolutePath(templateItemDestinationPath)); } else { @@ -111,12 +112,12 @@ private ITaskItem TransformTemplate(ITaskItem template, string language, IDictio ".", language, Path.GetExtension(unstructuredResource.ItemSpec)); - File.Copy(Path.Combine(TranslatedOutputDirectory, localizedFileName), templateItemDestinationPath, overwrite: true); + File.Copy(TaskEnvironment.GetAbsolutePath(Path.Combine(TranslatedOutputDirectory, localizedFileName)), TaskEnvironment.GetAbsolutePath(templateItemDestinationPath), overwrite: true); } else { // copy the original unaltered file - File.Copy(templateItemFullPath, templateItemDestinationPath, overwrite: true); + File.Copy(TaskEnvironment.GetAbsolutePath(templateItemFullPath), TaskEnvironment.GetAbsolutePath(templateItemDestinationPath), overwrite: true); } } } diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs index 1e9aeca0ebc..b0ca5cf824b 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/TranslateSource.cs @@ -8,6 +8,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class TranslateSource : XlfTask { [Required] @@ -20,8 +21,8 @@ protected override void ExecuteCore() string language = XlfFile.GetMetadataOrThrow(MetadataKey.XlfLanguage); string translatedFullPath = XlfFile.GetMetadataOrThrow(MetadataKey.XlfTranslatedFullPath); - TranslatableDocument sourceDocument = XlfTask.LoadSourceDocument(sourcePath, XlfFile.GetMetadata(MetadataKey.XlfSourceFormat)); - XlfDocument xlfDocument = XlfTask.LoadXlfDocument(XlfFile.ItemSpec); + TranslatableDocument sourceDocument = XlfTask.LoadSourceDocument(TaskEnvironment.GetAbsolutePath(sourcePath), XlfFile.GetMetadata(MetadataKey.XlfSourceFormat)); + XlfDocument xlfDocument = XlfTask.LoadXlfDocument(TaskEnvironment.GetAbsolutePath(XlfFile.ItemSpec)); bool validationFailed = false; xlfDocument.Validate(validationError => @@ -36,10 +37,10 @@ protected override void ExecuteCore() sourceDocument.Translate(translations); - Directory.CreateDirectory(Path.GetDirectoryName(translatedFullPath)); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(translatedFullPath))); - sourceDocument.RewriteRelativePathsToAbsolute(Path.GetFullPath(sourcePath)); - sourceDocument.Save(translatedFullPath); + sourceDocument.RewriteRelativePathsToAbsolute(TaskEnvironment.GetAbsolutePath(sourcePath)); + sourceDocument.Save(TaskEnvironment.GetAbsolutePath(translatedFullPath)); } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/UpdateXlf.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/UpdateXlf.cs index 340c113bfca..f5630202648 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/UpdateXlf.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/UpdateXlf.cs @@ -7,6 +7,7 @@ namespace XliffTasks.Tasks { + [MSBuildMultiThreadableTask] public sealed class UpdateXlf : XlfTask { [Required] @@ -19,7 +20,7 @@ public sealed class UpdateXlf : XlfTask public bool AllowModification { get; set; } private const string HowToUpdate = - "Run `msbuild /t:UpdateXlf` to update .xlf files or set UpdateXlfOnBuild=true" + "Run `dotnet build /t:UpdateXlf` to update .xlf files or set UpdateXlfOnBuild=true" + " to update them on every build, but note that it is strongly discouraged to set" + " UpdateXlfOnBuild=true in official/CI build environments as they should not" + " modify source code during the build."; @@ -31,19 +32,21 @@ protected override void ExecuteCore() string sourcePath = item.ItemSpec; string sourceDocumentPath = item.GetMetadataOrDefault(MetadataKey.SourceDocumentPath, item.ItemSpec); string sourceFormat = item.GetMetadataOrThrow(MetadataKey.XlfSourceFormat); - TranslatableDocument sourceDocument = XlfTask.LoadSourceDocument(sourcePath, sourceFormat); + TranslatableDocument sourceDocument = XlfTask.LoadSourceDocument(TaskEnvironment.GetAbsolutePath(sourcePath), sourceFormat); string sourceDocumentId = XlfTask.GetSourceDocumentId(sourcePath); foreach (string language in Languages) { string xlfPath = XlfTask.GetXlfPath(sourceDocumentPath, language); + Microsoft.Build.Framework.AbsolutePath absoluteXlfPath = TaskEnvironment.GetAbsolutePath(xlfPath); XlfDocument xlfDocument; try { - xlfDocument = XlfTask.LoadXlfDocument(xlfPath, language, createIfNonExistent: AllowModification); + xlfDocument = XlfTask.LoadXlfDocument(absoluteXlfPath, language, createIfNonExistent: AllowModification); } - catch (FileNotFoundException fileNotFoundEx) when (fileNotFoundEx.FileName == xlfPath) + // LoadXlfDocument reports the absolute path it was given, so compare against that. + catch (FileNotFoundException fileNotFoundEx) when (fileNotFoundEx.FileName == absoluteXlfPath.Value) { Release.Assert(!AllowModification); throw new BuildErrorException($"'{xlfPath}' for '{sourcePath}' does not exist. {HowToUpdate}"); @@ -68,8 +71,8 @@ protected override void ExecuteCore() throw new BuildErrorException($"'{xlfPath}' is out-of-date with '{sourcePath}'. {HowToUpdate}"); } - Directory.CreateDirectory(Path.GetDirectoryName(xlfPath)); - xlfDocument.Save(xlfPath); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(absoluteXlfPath))); + xlfDocument.Save(absoluteXlfPath); } } } diff --git a/src/Microsoft.DotNet.XliffTasks/Tasks/XlfTask.cs b/src/Microsoft.DotNet.XliffTasks/Tasks/XlfTask.cs index dd739fae456..e4e5293b95c 100644 --- a/src/Microsoft.DotNet.XliffTasks/Tasks/XlfTask.cs +++ b/src/Microsoft.DotNet.XliffTasks/Tasks/XlfTask.cs @@ -5,10 +5,11 @@ using System; using System.IO; using XliffTasks.Model; +using Microsoft.Build.Framework; namespace XliffTasks.Tasks { - public abstract class XlfTask : Task + public abstract class XlfTask : Task, IMultiThreadableTask { /// /// The language of the neutral (language-agnostic) .xlf file, which is handed to the @@ -24,6 +25,9 @@ internal XlfTask() { } + /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds. + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + public sealed override bool Execute() { try @@ -40,7 +44,7 @@ public sealed override bool Execute() protected abstract void ExecuteCore(); - internal static TranslatableDocument LoadSourceDocument(string path, string format) + internal static TranslatableDocument LoadSourceDocument(Microsoft.Build.Framework.AbsolutePath path, string format) { TranslatableDocument document; @@ -76,7 +80,7 @@ internal static TranslatableDocument LoadSourceDocument(string path, string form return document; } - internal static XlfDocument LoadXlfDocument(string path, string language = null, bool createIfNonExistent = false) + internal static XlfDocument LoadXlfDocument(Microsoft.Build.Framework.AbsolutePath path, string language = null, bool createIfNonExistent = false) { XlfDocument document = new();