From 244ce414c490438858ef37417d01381c729d6e3d Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Sun, 23 Aug 2026 09:48:13 +0200
Subject: [PATCH 1/9] Make MSBuild tasks safe for multi-threaded execution
MSBuild 18.x can execute tasks on multiple threads inside a single node
(`-mt`), but only for tasks that opt in via [MSBuildMultiThreadableTask].
Everything else is routed to an out-of-proc sidecar TaskHost, which is far
more expensive than the in-proc path.
Measuring a runtime inner-repo build in the VMR with `-mt -nodeReuse:true`
against a baseline showed only a 3.6% wall-clock improvement, because none of
Arcade's tasks were annotated: TaskHost task invocations went from 849 to
14,125 and total task time went *up* by 98 seconds, cancelling out most of the
gain from threading.
This annotates 133 of Arcade's 136 concrete tasks and makes the supporting
code multi-threading safe:
- Update the MSBuild package dependencies from 17.12.50 to 18.8.2, which is the
first version exposing both [MSBuildMultiThreadableTask]/IMultiThreadableTask
and ToolTask.TaskEnvironment.
- Reference Microsoft.Build.TaskAuthoring.Analyzer from eng/BuildTask.targets so
new violations are caught at build time. eng/MultiThreadableTaskAnalyzer.globalconfig
scopes it to tasks that have already opted in, and disables the API-shape
suggestions (MSBuildTask0006-0008, 0011) which would be binary-breaking for
task parameters set from targets across the ecosystem.
- Replace ambient process state with the injected TaskEnvironment: relative
paths now resolve against the project directory instead of the process-wide
current directory, environment variables are read per-project, and child
processes are started from TaskEnvironment.GetProcessStartInfo().
- Thread AbsolutePath through the shared path helpers in Packaging, GenFacades,
Feed, NuGetRepack, SharedFramework.Sdk, PackageTesting and XliffTasks.
- Fix a real data race: TargetFrameworkResolver.CreateOrGet used an
unsynchronized static Dictionary cache. It is now a ConcurrentDictionary.
- Stop sharing a static Newtonsoft JsonSerializer instance in PackageIndex.
- Delete the internal BuildTask/ILog base class. It only wrapped
Microsoft.Build.Utilities.Task and TaskLoggingHelper, and standing between
tasks and the MSBuild base class meant every derived task needed its own
TaskEnvironment plumbing. Tasks now derive from Microsoft.Build.Utilities.Task
directly and use Log/TaskLoggingHelper.
SignToolTask, CreateVisualStudioWorkload and CreateVisualStudioWorkloadSet are
deliberately left unannotated. Their helper chains (BatchSignUtil, ZipData,
Configuration, VerifySignatures, WorkloadPackageBase, MsiBase, SwixProject, ...)
still resolve paths against the process-wide current directory, and converting
them is a much larger change. Each carries a TODO pointing at the tracking
issue. Both run once per build, so keeping them TaskHost-routed costs
effectively nothing.
Contributes to https://github.com/dotnet/arcade/issues/17378
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
Directory.Packages.props | 1 +
eng/BuildTask.targets | 13 ++
eng/MultiThreadableTaskAnalyzer.globalconfig | 17 ++
eng/Version.Details.props | 10 +-
eng/Version.Details.xml | 20 ++-
src/Common/Internal/BuildTask.cs | 153 ------------------
.../src/CalculateAssemblyAndFileVersions.cs | 1 +
.../src/CheckRequiredDotNetVersion.cs | 24 ++-
.../src/CompareVersions.cs | 1 +
.../src/DownloadFile.cs | 16 +-
.../src/ExtractNgenMethodList.cs | 14 +-
.../src/GenerateChecksums.cs | 12 +-
.../src/GenerateResxSource.cs | 10 +-
...erateSourcePackageSourceLinkTargetsFile.cs | 10 +-
.../src/GetAssemblyFullName.cs | 1 +
.../src/GetLicenseFilePath.cs | 8 +-
.../src/GroupItemsBy.cs | 1 +
.../src/InstallDotNetCore.cs | 47 +++---
.../src/LocateDotNet.cs | 30 ++--
.../src/SaveItems.cs | 13 +-
.../src/SetCorFlags.cs | 8 +-
.../src/SingleError.cs | 1 +
src/Microsoft.DotNet.Arcade.Sdk/src/Unsign.cs | 8 +-
.../src/ValidateLicense.cs | 10 +-
.../src/AzureStorageAssetPublisher.cs | 2 +-
.../src/AzureStorageExtensions.cs | 4 +-
.../src/BlobFeedAction.cs | 2 +-
.../src/ConfigureInputFeed.cs | 10 +-
.../src/CreateAzureDevOpsFeed.cs | 1 +
.../src/LaunchDebugger.cs | 2 +
.../src/PublishArtifactsInManifest.cs | 15 +-
.../src/PublishArtifactsInManifestV3.cs | 3 +
.../src/PublishArtifactsInManifestV4.cs | 3 +
.../src/PublishBuildToMaestro.cs | 25 +--
.../src/PublishSignedAssets.cs | 15 +-
.../src/PushToBuildStorage.cs | 1 +
.../src/common/AzureStorageUtils.cs | 2 +-
.../common/CreateAzureContainerIfNotExists.cs | 2 +
.../src/common/CreateNewAzureContainer.cs | 2 +
.../src/common/UploadToAzure.cs | 12 +-
.../CatalogTests.cs | 3 +-
.../CatalogBuilder.cs | 5 +-
.../CatalogEntry.cs | 3 +-
.../GenerateFileCatalog.cs | 19 ++-
...osoft.DotNet.Build.Tasks.Installers.csproj | 1 -
.../src/CreateChangelogFile.cs | 8 +-
.../src/CreateControlFile.cs | 8 +-
.../src/CreateDebPackage.cs | 12 +-
.../src/CreateLightCommandPackageDrop.cs | 11 +-
.../src/CreateMD5SumsFile.cs | 10 +-
.../src/CreateRpmPackage.cs | 16 +-
.../src/CreateWixBuildWixpack.cs | 92 ++++++-----
.../src/CreateWixCommandPackageDropBase.cs | 4 +-
.../src/ExecWithRetries.cs | 5 +-
.../src/GenerateCurrentVersion.cs | 3 +-
.../src/GenerateGuidFromName.cs | 3 +-
.../src/GenerateMacOSDistributionFile.cs | 10 +-
.../src/GenerateMsiVersion.cs | 3 +-
.../src/StabilizeWixFileId.cs | 10 +-
.../src/ApplyBaseLine.cs | 8 +-
.../src/ApplyMetaPackages.cs | 8 +-
.../src/ApplyPreReleaseSuffix.cs | 8 +-
.../src/CreateTrimDependencyGroups.cs | 8 +-
.../src/FilterUnknownPackages.cs | 8 +-
.../src/GenerateNuSpec.cs | 18 ++-
.../src/GeneratePackageReport.cs | 10 +-
.../src/GenerateRuntimeDependencies.cs | 16 +-
.../GetApplicableAssetsFromPackageReports.cs | 8 +-
.../src/GetApplicableAssetsFromPackages.cs | 8 +-
.../src/GetAssemblyReferences.cs | 10 +-
.../src/GetInboxFrameworks.cs | 10 +-
.../src/GetLastStablePackage.cs | 8 +-
.../src/GetLayoutFiles.cs | 16 +-
.../src/GetMinimumNETStandard.cs | 3 +-
.../src/GetPackageDescription.cs | 24 ++-
.../src/GetPackageDestination.cs | 3 +-
.../src/GetPackageFromModule.cs | 8 +-
.../src/GetPackageVersion.cs | 3 +-
.../src/GetRuntimeJsonValues.cs | 10 +-
.../src/GetRuntimeTargets.cs | 10 +-
.../GetSupportedPackagesFromPackageReports.cs | 8 +-
.../src/HarvestPackage.cs | 40 +++--
...rosoft.DotNet.Build.Tasks.Packaging.csproj | 1 -
.../src/NuGetAssetResolver.cs | 6 +-
.../src/NuGetPack.cs | 26 +--
.../src/NuGetUtility.cs | 11 +-
.../src/PackageIndex.cs | 35 ++--
.../src/PackageItem.cs | 6 +-
.../src/PackageMetadata.cs | 3 +-
.../src/PackageReport.cs | 4 +-
.../src/PromoteDependencies.cs | 12 +-
.../src/SplitDependenciesBySupport.cs | 3 +-
.../src/SplitReferences.cs | 8 +-
.../src/UpdatePackageIndex.cs | 24 +--
.../src/ValidateFrameworkPackage.cs | 5 +-
...alidateHarvestVersionIsLatestForRelease.cs | 16 +-
.../src/ValidatePackage.cs | 4 +-
.../src/ValidationTask.cs | 9 +-
.../src/VersionUtility.cs | 3 +-
.../tests/CreateTrimDependencyGroupsTests.cs | 7 +-
.../tests/Log.cs | 6 +-
.../tests/PackageIndexTests.cs | 5 +-
.../tests/RuntimeGraphTests.cs | 3 +-
.../tests/TestBuildEngine.cs | 7 +-
...teHarvestVersionIsLatestForReleaseTests.cs | 2 +-
.../src/ChooseBestP2PTargetFrameworkTask.cs | 5 +-
.../src/ChooseBestTargetFrameworksTask.cs | 3 +-
....DotNet.Build.Tasks.TargetFramework.csproj | 4 -
.../src/TargetFrameworkResolver.cs | 35 ++--
.../src/GenerateFileFromTemplate.cs | 20 ++-
.../OptProf/FindLatestDrop.cs | 8 +-
.../OptProf/GenerateTrainingInputFiles.cs | 22 +--
.../OptProf/GenerateTrainingPropsFile.cs | 10 +-
.../GetRunSettingsSessionConfiguration.cs | 10 +-
.../Vsix/FinalizeInsertionVsixFile.cs | 12 +-
.../Vsix/GetPkgDefAssemblyDependencyGuid.cs | 1 +
.../src/CreateVisualStudioWorkload.wix.cs | 16 +-
.../src/CreateVisualStudioWorkloadSet.wix.cs | 7 +
...rosoft.DotNet.Build.Tasks.Workloads.csproj | 1 -
.../src/Wix/HarvesterToolTask.cs | 1 +
.../src/Wix/WixToolTask.cs | 1 +
.../src/Wix/WixToolTaskBase.cs | 2 +-
.../Microsoft.DotNet.CMake.Sdk.csproj | 4 -
.../src/CreateCMakeFileApiQuery.cs | 13 +-
.../src/GetCMakeArtifactsFromFileApi.cs | 41 ++---
.../src/CreateAkaMSLinks.cs | 1 +
.../src/DeleteAkaMSLinks.cs | 1 +
src/Microsoft.DotNet.GenAPI/GenAPITask.cs | 69 ++++++--
.../Microsoft.DotNet.GenAPI.csproj | 4 -
.../ClearAssemblyReferenceVersions.cs | 8 +-
.../GenPartialFacadeSource.cs | 17 +-
.../GenPartialFacadeSourceGenerator.cs | 24 +--
.../Microsoft.DotNet.GenFacades.csproj | 4 -
.../NotSupportedAssemblyGenerator.cs | 21 ++-
.../RoslynBuildTask.cs | 2 +-
.../SourceGenerator.cs | 10 +-
src/Microsoft.DotNet.GenFacades/TypeParser.cs | 8 +-
.../Sdk/AzureDevOpsTask.cs | 13 +-
.../Sdk/CancelHelixJob.cs | 1 +
.../Sdk/CheckAzurePipelinesTestResults.cs | 1 +
.../Sdk/CheckHelixJobStatus.cs | 1 +
.../CreateFailedTestsForFailedWorkItems.cs | 1 +
.../Sdk/CreateMTPWorkItems.cs | 1 +
.../Sdk/CreateXHarnessAndroidWorkItems.cs | 1 +
.../Sdk/CreateXHarnessAppleWorkItems.cs | 1 +
.../Sdk/CreateXUnitWorkItems.cs | 1 +
.../Sdk/DownloadFromResultsContainer.cs | 10 +-
.../Sdk/FindDotNetCliPackage.cs | 3 +-
.../Sdk/GetHelixWorkItems.cs | 1 +
.../Sdk/InstallDotNetTool.cs | 1 +
.../Sdk/SendHelixJob.cs | 12 +-
.../Sdk/StartAzurePipelinesTestRun.cs | 1 +
.../Sdk/StopAzurePipelinesTestRun.cs | 1 +
.../Sdk/WaitForHelixJobCompletion.cs | 1 +
.../tasks/src/NuGetVersionUpdater.cs | 30 ++--
.../tasks/src/ReplacePackageParts.cs | 22 ++-
.../tasks/src/UpdatePackageVersionTask.cs | 15 +-
.../tests/VersionUpdaterTests.cs | 19 +--
...CompatibilePackageTargetFrameworksTests.cs | 8 +-
.../GetCompatiblePackageTargetFrameworks.cs | 12 +-
.../Microsoft.DotNet.PackageTesting.csproj | 4 -
.../VerifyClosure.cs | 17 +-
.../VerifyTypes.cs | 11 +-
...icrosoft.DotNet.SharedFramework.Sdk.csproj | 1 -
.../src/CreateFrameworkListFile.cs | 15 +-
.../src/FileUtilities.cs | 4 +-
...ratePlatformManifestEntriesFromFileList.cs | 11 +-
...ratePlatformManifestEntriesFromTemplate.cs | 11 +-
.../src/GenerateSharedFrameworkDepsFile.cs | 17 +-
.../src/ValidateFileVersions.cs | 11 +-
.../SignCheckTask.cs | 14 +-
.../src/SignToolTask.cs | 23 ++-
.../tasks/src/ReadNuGetPackageInfos.cs | 9 +-
.../src/UsageReport/WritePackageUsageData.cs | 70 ++++++--
.../src/UsageReport/WriteUsageReports.cs | 22 +--
.../tasks/src/WriteBuildOutputProps.cs | 16 +-
.../GenerateSwaggerCode.cs | 8 +-
.../Model/Document.cs | 6 +-
.../Tasks/EnsureAllResourcesTranslated.cs | 3 +-
.../Tasks/GatherTranslatedSource.cs | 5 +-
.../Tasks/GatherXlf.cs | 1 +
.../Tasks/SortXlf.cs | 7 +-
.../Tasks/TransformTemplates.cs | 15 +-
.../Tasks/TranslateSource.cs | 11 +-
.../Tasks/UpdateXlf.cs | 15 +-
.../Tasks/XlfTask.cs | 10 +-
186 files changed, 1271 insertions(+), 823 deletions(-)
create mode 100644 eng/MultiThreadableTaskAnalyzer.globalconfig
delete mode 100644 src/Common/Internal/BuildTask.cs
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.1184.8.04.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..1f3d814807d 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,7 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix
///
/// A tool task to invoke the WiX harvesting tool (heat.exe).
///
+ [MSBuildMultiThreadableTask]
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..9bee4606fb8 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,7 @@ 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.
///
+ [MSBuildMultiThreadableTask]
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/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..507ed99004a 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,13 @@
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
+ [MSBuildMultiThreadableTask]
+ public class GenAPITask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
private const string InternalsVisibleTypeName = "System.Runtime.CompilerServices.InternalsVisibleToAttribute";
private const string DefaultFileHeader =
@@ -36,6 +38,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 +201,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 +264,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 +284,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..6e2814b4a07 100644
--- a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
+++ b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
@@ -9,8 +9,15 @@
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.
+ 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 +47,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..34da03ab50e 100644
--- a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
+++ b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
@@ -16,8 +16,17 @@ 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.
+ ///
+ 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 +53,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 +63,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 +85,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 +96,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/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..89069ed0ce0 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
diff --git a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
index a1d7c943d8c..26cb89f6946 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";
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..6290fafa470 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
@@ -15,6 +15,7 @@ namespace Microsoft.DotNet.Helix.Sdk
/// Task that installs a .NET tool in a given folder.
/// Handles parallel builds that install the same tool.
///
+ [MSBuildMultiThreadableTask]
public class InstallDotNetTool : MSBuildTaskBase
{
///
diff --git a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
index e2bacc45c52..0de6b4c21ea 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
@@ -16,7 +16,8 @@
namespace Microsoft.DotNet.Helix.Sdk
{
- public class SendHelixJob : HelixTask
+ [MSBuildMultiThreadableTask]
+ public class SendHelixJob : HelixTask, IMultiThreadableTask
{
public static class MetadataNames
{
@@ -40,6 +41,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 +311,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 +552,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 +568,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.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
[MSBuildMultiThreadableTask]
- public class ExecWithRetries : Microsoft.Build.Utilities.Task, ICancelableTask
+ public class ExecWithRetries : Microsoft.Build.Utilities.Task, ICancelableTask, IMultiThreadableTask
{
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
public string Command { get; set; }
@@ -67,6 +70,12 @@ public override bool Execute()
_runningExec = new Exec
{
BuildEngine = BuildEngine,
+ // Exec derives from ToolTask, which resolves WorkingDirectory and builds the child
+ // process environment through TaskEnvironment. MSBuild only injects that into tasks
+ // it instantiates itself, so this nested instance has to be given ours explicitly;
+ // otherwise a relative WorkingDirectory would resolve against the shared node's
+ // current directory instead of the project directory.
+ TaskEnvironment = TaskEnvironment,
Command = Command,
WorkingDirectory = WorkingDirectory,
IgnoreStandardErrorWarningFormat = IgnoreStandardErrorWarningFormat,
diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetPackageDescription.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetPackageDescription.cs
index cff8c135ef6..357ba0d1ff2 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetPackageDescription.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetPackageDescription.cs
@@ -64,7 +64,19 @@ public override bool Execute()
return false;
}
- Dictionary descriptionTable = s_descriptionCache.GetOrAdd(descriptionPath, LoadDescriptions);
+ if (!s_descriptionCache.TryGetValue(descriptionPath, out Dictionary descriptionTable))
+ {
+ descriptionTable = LoadDescriptions(descriptionPath);
+
+ // Only successful loads are cached. LoadDescriptions returns null after logging an
+ // IOException or UnauthorizedAccessException, and caching that would memoize a
+ // transient failure for every later invocation on this node. A concurrent race just
+ // parses the document twice, which is harmless.
+ if (descriptionTable != null)
+ {
+ s_descriptionCache.TryAdd(descriptionPath, descriptionTable);
+ }
+ }
string description = null;
diff --git a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
index 857a3a692da..bf9c0efa6d7 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Workloads/src/CreateVisualStudioWorkload.wix.cs
@@ -24,6 +24,13 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads
// deep helper chain (WorkloadPackageBase, MsiBase, SwixProject, EmbeddedTemplates, Utils) 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.
+ //
+ // 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 CreateVisualStudioWorkload : VisualStudioWorkloadTaskBase, IMultiThreadableTask
{
///
diff --git a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
index 6e2814b4a07..2cf34465eed 100644
--- a/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
+++ b/src/Microsoft.DotNet.GenFacades/GenPartialFacadeSource.cs
@@ -13,6 +13,13 @@ namespace Microsoft.DotNet.GenFacades
// 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.
diff --git a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
index 34da03ab50e..556fdd86f1c 100644
--- a/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
+++ b/src/Microsoft.DotNet.GenFacades/NotSupportedAssemblyGenerator.cs
@@ -21,6 +21,13 @@ namespace Microsoft.DotNet.GenFacades
/// 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
{
diff --git a/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs b/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
index 6290fafa470..d49f6ff36aa 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/InstallDotNetTool.cs
@@ -15,9 +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.
///
- [MSBuildMultiThreadableTask]
- 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)
///
@@ -97,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);
@@ -157,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.SignCheckTask/SignCheckTask.cs b/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs
index 3ea119d279f..82c27839b80 100644
--- a/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs
+++ b/src/Microsoft.DotNet.SignCheckTask/SignCheckTask.cs
@@ -15,6 +15,13 @@ namespace SignCheckTask
// 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.
diff --git a/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs b/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs
index 36a7e23ace4..6718691b035 100644
--- a/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs
+++ b/src/Microsoft.DotNet.SignTool/src/SignToolTask.cs
@@ -16,6 +16,13 @@ namespace Microsoft.DotNet.SignTool
// 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.
From 11f5b1d3ea4f6412dd05fb6eb4326f963cf10bd9 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Sun, 23 Aug 2026 20:49:30 +0200
Subject: [PATCH 3/9] Resolve aka.ms client certificate path through
TaskEnvironment
CreateAkaMSLinks and DeleteAkaMSLinks are both annotated, so they run in-process,
but their shared base passed the ClientCertificate input straight to
File.ReadAllText. A relative path would resolve against the shared node's current
directory rather than the project directory.
AkaMSLinksBase now implements IMultiThreadableTask and resolves the path.
Found by extending the validator to check the whole base chain for path, I/O,
environment and process APIs instead of reporting attribute-only tasks as
conditionally benign.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../src/AkaMSLinksBase.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
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))
{
From 90e58a3c56ab34443aa9f6934a15eda3edf904e4 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Sun, 23 Aug 2026 21:05:28 +0200
Subject: [PATCH 4/9] Document the task-object cache race in LocateDotNet and
CheckRequiredDotNetVersion
Both tasks do a non-atomic GetRegisteredTaskObject/RegisterTaskObject pair, which
under multithreaded execution lets two threads miss and populate the cache. Both
computations are pure, so the result is unchanged; the only visible effect is that
CheckRequiredDotNetVersion can log a failing check's error twice.
Recording this so the pattern is not mistaken for the SingleError case, where the
same race defeated the task's entire purpose and the attribute had to be removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../src/CheckRequiredDotNetVersion.cs | 5 +++++
src/Microsoft.DotNet.Arcade.Sdk/src/LocateDotNet.cs | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/src/Microsoft.DotNet.Arcade.Sdk/src/CheckRequiredDotNetVersion.cs b/src/Microsoft.DotNet.Arcade.Sdk/src/CheckRequiredDotNetVersion.cs
index 6cd7b82a836..3a3a900f8f9 100644
--- a/src/Microsoft.DotNet.Arcade.Sdk/src/CheckRequiredDotNetVersion.cs
+++ b/src/Microsoft.DotNet.Arcade.Sdk/src/CheckRequiredDotNetVersion.cs
@@ -64,6 +64,11 @@ public override bool Execute()
// The cache is registered per build, not per project, so the repository and the SDK
// version being validated have to be part of the entry. Otherwise a different repository
// or a different required version could reuse this result.
+ //
+ // The read/write pair below is not atomic, so under multithreaded execution two threads
+ // can both miss and both run the check. The check itself is pure, so the result is
+ // identical either way; the only observable effect is that a failing check can log its
+ // error twice, since deduplicating that reporting is part of what the cache buys.
var cachedResult = (CacheEntry)BuildEngine4.GetRegisteredTaskObject(s_cacheKey, RegisteredTaskObjectLifetime.Build);
if (cachedResult != null &&
string.Equals(globalJsonPath.Value, cachedResult.GlobalJsonPath, StringComparison.OrdinalIgnoreCase) &&
diff --git a/src/Microsoft.DotNet.Arcade.Sdk/src/LocateDotNet.cs b/src/Microsoft.DotNet.Arcade.Sdk/src/LocateDotNet.cs
index 85eb5da2341..8aa2f51d592 100644
--- a/src/Microsoft.DotNet.Arcade.Sdk/src/LocateDotNet.cs
+++ b/src/Microsoft.DotNet.Arcade.Sdk/src/LocateDotNet.cs
@@ -56,6 +56,11 @@ private void ExecuteImpl()
// The cache is registered per build, not per project, so the repository identity has to
// be part of the entry. Otherwise a second repository with a coincidentally matching
// global.json timestamp and PATH would reuse the first repository's dotnet.
+ //
+ // The read/write pair below is not atomic, so under multithreaded execution two threads
+ // can both miss and both populate it. That is benign here: the computation is pure and
+ // deterministic for a given (global.json, timestamp, PATH), so the loser of the race
+ // simply overwrites an identical entry. The cache is an optimization, not a lock.
var cachedResult = (CacheEntry)BuildEngine4.GetRegisteredTaskObject(s_cacheKey, RegisteredTaskObjectLifetime.Build);
if (cachedResult != null &&
string.Equals(globalJsonPath.Value, cachedResult.GlobalJsonPath, StringComparison.OrdinalIgnoreCase) &&
From f98f38477ae5f58023a55421aa9a3e7d430dea64 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 24 Aug 2026 08:41:11 +0200
Subject: [PATCH 5/9] Review: de-annotate six tasks with unverifiable
ambient-state dependencies
All six are P3 with no measured invocations in the hot-path profile, so
in-process routing buys no measured build-time saving while taking on real
correctness risk. Each keeps its `IMultiThreadableTask` implementation and
`TaskEnvironment`-based path handling -- correct either way, and the groundwork
for a later migration -- but loses the routing attribute, and carries a comment
recording why so it is not silently re-added.
- GenAPITask: `HostEnvironment` resolves the raw `LibPath`/`Assembly` values via
`Environment.ExpandEnvironmentVariables` plus `Directory.Exists`/`File.Exists`
(HostEnvironment.cs:719-740). The task resolves `OutputPath` and `HeaderFile`
through `TaskEnvironment` but hands these two through raw -- resolved at one
site, raw at another. Fixing it needs expand-then-resolve inside Cci, because
resolving a path that still contains %VAR% would itself be wrong.
- SendHelixJob: `JobDefinition` reads BUILD_REPOSITORY_NAME, BUILD_SOURCEBRANCH,
SYSTEM_TEAMPROJECT and BUILD_REASON straight from `Environment`
(JobDefinition.cs:209-223,402-423).
- CreateAzureDevOpsFeed, CreateNewAzureContainer, CreateAzureContainerIfNotExists:
build an `AzureCliCredential` when no PAT/AccountKey is supplied, which launches
`az` from the ambient process environment. This is the same class already used
to exclude PublishArtifactsInManifest*; the criterion was applied incompletely.
- UploadToAzure: cancellation state is `static`, so `Cancel()` on one instance
cancels the token handed to every other concurrent and future instance, and the
uploads use an unlinked timeout token rather than the task token.
Also avoided writing the literal attribute name in these comments, so an audit
grep for the attribute does not match a class that deliberately lacks it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
---
.../src/CreateAzureDevOpsFeed.cs | 5 ++++-
.../src/common/CreateAzureContainerIfNotExists.cs | 5 ++++-
.../src/common/CreateNewAzureContainer.cs | 5 ++++-
.../src/common/UploadToAzure.cs | 5 ++++-
src/Microsoft.DotNet.GenAPI/GenAPITask.cs | 6 +++++-
src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs | 6 +++++-
6 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs
index ba8f56e4b9f..71084054af2 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/CreateAzureDevOpsFeed.cs
@@ -18,7 +18,10 @@
namespace Microsoft.DotNet.Build.Tasks.Feed
{
- [MSBuildMultiThreadableTask]
+ // Deliberately not marked multithreadable: when no PAT is supplied this task builds an
+ // AzureCliCredential, which resolves and launches `az` from the ambient process environment
+ // rather than the project's injected one. Migrating requires supplying credentials explicitly or
+ // launching the CLI through TaskEnvironment.
public class CreateAzureDevOpsFeed : MSBuild.Task
{
[Output]
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateAzureContainerIfNotExists.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateAzureContainerIfNotExists.cs
index 041c892d2ee..5c82ae415dd 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateAzureContainerIfNotExists.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateAzureContainerIfNotExists.cs
@@ -8,7 +8,10 @@
namespace Microsoft.DotNet.Build.Tasks.Feed
{
- [MSBuildMultiThreadableTask]
+ // Deliberately not marked multithreadable: the AccountKey is null path builds an
+ // AzureCliCredential, which resolves and launches `az` from the ambient process environment
+ // rather than the project's injected one. Migrating requires supplying credentials explicitly or
+ // launching the CLI through TaskEnvironment.
public class CreateAzureContainerIfNotExists : CreateAzureContainer
{
///
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateNewAzureContainer.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateNewAzureContainer.cs
index 5cbaa459304..df75595efcb 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateNewAzureContainer.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/CreateNewAzureContainer.cs
@@ -14,7 +14,10 @@ namespace Microsoft.DotNet.Build.Tasks.Feed
/// try creating a container [ContainerName]-1, [ContainerName]-2 and so on until the name is unique.
/// The final name is saved in ContainerName.
///
- [MSBuildMultiThreadableTask]
+ // Deliberately not marked multithreadable: the AccountKey is null path builds an
+ // AzureCliCredential, which resolves and launches `az` from the ambient process environment
+ // rather than the project's injected one. Migrating requires supplying credentials explicitly or
+ // launching the CLI through TaskEnvironment.
public class CreateNewAzureContainer : CreateAzureContainer
{
public override async Task GetBlobStorageUtilsAsync()
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs
index 76679e854de..ca0759a809d 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs
@@ -13,7 +13,10 @@
namespace Microsoft.DotNet.Build.CloudTestTasks
{
- [MSBuildMultiThreadableTask]
+ // Deliberately not marked multithreadable: cancellation state below is static, so Cancel()
+ // on one instance cancels the token handed to every other concurrent and future instance in the
+ // process, and the uploads use an unlinked timeout token instead of the task token. Migrating
+ // requires a per-instance CancellationTokenSource and uploads linked to it.
public class UploadToAzure : AzureConnectionStringBuildTask, ICancelableTask, IMultiThreadableTask
{
private static readonly CancellationTokenSource TokenSource = new CancellationTokenSource();
diff --git a/src/Microsoft.DotNet.GenAPI/GenAPITask.cs b/src/Microsoft.DotNet.GenAPI/GenAPITask.cs
index 507ed99004a..d5278ff4b1f 100644
--- a/src/Microsoft.DotNet.GenAPI/GenAPITask.cs
+++ b/src/Microsoft.DotNet.GenAPI/GenAPITask.cs
@@ -19,7 +19,11 @@
namespace Microsoft.DotNet.GenAPI
{
- [MSBuildMultiThreadableTask]
+ // 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";
diff --git a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
index 0de6b4c21ea..a4e0f7a7753 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/SendHelixJob.cs
@@ -16,7 +16,11 @@
namespace Microsoft.DotNet.Helix.Sdk
{
- [MSBuildMultiThreadableTask]
+ // 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
From 01686432a53bc15eddf65be52abe166e729c0028 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 24 Aug 2026 12:19:57 +0200
Subject: [PATCH 6/9] Remove the unused LaunchDebugger task
LaunchDebugger has no UsingTask registration and no reference anywhere in
the repo, so it is unreachable dead code. It was one of the tasks left
unannotated for the multithreading migration, since Debugger.Launch attaches
a debugger to the whole process and a shared multithreaded node would attach
for every project building on it. Deleting it removes the exclusion instead
of documenting it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
---
.../src/LaunchDebugger.cs | 22 -------------------
1 file changed, 22 deletions(-)
delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Feed/src/LaunchDebugger.cs
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/LaunchDebugger.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/LaunchDebugger.cs
deleted file mode 100644
index 011ce7b9777..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/LaunchDebugger.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.Build.Utilities;
-using System.Diagnostics;
-using Microsoft.Build.Framework;
-
-namespace Microsoft.DotNet.Build.Tasks.Feed
-{
- // Not opted into multithreading, and deliberately never will be: Debugger.Launch attaches a
- // debugger to the whole process. In a shared multithreaded node that would affect every project
- // building on that node, and concurrent invocations would race to attach. Keeping this
- // diagnostic task in the out-of-proc TaskHost confines the attach to a single sidecar.
- public class LaunchDebugger : Microsoft.Build.Utilities.Task
- {
- public override bool Execute()
- {
- Debugger.Launch();
- return true;
- }
- }
-}
From 9dfec6fd2053570b67e78a6be85af9ecee316542 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 24 Aug 2026 12:34:15 +0200
Subject: [PATCH 7/9] Remove the unused UploadToAzure task
UploadToAzure has no UsingTask registration and no reference anywhere in the
repo, so it is unreachable dead code. Uploads go through
AzureStorageAssetPublisher and BlobFeedAction instead.
It was one of the tasks left unannotated for the multithreading migration:
its CancellationTokenSource is static, so Cancel() on one instance cancelled
the token handed to every other concurrent and future instance, and the
uploads used an unlinked timeout token rather than the task token. Deleting
it removes that bug along with the exclusion.
Its base AzureConnectionStringBuildTask and the AzureStorageUtils helpers stay
in use by CreateAzureContainer and the publishers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
---
.../src/common/UploadToAzure.cs | 145 ------------------
1 file changed, 145 deletions(-)
delete mode 100644 src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs
diff --git a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs b/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs
deleted file mode 100644
index ca0759a809d..00000000000
--- a/src/Microsoft.DotNet.Build.Tasks.Feed/src/common/UploadToAzure.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Build.Framework;
-using System.Collections.Generic;
-using Azure.Storage.Blobs.Specialized;
-using Azure.Storage.Blobs;
-using Microsoft.DotNet.Build.Tasks.Feed;
-
-namespace Microsoft.DotNet.Build.CloudTestTasks
-{
- // Deliberately not marked multithreadable: cancellation state below is static, so Cancel()
- // on one instance cancels the token handed to every other concurrent and future instance in the
- // process, and the uploads use an unlinked timeout token instead of the task token. Migrating
- // requires a per-instance CancellationTokenSource and uploads linked to it.
- public class UploadToAzure : AzureConnectionStringBuildTask, ICancelableTask, IMultiThreadableTask
- {
- private static readonly CancellationTokenSource TokenSource = new CancellationTokenSource();
- private static readonly CancellationToken CancellationToken = TokenSource.Token;
-
- /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
- public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
-
- ///
- /// The name of the container to access. The specified name must be in the correct format, see the
- /// following page for more info. https://msdn.microsoft.com/en-us/library/azure/dd135715.aspx
- ///
- [Required]
- public string ContainerName { get; set; }
-
- ///
- /// An item group of files to upload. Each item must have metadata RelativeBlobPath
- /// that specifies the path relative to ContainerName where the item will be uploaded.
- ///
- [Required]
- public ITaskItem[] Items { get; set; }
-
- ///
- /// Indicates if the destination blob should be overwritten if it already exists. The default if false.
- ///
- public bool Overwrite { get; set; } = false;
-
- ///
- /// Enables idempotency when Overwrite is false.
- ///
- /// false: (default) Attempting to upload an item that already exists fails.
- ///
- /// true: When an item already exists, download the existing blob to check if it's
- /// byte-for-byte identical to the one being uploaded. If so, pass. If not, fail.
- ///
- public bool PassIfExistingItemIdentical { get; set; }
-
- ///
- /// Specifies the maximum number of clients to concurrently upload blobs to azure
- ///
- [Obsolete]
- public int MaxClients { get; set; } = 8;
-
- public int UploadTimeoutInMinutes { get; set; } = 5;
-
- public void Cancel()
- {
- TokenSource.Cancel();
- }
-
- public override bool Execute()
- {
- return ExecuteAsync(CancellationToken).GetAwaiter().GetResult();
- }
-
- public async Task ExecuteAsync(CancellationToken ct)
- {
- if (Items.Length == 0)
- {
- Log.LogError("No items were provided for upload.");
- return false;
- }
-
- Log.LogMessage("Begin uploading blobs to Azure account {0} in container {1}.",
- AccountName,
- ContainerName);
-
- try
- {
- AzureStorageUtils blobUtils = new AzureStorageUtils(AccountName, AccountKey, ContainerName);
-
- List uploadTasks = new List();
-
- foreach (var item in Items)
- {
- uploadTasks.Add(Task.Run(async () =>
- {
- string relativeBlobPath = item.GetMetadata("RelativeBlobPath");
-
- if (string.IsNullOrEmpty(relativeBlobPath))
- {
- throw new Exception(string.Format("Metadata 'RelativeBlobPath' is missing for item '{0}'.", item.ItemSpec));
- }
-
- if (!File.Exists(TaskEnvironment.GetAbsolutePath(item.ItemSpec)))
- {
- throw new Exception(string.Format("The file '{0}' does not exist.", item.ItemSpec));
- }
-
- BlobClient blobReference = blobUtils.GetBlob(relativeBlobPath);
-
- if (!Overwrite && await blobReference.ExistsAsync())
- {
- if (PassIfExistingItemIdentical)
- {
- if (await blobReference.IsFileIdenticalToBlobAsync(TaskEnvironment.GetAbsolutePath(item.ItemSpec)))
- {
- return;
- }
- }
-
- throw new Exception(string.Format("The blob '{0}' already exists.", relativeBlobPath));
- }
-
- CancellationTokenSource timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(UploadTimeoutInMinutes));
-
- using (Stream localFileStream = File.OpenRead(TaskEnvironment.GetAbsolutePath(item.ItemSpec)))
- {
- await blobReference.UploadAsync(localFileStream, timeoutTokenSource.Token);
- }
- }));
- }
-
- await Task.WhenAll(uploadTasks);
-
- Log.LogMessage("Upload to Azure is complete, a total of {0} items were uploaded.", Items.Length);
- }
- catch (Exception e)
- {
- Log.LogErrorFromException(e, true);
- }
-
- return !Log.HasLoggedErrors;
- }
- }
-}
From 894c05a898380dbe1d345d9d8e98e5bdc2e081a2 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 24 Aug 2026 13:06:59 +0200
Subject: [PATCH 8/9] Resolve task input paths before they enter helper chains
Copilot review flagged eleven annotated tasks that pass a raw, user-settable
path into a helper that opens it with plain File/Directory/NuGet APIs. In a
shared -mt node those bind to the process current directory rather than the
invoking project.
The inputs are not absolute by construction. RuntimeGraph and
RuntimeIdentifierGraph come from $(RuntimeIdentifierGraphPath), which is a
public settable property, and RuntimeFile comes from
$(RuntimeIdGraphDefinitionFile), which is explicitly guarded with
Condition="'$(...)' == ''" so it is designed to be overridden. Only the
Bundled* SDK defaults are absolute by construction.
Resolved through the injected TaskEnvironment:
- ChooseBestTargetFrameworksTask, ChooseBestP2PTargetFrameworkTask: RuntimeGraph.
These had the attribute but did not implement IMultiThreadableTask, so they had
no way to resolve anything; both now do. TargetFrameworkResolver also caches by
the path string, so a relative value additionally let two projects share one
resolver built from different files.
- GeneratePackageReport, GetApplicableAssetsFromPackages, HarvestPackage:
RuntimeFile, before constructing the NuGet asset resolvers.
- GenerateSharedFrameworkDepsFile: RuntimeIdentifierGraph. It is optional, so an
unset value is passed through unchanged to keep the existing ReadRuntimeGraph
error instead of throwing from GetAbsolutePath.
- NuGetPack: BaseDirectory before PopulateFiles, and the nuspec-relative fallback.
- GetCompatiblePackageTargetFrameworks: PackagePaths. Also had the attribute
without the interface.
- WritePackageUsageData: the four package-file arrays feeding ReadIdentity.
- CreateXHarnessAndroidWorkItems, CreateXHarnessAppleWorkItems: ApkPath and
AppBundlePath, plus the Apple TmpDir. Relative paths here are the documented
usage in tools/xharness-runner/Readme.md, so this was the most likely of the
set to bite. TaskEnvironment is added to the shared XHarnessTaskBase.
The XHarness tests already covered relative paths, which is why five of them
caught the change. They now resolve their MockFileSystem registrations through
the same TaskEnvironment the task uses, so the relative-path coverage is kept
and the resolution itself is exercised.
Full non-incremental Release build: 0 errors, 0 warnings.
Helix.Sdk 236, Packaging 48, PackageTesting 9, SourceBuild 1: all pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
---
.../src/GeneratePackageReport.cs | 4 ++--
.../src/GetApplicableAssetsFromPackages.cs | 2 +-
.../src/HarvestPackage.cs | 4 ++--
.../src/NuGetPack.cs | 4 +++-
.../src/ChooseBestP2PTargetFrameworkTask.cs | 7 +++++--
.../src/ChooseBestTargetFrameworksTask.cs | 7 +++++--
.../CreateXHarnessAndroidWorkItemsTests.cs | 4 ++--
.../CreateXHarnessAppleWorkItemsTests.cs | 4 ++--
.../Sdk/CreateXHarnessAndroidWorkItems.cs | 5 +++++
.../Sdk/CreateXHarnessAppleWorkItems.cs | 10 +++++++++-
src/Microsoft.DotNet.Helix/Sdk/XharnessTaskBase.cs | 5 ++++-
.../GetCompatiblePackageTargetFrameworks.cs | 7 +++++--
.../src/GenerateSharedFrameworkDepsFile.cs | 7 ++++++-
.../tasks/src/UsageReport/WritePackageUsageData.cs | 11 +++++++----
14 files changed, 58 insertions(+), 23 deletions(-)
diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GeneratePackageReport.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GeneratePackageReport.cs
index acdc457882f..6c656afa825 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GeneratePackageReport.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GeneratePackageReport.cs
@@ -337,7 +337,7 @@ private void LoadFiles()
}
}
- _resolver = new AggregateNuGetAssetResolver(RuntimeFile);
+ _resolver = new AggregateNuGetAssetResolver(TaskEnvironment.GetAbsolutePath(RuntimeFile));
foreach (string packageId in packageItems.Keys)
{
_resolver.AddPackageItems(packageId, packageItems[packageId].Select(f => f.TargetPath));
@@ -352,7 +352,7 @@ private void LoadFiles()
.Select(pf => pf.TargetPath)
.Where(f => !NuGetAssetResolver.IsPlaceholder(f));
- _resolverWithoutPlaceholders = new NuGetAssetResolver(RuntimeFile, filesWithoutPlaceholders);
+ _resolverWithoutPlaceholders = new NuGetAssetResolver(TaskEnvironment.GetAbsolutePath(RuntimeFile), filesWithoutPlaceholders);
}
}
diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetApplicableAssetsFromPackages.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetApplicableAssetsFromPackages.cs
index fb0a12141bf..598fdd0d975 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetApplicableAssetsFromPackages.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/GetApplicableAssetsFromPackages.cs
@@ -192,7 +192,7 @@ private void LoadFiles()
}
}
- _resolver = new AggregateNuGetAssetResolver(RuntimeFile);
+ _resolver = new AggregateNuGetAssetResolver(TaskEnvironment.GetAbsolutePath(RuntimeFile));
foreach (string packageId in _packageToPackageItems.Keys)
{
_resolver.AddPackageItems(packageId, _packageToPackageItems[packageId].Select(f => f.TargetPath));
diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/HarvestPackage.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/HarvestPackage.cs
index 5a05927567c..4c84f0d158f 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/HarvestPackage.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/HarvestPackage.cs
@@ -161,7 +161,7 @@ private void HarvestSupportedFrameworks()
{
List supportedFrameworks = new List();
- AggregateNuGetAssetResolver resolver = new AggregateNuGetAssetResolver(RuntimeFile);
+ AggregateNuGetAssetResolver resolver = new AggregateNuGetAssetResolver(TaskEnvironment.GetAbsolutePath(RuntimeFile));
string packagePath = _packageFolders[PackageId];
foreach (var packageFolder in _packageFolders)
@@ -174,7 +174,7 @@ private void HarvestSupportedFrameworks()
// and use the netstandard reference assembly to determine the API version
var filesWithoutPlaceholders = GetPackageItems(packagePath)
.Where(f => !NuGetAssetResolver.IsPlaceholder(f));
- NuGetAssetResolver resolverWithoutPlaceholders = new NuGetAssetResolver(RuntimeFile, filesWithoutPlaceholders);
+ NuGetAssetResolver resolverWithoutPlaceholders = new NuGetAssetResolver(TaskEnvironment.GetAbsolutePath(RuntimeFile), filesWithoutPlaceholders);
string package = $"{PackageId}/{PackageVersion}";
diff --git a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/NuGetPack.cs b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/NuGetPack.cs
index 3fa1a8ff99b..639e4aa4de6 100644
--- a/src/Microsoft.DotNet.Build.Tasks.Packaging/src/NuGetPack.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.Packaging/src/NuGetPack.cs
@@ -274,7 +274,9 @@ public void Pack(string nuspecPath, string nupkgPath, Manifest manifest, bool pa
PackageBuilder builder = new PackageBuilder(deterministic: Deterministic);
SetDeterministicTimestamp(builder, DeterministicTimestamp);
- string baseDirectoryPath = (string.IsNullOrEmpty(BaseDirectory)) ? Path.GetDirectoryName(nuspecPath) : BaseDirectory;
+ string baseDirectoryPath = string.IsNullOrEmpty(BaseDirectory)
+ ? Path.GetDirectoryName(TaskEnvironment.GetAbsolutePath(nuspecPath))
+ : TaskEnvironment.GetAbsolutePath(BaseDirectory);
builder.Populate(manifest.Metadata);
builder.PopulateFiles(baseDirectoryPath, manifest.Files);
diff --git a/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestP2PTargetFrameworkTask.cs b/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestP2PTargetFrameworkTask.cs
index e9a5f95ef11..b30d2b5cf93 100644
--- a/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestP2PTargetFrameworkTask.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestP2PTargetFrameworkTask.cs
@@ -14,11 +14,14 @@
namespace Microsoft.DotNet.Build.Tasks.TargetFramework
{
[MSBuildMultiThreadableTask]
- public class ChooseBestP2PTargetFrameworkTask : Microsoft.Build.Utilities.Task
+ public class ChooseBestP2PTargetFrameworkTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
private const string NEAREST_TARGET_FRAMEWORK = "NearestTargetFramework";
private const string TARGET_FRAMEWORKS = "TargetFrameworks";
+ /// Injected by MSBuild so paths resolve against the project directory in multithreaded builds.
+ public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
+
[Required]
public string? RuntimeGraph { get; set; }
@@ -60,7 +63,7 @@ public override bool Execute()
return false;
}
- TargetFrameworkResolver targetFrameworkResolver = TargetFrameworkResolver.CreateOrGet(RuntimeGraph!);
+ TargetFrameworkResolver targetFrameworkResolver = TargetFrameworkResolver.CreateOrGet(TaskEnvironment.GetAbsolutePath(RuntimeGraph!));
List assignedProjects = new(AnnotatedProjectReferences.Length);
foreach (ITaskItem annotatedProjectReference in AnnotatedProjectReferences)
diff --git a/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestTargetFrameworksTask.cs b/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestTargetFrameworksTask.cs
index 9b703e3d4da..aeb4a8814a8 100644
--- a/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestTargetFrameworksTask.cs
+++ b/src/Microsoft.DotNet.Build.Tasks.TargetFramework/src/ChooseBestTargetFrameworksTask.cs
@@ -10,8 +10,11 @@
namespace Microsoft.DotNet.Build.Tasks.TargetFramework
{
[MSBuildMultiThreadableTask]
- public class ChooseBestTargetFrameworksTask : Microsoft.Build.Utilities.Task
+ public class ChooseBestTargetFrameworksTask : 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[]? BuildTargetFrameworks { get; set; }
@@ -30,7 +33,7 @@ public class ChooseBestTargetFrameworksTask : Microsoft.Build.Utilities.Task
public override bool Execute()
{
List bestTargetFrameworkList = new(BuildTargetFrameworks!.Length);
- TargetFrameworkResolver targetframeworkResolver = TargetFrameworkResolver.CreateOrGet(RuntimeGraph!);
+ TargetFrameworkResolver targetframeworkResolver = TargetFrameworkResolver.CreateOrGet(TaskEnvironment.GetAbsolutePath(RuntimeGraph!));
foreach (ITaskItem buildTargetFramework in BuildTargetFrameworks)
{
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/CreateXHarnessAndroidWorkItems.cs b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
index 89069ed0ce0..742e90147da 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAndroidWorkItems.cs
@@ -74,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 26cb89f6946..1e3ee06c70e 100644
--- a/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
+++ b/src/Microsoft.DotNet.Helix/Sdk/CreateXHarnessAppleWorkItems.cs
@@ -64,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);
@@ -102,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/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.PackageTesting/GetCompatiblePackageTargetFrameworks.cs b/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs
index 3e7612186ed..350ac34ca19 100644
--- a/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs
+++ b/src/Microsoft.DotNet.PackageTesting/GetCompatiblePackageTargetFrameworks.cs
@@ -11,11 +11,14 @@
namespace Microsoft.DotNet.PackageTesting
{
[MSBuildMultiThreadableTask]
- public class GetCompatiblePackageTargetFrameworks : Microsoft.Build.Utilities.Task
+ public class GetCompatiblePackageTargetFrameworks : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
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));
diff --git a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs
index 4a44701f543..02e893ef4fd 100644
--- a/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs
+++ b/src/Microsoft.DotNet.SharedFramework.Sdk/src/GenerateSharedFrameworkDepsFile.cs
@@ -100,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))
diff --git a/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs b/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs
index 03fc1cec470..7973cf4f9b1 100644
--- a/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs
+++ b/src/Microsoft.DotNet.SourceBuild/tasks/src/UsageReport/WritePackageUsageData.cs
@@ -113,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();
@@ -321,5 +321,8 @@ private string[] ReadRidsFromRuntimeJson(string path)
.Select(o => o.Name)
.ToArray();
}
+
+ private PackageIdentity ReadIdentityFromResolvedPath(string nupkgFile) =>
+ ReadNuGetPackageInfos.ReadIdentity(TaskEnvironment.GetAbsolutePath(nupkgFile));
}
}
From d36f35c756b447446e09847bb4d17562addb6778 Mon Sep 17 00:00:00 2001
From: Viktor Hofer <7412651+ViktorHofer@users.noreply.github.com>
Date: Mon, 24 Aug 2026 13:28:16 +0200
Subject: [PATCH 9/9] Remove inert multi-threadable attributes from
unregistered WiX tool tasks
WixToolTask and HarvesterToolTask are not registered via UsingTask; MsiBase
constructs them directly. MSBuild therefore never routes them and never injects
a TaskEnvironment, so WixToolTaskBase resolves against TaskEnvironment.Fallback
and the annotation had no effect other than marking part of the
CreateVisualStudioWorkload helper chain as migrated, which contradicts the
TODO comments on both entry-point tasks.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: be19d794-fe4f-43fe-9e6e-f6e6b13ec6e7
---
.../src/Wix/HarvesterToolTask.cs | 7 ++++++-
.../src/Wix/WixToolTask.cs | 7 ++++++-
2 files changed, 12 insertions(+), 2 deletions(-)
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 1f3d814807d..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,7 +10,12 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads.Wix
///
/// A tool task to invoke the WiX harvesting tool (heat.exe).
///
- [MSBuildMultiThreadableTask]
+ // 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 9bee4606fb8..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,7 +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.
///
- [MSBuildMultiThreadableTask]
+ // 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();