From d564eb1714220c7dbce6e95c93f6d45d3897f9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Fri, 30 May 2025 13:46:11 +0200 Subject: [PATCH 01/51] wip --- .../TaskExecutionHost/TaskExecutionHost.cs | 120 +++++++++++++++++- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 9523e2ea4ca..5ce09004df7 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -953,18 +953,30 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters } else { - TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); - try + // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances + bool shouldUseTaskHost = ShouldUseTaskHostForCustomFactory(); + + if (shouldUseTaskHost) { - task = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? - taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : - _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + // Create a TaskHostTask to run the custom factory's task out of process + task = CreateTaskHostTaskForCustomFactory(taskIdentityParameters); } - finally + else { + // Normal in-process execution for custom task factories + TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); + try + { + task = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? + taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : + _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + } + finally + { #if FEATURE_APPDOMAIN - loggingHost.MarkAsInactive(); + loggingHost.MarkAsInactive(); #endif + } } } } @@ -1662,5 +1674,99 @@ private void DisplayCancelWaitMessage() // if the task logging context is no longer valid, we choose to eat the exception because we can't log the message anyway. } } + + /// + /// Determines whether custom (non-AssemblyTaskFactory) task factories should use task host for out-of-process execution. + /// + /// True if tasks from custom factories should run out of process + private bool ShouldUseTaskHostForCustomFactory() + { + // Check the global environment variable that forces all tasks out of process + bool forceTaskHostLaunch = (Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"); + + if (!forceTaskHostLaunch) + { + return false; + } + + // Exclude well-known tasks that are known to depend on IBuildEngine callbacks + // as forcing those out of proc would set them up for known failure + if (TypeLoader.IsPartialTypeNameMatch(_taskName, "MSBuild") || + TypeLoader.IsPartialTypeNameMatch(_taskName, "CallTarget")) + { + return false; + } + + return true; + } + + /// + /// Creates a TaskHostTask wrapper to run a custom factory's task out of process. + /// + /// Task identity parameters + /// A TaskHostTask that will execute the real task out of process + private ITask CreateTaskHostTaskForCustomFactory(IDictionary taskIdentityParameters) + { + // First, create the actual task using the custom factory + TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); + ITask actualTask; + + try + { + actualTask = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? + taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : + _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + } + finally + { +#if FEATURE_APPDOMAIN + loggingHost.MarkAsInactive(); +#endif + } + + if (actualTask == null) + { + return null; + } + + // Create a LoadedType for the actual task type so we can wrap it in TaskHostTask + Type taskType = actualTask.GetType(); + LoadedType taskLoadedType = new LoadedType( + taskType, + AssemblyLoadInfo.Create(taskType.Assembly.FullName, taskType.Assembly.Location), + taskType.Assembly, + typeof(ITaskItem)); + + // Create task host parameters for out-of-process execution + IDictionary taskHostParameters = new Dictionary + { + [XMakeAttributes.runtime] = XMakeAttributes.GetCurrentMSBuildRuntime(), + [XMakeAttributes.architecture] = XMakeAttributes.GetCurrentMSBuildArchitecture() + }; + + // Merge with any existing task identity parameters + if (taskIdentityParameters != null) + { + foreach (var kvp in taskIdentityParameters) + { + taskHostParameters[kvp.Key] = kvp.Value; + } + } + + // Clean up the original task since we're going to wrap it + // _taskFactoryWrapper.TaskFactory.CleanupTask(actualTask); // Create and return the TaskHostTask wrapper +#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter + return new TaskHostTask( + _taskLocation, + _taskLoggingContext, + _buildComponentHost, + taskHostParameters, + taskLoadedType +#if FEATURE_APPDOMAIN + , AppDomainSetup +#endif + ); +#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter + } } } From 4b374773e66ccafbe3d71659634ea0e8ec2aa980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 3 Jun 2025 15:46:30 +0200 Subject: [PATCH 02/51] wip2 --- .../BackEnd/BuildManager/BuildManager.cs | 3 ++ src/Shared/AssemblyLoadInfo.cs | 2 - src/Shared/Constants.cs | 5 +++ .../RoslynCodeTaskFactory.cs | 41 ++++++++++--------- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 40d303360c4..fd1f6024f3d 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1159,6 +1159,9 @@ public void EndBuild() } } + // clean up inline tasks + FileUtilities.DeleteDirectoryNoThrow(Path.Combine(FileUtilities.TempFileDirectory, MSBuildConstants.InlineTaskTempDllSubPath), recursive: true); + void SerializeCaches() { string errorMessage = CacheSerialization.SerializeCaches( diff --git a/src/Shared/AssemblyLoadInfo.cs b/src/Shared/AssemblyLoadInfo.cs index be467ff2a20..a17f3204fd5 100644 --- a/src/Shared/AssemblyLoadInfo.cs +++ b/src/Shared/AssemblyLoadInfo.cs @@ -26,8 +26,6 @@ internal static AssemblyLoadInfo Create(string assemblyName, string assemblyFile { ErrorUtilities.VerifyThrow((!string.IsNullOrEmpty(assemblyName)) || (!string.IsNullOrEmpty(assemblyFile)), "We must have either the assembly name or the assembly file/path."); - ErrorUtilities.VerifyThrow((assemblyName == null) || (assemblyFile == null), - "We must not have both the assembly name and the assembly file/path."); if (assemblyName != null) { diff --git a/src/Shared/Constants.cs b/src/Shared/Constants.cs index 4aa800ef2d2..f7a131836bd 100644 --- a/src/Shared/Constants.cs +++ b/src/Shared/Constants.cs @@ -114,6 +114,11 @@ internal static class MSBuildConstants /// internal const string ProjectReferenceTargetsOrDefaultTargetsMarker = ".projectReferenceTargetsOrDefaultTargets"; + /// + /// The sub-path within the temporary directory where compiled inline tasks are located. + /// + internal const string InlineTaskTempDllSubPath = nameof(InlineTaskTempDllSubPath); + // One-time allocations to avoid implicit allocations for Split(), Trim(). internal static readonly char[] SemicolonChar = [';']; internal static readonly char[] SpaceChar = [' ']; diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index d5d9ebda785..c37239666af 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -84,7 +84,7 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory /// A cache of objects and their corresponding compiled assembly. This cache ensures that two of the exact same code task /// declarations are not compiled multiple times. /// - private static readonly ConcurrentDictionary CompiledAssemblyCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary CompiledAssemblyCache = new ConcurrentDictionary(); /// /// Stores the path to the directory that this assembly is located in. @@ -162,7 +162,7 @@ public bool Initialize(string taskName, IDictionary pa } // Attempt to compile an assembly (or get one from the cache) - if (!TryCompileInMemoryAssembly(taskFactoryLoggingHost, taskInfo, out Assembly assembly)) + if (!TryCompileAssembly(taskFactoryLoggingHost, taskInfo, out string assemblyPath, out Assembly assembly)) { return false; } @@ -655,17 +655,23 @@ private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDec } /// - /// Attempts to compile the current source code and load the assembly into memory. + /// Attempts to compile the current source code. /// /// An to use give to the compiler task so that messages can be logged. /// A object containing details about the task. - /// The if the source code be compiled and loaded, otherwise null. - /// true if the source code could be compiled and loaded, otherwise null. - private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) + /// The path to a dll if the source code be compiled, otherwise null. + /// The loaded assembly if compilation and loading succeeded, otherwise null. + /// true if the source code could be compiled and loaded, otherwise false. + private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out string assemblyPath, out Assembly assembly) { + assembly = null; + assemblyPath = null; + // First attempt to get a compiled assembly from the cache - if (CompiledAssemblyCache.TryGetValue(taskInfo, out assembly)) + if (CompiledAssemblyCache.TryGetValue(taskInfo, out var cachedEntry)) { + assemblyPath = cachedEntry.Path; + assembly = cachedEntry.Assembly; return true; } @@ -675,9 +681,9 @@ private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTask } // The source code cannot actually be compiled "in memory" so instead the source code is written to disk in - // the temp folder as well as the assembly. After compilation, the source code and assembly are deleted. + // the temp folder as well as the assembly. After build, the source code and assembly are deleted. string sourceCodePath = FileUtilities.GetTemporaryFileName(".tmp"); - string assemblyPath = FileUtilities.GetTemporaryFileName(".dll"); + assemblyPath = FileUtilities.GetTemporaryFile(Path.Combine(FileUtilities.TempFileDirectory, MSBuildConstants.InlineTaskTempDllSubPath), null, ".dll", false); // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT // is set (which allows for debugging problems) @@ -759,12 +765,14 @@ private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTask } } - // Return the assembly which is loaded into memory - assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); - - // Attempt to cache the compiled assembly - CompiledAssemblyCache.TryAdd(taskInfo, assembly); + // Load the compiled assembly + assembly = Assembly.LoadFrom(assemblyPath); + if (assembly == null) + { + return false; + } + CompiledAssemblyCache.TryAdd(taskInfo, (assemblyPath, assembly)); return true; } catch (Exception e) @@ -774,11 +782,6 @@ private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTask } finally { - if (FileSystems.Default.FileExists(assemblyPath)) - { - File.Delete(assemblyPath); - } - if (deleteSourceCodeFile && FileSystems.Default.FileExists(sourceCodePath)) { File.Delete(sourceCodePath); From 5eee4011acad7d809de2a03c1e4c17bcabe648c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 3 Jun 2025 16:03:34 +0200 Subject: [PATCH 03/51] refactor envvar to traits --- .../BackEnd/TaskExecutionHost/TaskExecutionHost.cs | 5 +---- src/Build/Instance/TaskRegistry.cs | 12 ++---------- src/Framework/Traits.cs | 8 ++++++++ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 5ce09004df7..f8c2bbfe6e6 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -1681,10 +1681,7 @@ private void DisplayCancelWaitMessage() /// True if tasks from custom factories should run out of process private bool ShouldUseTaskHostForCustomFactory() { - // Check the global environment variable that forces all tasks out of process - bool forceTaskHostLaunch = (Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"); - - if (!forceTaskHostLaunch) + if (!Traits.Instance.ForceTaskHostLaunch) { return false; } diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index c4023835131..2e3433b7ccb 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -60,14 +60,6 @@ internal sealed class TaskRegistry : ITranslatable /// private Toolset _toolset; - /// - /// If true, we will force all tasks to run in the MSBuild task host EXCEPT - /// a small well-known set of tasks that are known to depend on IBuildEngine - /// callbacks; as forcing those out of proc would be just setting them up for - /// known failure. - /// - private static readonly bool s_forceTaskHostLaunch = (Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"); - /// /// Simple name for the MSBuild tasks (v4), used for shimming in loading /// task factory UsingTasks @@ -1277,7 +1269,7 @@ public bool ComputeIfCustom() !FileClassifier.IsMicrosoftAssembly(_taskFactoryAssemblyLoadInfo.AssemblyName)) || (!string.IsNullOrEmpty(_taskFactoryAssemblyLoadInfo.AssemblyFile) && // This condition will as well capture Microsoft tasks pulled from NuGet cache - since we decide based on assembly name. - // Hence we do not have to add the 'IsMicrosoftPackageInNugetCache' call anywhere here + // Hence we do not have to add the 'IsMicrosoftPackageInNugetCache' call anywhere here !FileClassifier.IsMicrosoftAssembly(Path.GetFileName(_taskFactoryAssemblyLoadInfo.AssemblyFile)) && !FileClassifier.Shared.IsBuiltInLogic(_taskFactoryAssemblyLoadInfo.AssemblyFile))) // and let's consider all tasks imported by common targets as non custom logic. @@ -1477,7 +1469,7 @@ private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLo bool explicitlyLaunchTaskHost = isTaskHostFactory || ( - s_forceTaskHostLaunch && + Traits.Instance.ForceTaskHostLaunch && !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "MSBuild") && !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "CallTarget")); diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index da8da210b3c..5abfeae8c52 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -139,6 +139,14 @@ public Traits() public readonly bool InProcNodeDisabled = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; + /// + /// If true, we will force all tasks to run in the MSBuild task host EXCEPT + /// a small well-known set of tasks that are known to depend on IBuildEngine + /// callbacks; as forcing those out of proc would be just setting them up for + /// known failure. + /// + public readonly bool ForceTaskHostLaunch = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"; + /// /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. /// mirroring From f1ebda653c2d837513008e7ce15a75be96b54c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 3 Jun 2025 17:58:38 +0200 Subject: [PATCH 04/51] cleanup taskexecutionhost --- .../TaskExecutionHost/TaskExecutionHost.cs | 51 ++++++------------- src/Shared/AssemblyLoadInfo.cs | 2 + 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index f8c2bbfe6e6..40197e20b16 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -954,9 +954,8 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters else { // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances - bool shouldUseTaskHost = ShouldUseTaskHostForCustomFactory(); - if (shouldUseTaskHost) + if (Traits.Instance.ForceTaskHostLaunch) { // Create a TaskHostTask to run the custom factory's task out of process task = CreateTaskHostTaskForCustomFactory(taskIdentityParameters); @@ -1675,42 +1674,21 @@ private void DisplayCancelWaitMessage() } } - /// - /// Determines whether custom (non-AssemblyTaskFactory) task factories should use task host for out-of-process execution. - /// - /// True if tasks from custom factories should run out of process - private bool ShouldUseTaskHostForCustomFactory() - { - if (!Traits.Instance.ForceTaskHostLaunch) - { - return false; - } - - // Exclude well-known tasks that are known to depend on IBuildEngine callbacks - // as forcing those out of proc would set them up for known failure - if (TypeLoader.IsPartialTypeNameMatch(_taskName, "MSBuild") || - TypeLoader.IsPartialTypeNameMatch(_taskName, "CallTarget")) - { - return false; - } - - return true; - } - /// /// Creates a TaskHostTask wrapper to run a custom factory's task out of process. + /// This is used when Traits.Instance.ForceTaskHostLaunch is true to ensure + /// custom task factories's tasks run in isolation. /// - /// Task identity parameters - /// A TaskHostTask that will execute the real task out of process + /// Task identity parameters. No internal implementations support this + /// A TaskHostTask that will execute the inner task out of process, or null if task creation fails. private ITask CreateTaskHostTaskForCustomFactory(IDictionary taskIdentityParameters) { - // First, create the actual task using the custom factory TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); - ITask actualTask; + ITask innerTask; try { - actualTask = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? + innerTask = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); } @@ -1721,20 +1699,20 @@ private ITask CreateTaskHostTaskForCustomFactory(IDictionary tas #endif } - if (actualTask == null) + if (innerTask == null) { return null; } // Create a LoadedType for the actual task type so we can wrap it in TaskHostTask - Type taskType = actualTask.GetType(); + Type taskType = innerTask.GetType(); LoadedType taskLoadedType = new LoadedType( taskType, - AssemblyLoadInfo.Create(taskType.Assembly.FullName, taskType.Assembly.Location), + AssemblyLoadInfo.Create(null, taskType.Assembly.Location), taskType.Assembly, typeof(ITaskItem)); - // Create task host parameters for out-of-process execution + // Default task host parameters for out-of-process execution for inline tasks IDictionary taskHostParameters = new Dictionary { [XMakeAttributes.runtime] = XMakeAttributes.GetCurrentMSBuildRuntime(), @@ -1742,16 +1720,17 @@ private ITask CreateTaskHostTaskForCustomFactory(IDictionary tas }; // Merge with any existing task identity parameters - if (taskIdentityParameters != null) + if (taskIdentityParameters?.Count > 0) { - foreach (var kvp in taskIdentityParameters) + foreach (var kvp in taskIdentityParameters.Where(kvp => !taskHostParameters.ContainsKey(kvp.Key))) { taskHostParameters[kvp.Key] = kvp.Value; } } // Clean up the original task since we're going to wrap it - // _taskFactoryWrapper.TaskFactory.CleanupTask(actualTask); // Create and return the TaskHostTask wrapper + _taskFactoryWrapper.TaskFactory.CleanupTask(innerTask); + #pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter return new TaskHostTask( _taskLocation, diff --git a/src/Shared/AssemblyLoadInfo.cs b/src/Shared/AssemblyLoadInfo.cs index a17f3204fd5..be467ff2a20 100644 --- a/src/Shared/AssemblyLoadInfo.cs +++ b/src/Shared/AssemblyLoadInfo.cs @@ -26,6 +26,8 @@ internal static AssemblyLoadInfo Create(string assemblyName, string assemblyFile { ErrorUtilities.VerifyThrow((!string.IsNullOrEmpty(assemblyName)) || (!string.IsNullOrEmpty(assemblyFile)), "We must have either the assembly name or the assembly file/path."); + ErrorUtilities.VerifyThrow((assemblyName == null) || (assemblyFile == null), + "We must not have both the assembly name and the assembly file/path."); if (assemblyName != null) { From 7a7b77bbcd88b6b390fa74b555533fb37e2ab629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 3 Jun 2025 18:10:49 +0200 Subject: [PATCH 05/51] inline task cache per dll --- src/Build/BackEnd/BuildManager/BuildManager.cs | 6 +++++- .../RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index fd1f6024f3d..dbbfcfbc9df 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1160,7 +1160,11 @@ public void EndBuild() } // clean up inline tasks - FileUtilities.DeleteDirectoryNoThrow(Path.Combine(FileUtilities.TempFileDirectory, MSBuildConstants.InlineTaskTempDllSubPath), recursive: true); + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + FileUtilities.DeleteDirectoryNoThrow(processSpecificInlineTaskDir, recursive: true); void SerializeCaches() { diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index c37239666af..80228cc0ca5 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -683,7 +683,16 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT // The source code cannot actually be compiled "in memory" so instead the source code is written to disk in // the temp folder as well as the assembly. After build, the source code and assembly are deleted. string sourceCodePath = FileUtilities.GetTemporaryFileName(".tmp"); - assemblyPath = FileUtilities.GetTemporaryFile(Path.Combine(FileUtilities.TempFileDirectory, MSBuildConstants.InlineTaskTempDllSubPath), null, ".dll", false); + + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + + Directory.CreateDirectory(processSpecificInlineTaskDir); + + assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT // is set (which allows for debugging problems) From 9c5c01ecd1451ea71ab9eb5e9f6a1fd8cbe775c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 3 Jun 2025 18:33:36 +0200 Subject: [PATCH 06/51] undo unnecessary path caching --- .../RoslynCodeTaskFactory.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 80228cc0ca5..631f2a7ee5b 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -84,7 +84,7 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory /// A cache of objects and their corresponding compiled assembly. This cache ensures that two of the exact same code task /// declarations are not compiled multiple times. /// - private static readonly ConcurrentDictionary CompiledAssemblyCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary CompiledAssemblyCache = new ConcurrentDictionary(); /// /// Stores the path to the directory that this assembly is located in. @@ -162,7 +162,7 @@ public bool Initialize(string taskName, IDictionary pa } // Attempt to compile an assembly (or get one from the cache) - if (!TryCompileAssembly(taskFactoryLoggingHost, taskInfo, out string assemblyPath, out Assembly assembly)) + if (!TryCompileAssembly(taskFactoryLoggingHost, taskInfo, out Assembly assembly)) { return false; } @@ -659,19 +659,15 @@ private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDec /// /// An to use give to the compiler task so that messages can be logged. /// A object containing details about the task. - /// The path to a dll if the source code be compiled, otherwise null. /// The loaded assembly if compilation and loading succeeded, otherwise null. /// true if the source code could be compiled and loaded, otherwise false. - private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out string assemblyPath, out Assembly assembly) + private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) { assembly = null; - assemblyPath = null; // First attempt to get a compiled assembly from the cache - if (CompiledAssemblyCache.TryGetValue(taskInfo, out var cachedEntry)) + if (CompiledAssemblyCache.TryGetValue(taskInfo, out assembly)) { - assemblyPath = cachedEntry.Path; - assembly = cachedEntry.Assembly; return true; } @@ -691,7 +687,7 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT Directory.CreateDirectory(processSpecificInlineTaskDir); - assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + string assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT @@ -781,7 +777,7 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT return false; } - CompiledAssemblyCache.TryAdd(taskInfo, (assemblyPath, assembly)); + CompiledAssemblyCache.TryAdd(taskInfo, assembly); return true; } catch (Exception e) From 2bb19601b79b4e3746a4b00ecd2f0af01147a271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 3 Jun 2025 18:36:37 +0200 Subject: [PATCH 07/51] nit --- src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 631f2a7ee5b..a63a50bd746 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -770,12 +770,8 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT } } - // Load the compiled assembly + // Return the compiled assembly assembly = Assembly.LoadFrom(assemblyPath); - if (assembly == null) - { - return false; - } CompiledAssemblyCache.TryAdd(taskInfo, assembly); return true; From c76be284fc52b82ad7aa21dd339fb2b3693c3624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 9 Jun 2025 10:31:05 +0200 Subject: [PATCH 08/51] small fixes --- src/Build/BackEnd/BuildManager/BuildManager.cs | 16 ++++++++++------ .../TaskExecutionHost/TaskExecutionHost.cs | 2 +- src/Build/Instance/TaskRegistry.cs | 2 +- src/Framework/Traits.cs | 2 +- .../RoslynCodeTaskFactory.cs | 15 +++++++++++++-- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index dbbfcfbc9df..d8bb4e0c3b8 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1148,6 +1148,16 @@ public void EndBuild() Reset(); _buildManagerState = BuildManagerState.Idle; + if (Traits.Instance.ForceAllTasksOutOfProc) + { + // clean up inline tasks + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + FileUtilities.DeleteDirectoryNoThrow(processSpecificInlineTaskDir, recursive: true); + } + MSBuildEventSource.Log.BuildStop(); _threadException?.Throw(); @@ -1159,12 +1169,6 @@ public void EndBuild() } } - // clean up inline tasks - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - FileUtilities.DeleteDirectoryNoThrow(processSpecificInlineTaskDir, recursive: true); void SerializeCaches() { diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 40197e20b16..8f091506928 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -955,7 +955,7 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters { // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances - if (Traits.Instance.ForceTaskHostLaunch) + if (Traits.Instance.ForceAllTasksOutOfProc) { // Create a TaskHostTask to run the custom factory's task out of process task = CreateTaskHostTaskForCustomFactory(taskIdentityParameters); diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index 2e3433b7ccb..70568d46786 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -1469,7 +1469,7 @@ private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLo bool explicitlyLaunchTaskHost = isTaskHostFactory || ( - Traits.Instance.ForceTaskHostLaunch && + Traits.Instance.ForceAllTasksOutOfProc && !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "MSBuild") && !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "CallTarget")); diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 5abfeae8c52..8ea2720ef2e 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -145,7 +145,7 @@ public Traits() /// callbacks; as forcing those out of proc would be just setting them up for /// known failure. /// - public readonly bool ForceTaskHostLaunch = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"; + public readonly bool ForceAllTasksOutOfProc = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"; /// /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index a63a50bd746..df1f69ba886 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -659,7 +659,7 @@ private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDec /// /// An to use give to the compiler task so that messages can be logged. /// A object containing details about the task. - /// The loaded assembly if compilation and loading succeeded, otherwise null. + /// The if the source code be compiled and loaded, otherwise null. /// true if the source code could be compiled and loaded, otherwise false. private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) { @@ -771,7 +771,18 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT } // Return the compiled assembly - assembly = Assembly.LoadFrom(assemblyPath); + if (Traits.Instance.ForceAllTasksOutOfProc) + { + assembly = Assembly.LoadFrom(assemblyPath); + } + else + { + assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); + if (FileSystems.Default.FileExists(assemblyPath)) + { + File.Delete(assemblyPath); + } + } CompiledAssemblyCache.TryAdd(taskInfo, assembly); return true; From efc743e0ba8a7b33f4ab5426437f8816793f9c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 10 Jun 2025 11:50:28 +0200 Subject: [PATCH 09/51] refactor --- .../BackEnd/BuildManager/BuildManager.cs | 2 +- .../TaskExecutionHost/TaskExecutionHost.cs | 60 +++++++++---------- src/Build/Instance/TaskRegistry.cs | 10 +++- src/Framework/IOutOfProcTaskFactory.cs | 11 ++++ src/Framework/Traits.cs | 7 +-- src/Tasks/CodeTaskFactory.cs | 2 +- .../RoslynCodeTaskFactory.cs | 3 +- src/Tasks/XamlTaskFactory/XamlTaskFactory.cs | 5 +- 8 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 src/Framework/IOutOfProcTaskFactory.cs diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index d8bb4e0c3b8..1cd9e89768d 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1148,7 +1148,7 @@ public void EndBuild() Reset(); _buildManagerState = BuildManagerState.Idle; - if (Traits.Instance.ForceAllTasksOutOfProc) + if (Traits.Instance.ForceTaskFactoryOutOfProc) { // clean up inline tasks string processSpecificInlineTaskDir = Path.Combine( diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 8f091506928..90905920537 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -953,29 +953,38 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters } else { - // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances - - if (Traits.Instance.ForceAllTasksOutOfProc) - { - // Create a TaskHostTask to run the custom factory's task out of process - task = CreateTaskHostTaskForCustomFactory(taskIdentityParameters); - } - else + TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); + try { - // Normal in-process execution for custom task factories - TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); - try + // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + // Custom Task factories are not supported + if (_taskFactoryWrapper.TaskFactory is not IOutOfProcTaskFactory) + { + _taskLoggingContext.LogError( + new BuildEventFileInfo(_taskLocation), + "TaskInstantiationFailureErrorCustomFactoryNotSupported", + _taskName, + _taskFactoryWrapper.TaskFactory.FactoryName); + return null; + } + + task = CreateTaskHostTaskForOutOfProcFactory(taskIdentityParameters, loggingHost); + } + else { + // Normal in-process execution for custom task factories task = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); } - finally - { + } + finally + { #if FEATURE_APPDOMAIN - loggingHost.MarkAsInactive(); + loggingHost.MarkAsInactive(); #endif - } } } } @@ -1676,28 +1685,19 @@ private void DisplayCancelWaitMessage() /// /// Creates a TaskHostTask wrapper to run a custom factory's task out of process. - /// This is used when Traits.Instance.ForceTaskHostLaunch is true to ensure + /// This is used when Traits.Instance.ForceTaskFactoryOutOfProc is true to ensure /// custom task factories's tasks run in isolation. /// /// Task identity parameters. No internal implementations support this + /// The logging host to use for the task. /// A TaskHostTask that will execute the inner task out of process, or null if task creation fails. - private ITask CreateTaskHostTaskForCustomFactory(IDictionary taskIdentityParameters) + private ITask CreateTaskHostTaskForOutOfProcFactory(IDictionary taskIdentityParameters, TaskFactoryLoggingHost loggingHost) { - TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); ITask innerTask; - try - { - innerTask = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? - taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : - _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); - } - finally - { -#if FEATURE_APPDOMAIN - loggingHost.MarkAsInactive(); -#endif - } + innerTask = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? + taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : + _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); if (innerTask == null) { diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index 70568d46786..aec429e8334 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -60,6 +60,14 @@ internal sealed class TaskRegistry : ITranslatable /// private Toolset _toolset; + /// + /// If true, we will force all tasks to run in the MSBuild task host EXCEPT + /// a small well-known set of tasks that are known to depend on IBuildEngine + /// callbacks; as forcing those out of proc would be just setting them up for + /// known failure. + /// + private static readonly bool s_forceTaskHostLaunch = (Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"); + /// /// Simple name for the MSBuild tasks (v4), used for shimming in loading /// task factory UsingTasks @@ -1469,7 +1477,7 @@ private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLo bool explicitlyLaunchTaskHost = isTaskHostFactory || ( - Traits.Instance.ForceAllTasksOutOfProc && + s_forceTaskHostLaunch && !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "MSBuild") && !TypeLoader.IsPartialTypeNameMatch(RegisteredName, "CallTarget")); diff --git a/src/Framework/IOutOfProcTaskFactory.cs b/src/Framework/IOutOfProcTaskFactory.cs new file mode 100644 index 00000000000..590c70dc778 --- /dev/null +++ b/src/Framework/IOutOfProcTaskFactory.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.Framework; + +/// +/// Interface that a task factory Instance should implement if its tasks can run out of proc. +/// +internal interface IOutOfProcTaskFactory +{ +} diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 8ea2720ef2e..d51c7003956 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -140,12 +140,9 @@ public Traits() public readonly bool InProcNodeDisabled = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; /// - /// If true, we will force all tasks to run in the MSBuild task host EXCEPT - /// a small well-known set of tasks that are known to depend on IBuildEngine - /// callbacks; as forcing those out of proc would be just setting them up for - /// known failure. + /// Forces execution of tasks coming from a different TaskFactory than AssemblyTaskFactory out of proc. /// - public readonly bool ForceAllTasksOutOfProc = Environment.GetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC") == "1"; + public readonly bool ForceTaskFactoryOutOfProc = Environment.GetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC") == "1"; /// /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 203efd6b0d6..ffa08706371 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -27,7 +27,7 @@ namespace Microsoft.Build.Tasks /// /// A task factory which can take code dom supported languages and create a task out of it /// - public class CodeTaskFactory : ITaskFactory + public class CodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory { /// /// This dictionary keeps track of custom references to compiled assemblies. The in-memory assembly is loaded from a byte diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index df1f69ba886..e825c40b20a 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -22,7 +22,7 @@ namespace Microsoft.Build.Tasks { - public sealed class RoslynCodeTaskFactory : ITaskFactory + public sealed class RoslynCodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory { /// /// A set of default namespaces to add so that user does not have to include them. Make sure that these are covered @@ -689,7 +689,6 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT string assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); - // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT // is set (which allows for debugging problems) bool deleteSourceCodeFile = Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") == null; diff --git a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs index 18e0ce17d1d..54dacef6e97 100644 --- a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs +++ b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs @@ -24,7 +24,7 @@ namespace Microsoft.Build.Tasks /// /// The task factory provider for XAML tasks. /// - public class XamlTaskFactory : ITaskFactory + public class XamlTaskFactory : ITaskFactory, IOutOfProcTaskFactory { /// /// The namespace we put the task in. @@ -125,7 +125,8 @@ public bool Initialize(string taskName, IDictionary ta Path.Combine(pathToMSBuildBinaries, "Microsoft.Build.Tasks.Core.dll") ]) { - GenerateInMemory = true, + GenerateInMemory = false, + OutputAssembly = Path.Combine(Path.GetTempPath(), $"{taskName}_XamlTask.dll"), TreatWarningsAsErrors = false }; From 86c24fbfdc3443516d7816f503902ca8325e5dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 10 Jun 2025 13:21:09 +0200 Subject: [PATCH 10/51] other task factories --- src/Tasks/CodeTaskFactory.cs | 20 +++++++++++++++---- .../RoslynCodeTaskFactory.cs | 11 +++++----- src/Tasks/XamlTaskFactory/XamlTaskFactory.cs | 16 +++++++++++++-- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index ffa08706371..aea1c59df20 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -271,7 +271,7 @@ public bool Initialize(string taskName, IDictionary ta _taskParameterTypeInfo = taskParameters; - _compiledAssembly = CompileInMemoryAssembly(); + _compiledAssembly = CompileAssembly(); // If it wasn't compiled, it logged why. // If it was, continue. @@ -706,7 +706,7 @@ bool TryCacheAssemblyIdentityFromPath(string assemblyFile, out string candidateA /// Compile the assembly in memory and get a reference to the assembly itself. /// If compilation fails, returns null. /// - private Assembly CompileInMemoryAssembly() + private Assembly CompileAssembly() { // Combine our default assembly references with those specified var finalReferencedAssemblies = CombineReferencedAssemblies(); @@ -714,6 +714,18 @@ private Assembly CompileInMemoryAssembly() // Combine our default using's with those specified string[] finalUsingNamespaces = CombineUsingNamespaces(); + // for the out of proc execution + string taskAssemblyPath = null; + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + Directory.CreateDirectory(processSpecificInlineTaskDir); + taskAssemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + } + // Language can be anything that has a codedom provider, in the standard naming method // "c#;cs;csharp", "vb;vbs;visualbasic;vbscript", "js;jscript;javascript", "vj#;vjs;vjsharp", "c++;mc;cpp" using (CodeDomProvider provider = CodeDomProvider.CreateProvider(_language)) @@ -729,8 +741,8 @@ private Assembly CompileInMemoryAssembly() // We don't need debug information IncludeDebugInformation = true, - // Not a file based assembly - GenerateInMemory = true, + GenerateInMemory = !Traits.Instance.ForceTaskFactoryOutOfProc, + OutputAssembly = taskAssemblyPath, // Indicates that a .dll should be generated. GenerateExecutable = false diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index e825c40b20a..6892b7ad6a5 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -770,17 +770,13 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT } // Return the compiled assembly - if (Traits.Instance.ForceAllTasksOutOfProc) + if (Traits.Instance.ForceTaskFactoryOutOfProc) { assembly = Assembly.LoadFrom(assemblyPath); } else { assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); - if (FileSystems.Default.FileExists(assemblyPath)) - { - File.Delete(assemblyPath); - } } CompiledAssemblyCache.TryAdd(taskInfo, assembly); @@ -797,6 +793,11 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT { File.Delete(sourceCodePath); } + + if (!Traits.Instance.ForceTaskFactoryOutOfProc && FileSystems.Default.FileExists(assemblyPath)) + { + File.Delete(assemblyPath); + } } } } diff --git a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs index 54dacef6e97..7d359ef8598 100644 --- a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs +++ b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs @@ -115,6 +115,18 @@ public bool Initialize(string taskName, IDictionary ta // MSBuildToolsDirectoryRoot is the canonical location for MSBuild dll's. string pathToMSBuildBinaries = BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot; + // for the out of proc execution + string taskAssemblyPath = null; + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + Directory.CreateDirectory(processSpecificInlineTaskDir); + taskAssemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + } + // create the code generator options // Since we are running msbuild 12.0 these had better load. var compilerParameters = new CompilerParameters( @@ -125,8 +137,8 @@ public bool Initialize(string taskName, IDictionary ta Path.Combine(pathToMSBuildBinaries, "Microsoft.Build.Tasks.Core.dll") ]) { - GenerateInMemory = false, - OutputAssembly = Path.Combine(Path.GetTempPath(), $"{taskName}_XamlTask.dll"), + GenerateInMemory = !Traits.Instance.ForceTaskFactoryOutOfProc, + OutputAssembly = taskAssemblyPath, TreatWarningsAsErrors = false }; From ffa53f970c06c8eb3b2dca3121136c03cdd47730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 10 Jun 2025 14:41:45 +0200 Subject: [PATCH 11/51] add error for custom taskfactories, cleanup --- .../BackEnd/BuildManager/BuildManager.cs | 1 - .../TaskExecutionHost/TaskExecutionHost.cs | 8 +- src/Build/Resources/Strings.resx | 92 +++++++++---------- src/Build/Resources/xlf/Strings.cs.xlf | 5 + src/Build/Resources/xlf/Strings.de.xlf | 5 + src/Build/Resources/xlf/Strings.es.xlf | 5 + src/Build/Resources/xlf/Strings.fr.xlf | 5 + src/Build/Resources/xlf/Strings.it.xlf | 5 + src/Build/Resources/xlf/Strings.ja.xlf | 5 + src/Build/Resources/xlf/Strings.ko.xlf | 5 + src/Build/Resources/xlf/Strings.pl.xlf | 5 + src/Build/Resources/xlf/Strings.pt-BR.xlf | 5 + src/Build/Resources/xlf/Strings.ru.xlf | 5 + src/Build/Resources/xlf/Strings.tr.xlf | 5 + src/Build/Resources/xlf/Strings.zh-Hans.xlf | 5 + src/Build/Resources/xlf/Strings.zh-Hant.xlf | 5 + .../RoslynCodeTaskFactory.cs | 2 - 17 files changed, 113 insertions(+), 55 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 1cd9e89768d..509e049f4ac 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1169,7 +1169,6 @@ public void EndBuild() } } - void SerializeCaches() { string errorMessage = CacheSerialization.SerializeCaches( diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 90905920537..0a011fd78ba 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -959,14 +959,14 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances if (Traits.Instance.ForceTaskFactoryOutOfProc) { - // Custom Task factories are not supported + // Custom Task factories are not supported, internal TaskFactories implement this marker interface if (_taskFactoryWrapper.TaskFactory is not IOutOfProcTaskFactory) { _taskLoggingContext.LogError( new BuildEventFileInfo(_taskLocation), - "TaskInstantiationFailureErrorCustomFactoryNotSupported", - _taskName, - _taskFactoryWrapper.TaskFactory.FactoryName); + "CustomTaskFactoryOutOfProcNotSupported", + _taskFactoryWrapper.TaskFactory.FactoryName, + _taskName); return null; } diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 71a53635fdd..64d77f47226 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -145,7 +145,7 @@ The operation cannot be completed because EndBuild has already been called but existing submissions have not yet completed. - + Property '{0}' with value '{1}' expanded from the environment. @@ -479,7 +479,7 @@ likely because of a programming error in the logger). When a logger dies, we cannot proceed with the build, and we throw a special exception to abort the build. - + MSB3094: "{2}" refers to {0} item(s), and "{3}" refers to {1} item(s). They must have the same number of items. {StrBegin="MSB3094: "} @@ -604,7 +604,7 @@ LOCALIZATION: "{0}" is the expression that was bad. "{1}" is a message from an FX exception that describes why the expression is bad. - + Found multiple overloads for method "{0}" with {1} parameter(s). That is currently not supported. @@ -1171,7 +1171,7 @@ LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. Also, Microsoft.Build.Framework should not be localized - + MSB4181: The "{0}" task returned false but did not log an error. {StrBegin="MSB4181: "} @@ -1301,7 +1301,7 @@ MSB4067: The element <{0}> beneath element <{1}> is unrecognized. {StrBegin="MSB4067: "} - + MSB4067: The element <{0}> beneath element <{1}> is unrecognized. If you intended this to be a property, enclose it within a <PropertyGroup> element. {StrBegin="MSB4067: "} @@ -1761,7 +1761,7 @@ Utilization: {0} Average Utilization: {1:###.0} MSB4231: ProjectRootElement can't reload if it contains unsaved changes. {StrBegin="MSB4231: "} - + The parameters have been truncated beyond this point. To view all parameters, clear the MSBUILDTRUNCATETASKINPUTLOGGING environment variable. @@ -1989,9 +1989,9 @@ Utilization: {0} Average Utilization: {1:###.0} - {0} -> Cache Hit + {0} -> Cache Hit - {StrBegin="{0} -> "}LOCALIZATION: This string is used to indicate progress and matches the format for a log message from Microsoft.Common.CurrentVersion.targets. {0} is a project name. + {StrBegin="{0} -> "}LOCALIZATION: This string is used to indicate progress and matches the format for a log message from Microsoft.Common.CurrentVersion.targets. {0} is a project name. @@ -2060,7 +2060,7 @@ Utilization: {0} Average Utilization: {1:###.0} Imported files archive exceeded 2GB limit and it's not embedded. - Forward compatible reading is not supported for file format version {0} (needs >= 18). + Forward compatible reading is not supported for file format version {0} (needs >= 18). LOCALIZATION: {0} is an integer number denoting version. @@ -2179,19 +2179,19 @@ Utilization: {0} Average Utilization: {1:###.0} It is recommended to specify explicit 'Culture' metadata, or 'WithCulture=false' metadata with 'EmbeddedResource' item in order to avoid wrong or nondeterministic culture estimation. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Project {0} specifies 'EmbeddedResource' item '{1}', that has possibly a culture denoting extension ('{2}'), but explicit 'Culture' nor 'WithCulture=false' metadata are not specified. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Avoid specifying 'Always' for 'CopyToOutputDirectory' as this can lead to unnecessary copy operations during build. Use 'PreserveNewest' or 'IfDifferent' metadata value, or set the 'SkipUnchangedFilesOnCopyAlways' property to true to employ more effective copying. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Project {0} specifies '{1}' item '{2}', that has 'CopyToOutputDirectory' set as 'Always'. Change the metadata or use 'CopyToOutputDirectory' property. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. 'TargetFramework' (singular) and 'TargetFrameworks' (plural) properties should not be specified in the scripts at the same time. @@ -2398,11 +2398,7 @@ Utilization: {0} Average Utilization: {1:###.0} Loading telemetry libraries failed with exception: {0}. - - + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + \ No newline at end of file diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 03650f88f89..dbd2a3b7936 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -446,6 +446,11 @@ Pravidlo vlastní kontroly: {0} se úspěšně zaregistrovalo. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: Výchozí překladač sady SDK nedokázal přeložit sadu SDK „{0}“, protože adresář „{1}“ neexistoval. diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 2ad593ea66a..29a7137a0f1 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -446,6 +446,11 @@ Die benutzerdefinierte Prüfregel „{0}“ wurde erfolgreich registriert. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: Der SDK-Standardresolver konnte SDK "{0}" nicht auflösen, da das Verzeichnis "{1}" nicht vorhanden war. diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index b4584d786bf..51ec206fd6f 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -446,6 +446,11 @@ La regla de comprobación personalizada: "{0}" se ha registrado correctamente. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: La resolución predeterminada del SDK no pudo resolver el SDK "{0}" porque el directorio "{1}" no existía. diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index 909a507f566..4c2bb805a39 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -446,6 +446,11 @@ Règle de vérification personnalisée : «{0}» a été correctement inscrite. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: Le programme de résolution du SDK par défaut n’a pas pu résoudre le SDK «{0}», car le répertoire «{1}» n’existait pas. diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 44e0f02b051..527dda01f71 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -446,6 +446,11 @@ La regola di controllo personalizzata “{0}” è stata registrata correttamente. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: il resolver SDK predefinito non è riuscito a risolvere l'SDK "{0}" perché la directory "{1}" non esiste. diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index fa181c689ea..1c6420af0c7 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -446,6 +446,11 @@ カスタム チェック ルール: '{0}' が正常に登録されました。 The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: ディレクトリ "{0}" が存在しなかったため、既定の SDK リゾルバーは SDK "{1}" を解決できませんでした。 diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index f5766935dde..eab88c11640 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -446,6 +446,11 @@ 사용자 지정 검사 규칙 '{0}'이(가) 등록되었습니다. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: 디렉터리 "{0}"이(가) 없으므로 기본 SDK 확인자가 SDK "{1}"을(를) 확인하지 못했습니다. diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index a218f56d244..2623d36b580 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -446,6 +446,11 @@ Niestandardowa reguła kontrolera: „{0}” została pomyślnie zarejestrowana. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: Domyślne narzędzie Resolver zestawu SDK nie może rozpoznać zestawu SDK „{0}”, ponieważ katalog „{1}” nie istnieje. diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index bde8513a69a..571b4877072 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -446,6 +446,11 @@ Regra de verificação personalizada: '{0}' foi registrada com sucesso. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: O resolvedor SDK padrão falhou ao resolver SDK "{0}" porque o diretório "{1}" não existia. diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 34b7d9d8b23..04a580dbbf8 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -446,6 +446,11 @@ Правило настраиваемой проверки "{0}" успешно зарегистрировано. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: стандартному сопоставителю пакетов SDK не удалось разрешить пакет SDK "{0}", так как каталог "{1}" не существует. diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 004fafbaa16..2554906a57e 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -446,6 +446,11 @@ ‘{0}’ özel denetim kuralı başarıyla kaydedildi. The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: "{1}" dizini olmadığından, varsayılan SDK çözümleyicisi "{0}" SDK’sını çözümleyemedi. diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index 13047d4ac40..6be32690d9d 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -446,6 +446,11 @@ 已成功注册自定义检查规则“{0}”。 The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: 默认 SDK 解析程序解析 SDK“{0}”失败,因为目录“{1}”不存在。 diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index c4be1e9169a..dc6fbe9192f 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -446,6 +446,11 @@ 已成功註冊自訂檢查規則: '{0}'。 The message is emitted on successful loading of the custom check rule in process. + + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + + MSB4276: The default SDK resolver failed to resolve SDK "{0}" because directory "{1}" did not exist. MSB4276: 預設的 SDK 解析程式無法解析 SDK "{0}",因為目錄 "{1}" 不存在。 diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 6892b7ad6a5..605fc76373e 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -663,8 +663,6 @@ private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDec /// true if the source code could be compiled and loaded, otherwise false. private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) { - assembly = null; - // First attempt to get a compiled assembly from the cache if (CompiledAssemblyCache.TryGetValue(taskInfo, out assembly)) { From e6f35e60046013ef30b7a5f9f5b09b5bab2f8589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 10 Jun 2025 15:09:47 +0200 Subject: [PATCH 12/51] docstring --- src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index 0a011fd78ba..bddc2c929f8 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -1684,13 +1684,13 @@ private void DisplayCancelWaitMessage() } /// - /// Creates a TaskHostTask wrapper to run a custom factory's task out of process. + /// Creates a wrapper to run a non-AssemblyTaskFactory task out of process. /// This is used when Traits.Instance.ForceTaskFactoryOutOfProc is true to ensure - /// custom task factories's tasks run in isolation. + /// non-AssemblyTaskFactory tasks run in isolation. /// - /// Task identity parameters. No internal implementations support this + /// Task identity parameters. /// The logging host to use for the task. - /// A TaskHostTask that will execute the inner task out of process, or null if task creation fails. + /// A TaskHostTask that will execute the inner task out of process, or null if task creation fails. private ITask CreateTaskHostTaskForOutOfProcFactory(IDictionary taskIdentityParameters, TaskFactoryLoggingHost loggingHost) { ITask innerTask; From 26b2a3e7b6c2a6c7282d7bfca47964fa1b5a97f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 10 Jun 2025 18:41:09 +0200 Subject: [PATCH 13/51] roslyncodetaskfactory outofproc tests, fix excluding intrinsictaskfactory --- .../TaskExecutionHost/TaskExecutionHost.cs | 3 +- .../RoslynCodeTaskFactory_Tests.cs | 53 +++++++++++++++---- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index bddc2c929f8..725ff0d2f28 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -957,7 +957,8 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters try { // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances - if (Traits.Instance.ForceTaskFactoryOutOfProc) + // IntrinsicTaskFactory tasks run in proc always + if (Traits.Instance.ForceTaskFactoryOutOfProc && _taskFactoryWrapper.TaskFactory is not IntrinsicTaskFactory) { // Custom Task factories are not supported, internal TaskFactories implement this marker interface if (_taskFactoryWrapper.TaskFactory is not IOutOfProcTaskFactory) diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs index e1867c0129a..595f0d27486 100644 --- a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs @@ -41,11 +41,18 @@ public RoslynCodeTaskFactory_Tests() _verifySettings.ScrubLinesContaining("Runtime Version:"); } - [Fact] - public void InlineTaskWithAssemblyPlatformAgnostic() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void InlineTaskWithAssemblyPlatformAgnostic(bool forceOutOfProc) { using (TestEnvironment env = TestEnvironment.Create()) { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + TransientTestFolder folder = env.CreateFolder(createFolder: true); string location = Assembly.GetExecutingAssembly().Location; TransientTestFile inlineTask = env.CreateFile(folder, "5106.proj", @$" @@ -81,12 +88,19 @@ public void InlineTaskWithAssemblyPlatformAgnostic() } } - [Fact] + [Theory] + [InlineData(false)] + [InlineData(true)] [SkipOnPlatform(TestPlatforms.AnyUnix, ".NETFramework 4.0 isn't on unix machines.")] - public void InlineTaskWithAssembly() + public void InlineTaskWithAssembly(bool forceOutOfProc) { using (TestEnvironment env = TestEnvironment.Create()) { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + TransientTestFolder folder = env.CreateFolder(createFolder: true); TransientTestFile assemblyProj = env.CreateFile(folder, "5106.csproj", @$" @@ -143,8 +157,10 @@ public static string ToPrint() { } } - [Fact] - public void RoslynCodeTaskFactory_ReuseCompilation() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) { string text1 = $@" @@ -198,6 +214,10 @@ public void RoslynCodeTaskFactory_ReuseCompilation() "; using var env = TestEnvironment.Create(); + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } var p2 = env.CreateTestProjectWithFiles("p2.proj", text2); text1 = text1.Replace("p2.proj", p2.ProjectFile); @@ -605,8 +625,10 @@ public void SourceCodeFromFile() } } - [Fact] - public void MismatchedTaskNameAndTaskClassName() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MismatchedTaskNameAndTaskClassName(bool forceOutOfProc) { const string taskName = "SayHello"; const string className = "HelloWorld"; @@ -640,6 +662,10 @@ public override bool Execute() using (TestEnvironment env = TestEnvironment.Create()) { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } TransientTestProjectWithFiles proj = env.CreateTestProjectWithFiles(projectContent); var logger = proj.BuildProjectExpectFailure(); logger.AssertLogContains(errorMessage); @@ -722,8 +748,10 @@ public void EmbedsGeneratedFileInBinlogWhenFailsToCompile() } #if !FEATURE_RUN_EXE_IN_TESTS - [Fact] - public void RoslynCodeTaskFactory_UsingAPI() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RoslynCodeTaskFactory_UsingAPI(bool forceOutOfProc) { string text = $@" @@ -752,6 +780,11 @@ public void RoslynCodeTaskFactory_UsingAPI() "; using var env = TestEnvironment.Create(); + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + RunnerUtilities.ApplyDotnetHostPathEnvironmentVariable(env); var dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); From 2e10e3e3a95909e61c4ef71eae55f5dc6500f3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 11 Jun 2025 16:46:21 +0200 Subject: [PATCH 14/51] hacks --- src/MSBuild/OutOfProcTaskHostNode.cs | 30 ++++++++++++++ .../RoslynCodeTaskFactory.cs | 39 ++++++++++++------- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 9b670a086d4..2a9e325c1cf 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -930,6 +930,36 @@ private void RunTask(object state) string taskName = taskConfiguration.TaskName; string taskLocation = taskConfiguration.TaskLocation; + string[] directoriesToAddToAppDomain; + // if the tasklocation ends with inline_task.dll + if (taskLocation.EndsWith("inline_task.dll", StringComparison.OrdinalIgnoreCase)) + { + // load manifest of the task assembly + string manifestPath = taskLocation + ".loadmanifest"; + // read all lines to list if it exists + if (File.Exists(manifestPath)) + { + directoriesToAddToAppDomain = File.ReadAllLines(manifestPath); + if (directoriesToAddToAppDomain != null) + { + // let's do the appdomain magic here... + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + var assemblyName = new AssemblyName(args.Name); + foreach (string directory in directoriesToAddToAppDomain) + { + string assemblyPath = Path.Combine(directory, assemblyName.Name + ".dll"); + if (File.Exists(assemblyPath)) + { + return Assembly.LoadFrom(assemblyPath); + } + } + + return null; + }; + } + } + } // We will not create an appdomain now because of a bug // As a fix, we will create the class directly without wrapping it in a domain diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 605fc76373e..f255d1c94f2 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -96,6 +96,8 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory /// private TaskLoggingHelper _log; + private string _assemblyPath; + /// /// Stores functions that were added to the current app domain. Should be removed once we're finished. /// @@ -588,6 +590,15 @@ internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTask handlerAddedToAppDomain = (_, eventArgs) => TryLoadAssembly(directoriesToAddToAppDomain, new AssemblyName(eventArgs.Name)); AppDomain.CurrentDomain.AssemblyResolve += handlerAddedToAppDomain; + // in case of taskhost we cache the resolution to a file placed next to the task assembly + // so the taskhost can recreate this tryloadassembly logic + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + // simply serialize the directories to a file next to the task assembly + string pth = _assemblyPath + ".loadmanifest"; + File.WriteAllLines(pth, directoriesToAddToAppDomain); + } + return !hasInvalidReference; static Assembly TryLoadAssembly(List directories, AssemblyName name) @@ -669,6 +680,15 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT return true; } + string _taskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + + Directory.CreateDirectory(_taskDir); + + _assemblyPath = FileUtilities.GetTemporaryFile(_taskDir, null, "inline_task.dll", false); + if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references)) { return false; @@ -678,15 +698,6 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT // the temp folder as well as the assembly. After build, the source code and assembly are deleted. string sourceCodePath = FileUtilities.GetTemporaryFileName(".tmp"); - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - - Directory.CreateDirectory(processSpecificInlineTaskDir); - - string assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); - // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT // is set (which allows for debugging problems) bool deleteSourceCodeFile = Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") == null; @@ -743,7 +754,7 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT managedCompiler.NoConfig = true; managedCompiler.NoLogo = true; managedCompiler.Optimize = false; - managedCompiler.OutputAssembly = new TaskItem(assemblyPath); + managedCompiler.OutputAssembly = new TaskItem(_assemblyPath); managedCompiler.References = references; managedCompiler.Sources = [new TaskItem(sourceCodePath)]; managedCompiler.TargetType = "Library"; @@ -770,11 +781,11 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT // Return the compiled assembly if (Traits.Instance.ForceTaskFactoryOutOfProc) { - assembly = Assembly.LoadFrom(assemblyPath); + assembly = Assembly.LoadFrom(_assemblyPath); } else { - assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); + assembly = Assembly.Load(File.ReadAllBytes(_assemblyPath)); } CompiledAssemblyCache.TryAdd(taskInfo, assembly); @@ -792,9 +803,9 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT File.Delete(sourceCodePath); } - if (!Traits.Instance.ForceTaskFactoryOutOfProc && FileSystems.Default.FileExists(assemblyPath)) + if (!Traits.Instance.ForceTaskFactoryOutOfProc && FileSystems.Default.FileExists(_assemblyPath)) { - File.Delete(assemblyPath); + File.Delete(_assemblyPath); } } } From 8edeef3174103e2d8bfb3cf1d05f8c552e9e8e8b Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Fri, 6 Jun 2025 03:12:51 -0500 Subject: [PATCH 15/51] Support testhost.exe in TaskHost-launching tests (#11956) Some tests were failing when run from Visual Studio because they were running in a .NET 9 process launched as `TestHost.exe`, not in a `dotnet.exe` host. That caused TaskHost launching to try to run `TestHost.exe MSBuild.dll` which failed, as TestHost isn't a general-purpose launcher. Add another fallback to `GetCurrentHost` to fall back to the current shared framework's parent `dotnet.exe`, if we can find it. --- .../Components/Communications/CurrentHost.cs | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/CurrentHost.cs b/src/Build/BackEnd/Components/Communications/CurrentHost.cs index 6e08f93e955..fa29bf68aa9 100644 --- a/src/Build/BackEnd/Components/Communications/CurrentHost.cs +++ b/src/Build/BackEnd/Components/Communications/CurrentHost.cs @@ -6,42 +6,62 @@ using Microsoft.Build.Shared; #endif -#nullable disable +namespace Microsoft.Build.BackEnd; -namespace Microsoft.Build.BackEnd +internal static class CurrentHost { - internal static class CurrentHost - { #if RUNTIME_TYPE_NETCORE - private static string s_currentHost; + private static string? s_currentHost; #endif - /// - /// Identify the .NET host of the current process. - /// - /// The full path to the executable hosting the current process, or null if running on Full Framework on Windows. - public static string GetCurrentHost() - { + /// + /// Identify the .NET host of the current process. + /// + /// The full path to the executable hosting the current process, or null if running on Full Framework on Windows. + public static string? GetCurrentHost() + { #if RUNTIME_TYPE_NETCORE - if (s_currentHost == null) + if (s_currentHost == null) + { + string dotnetExeName = NativeMethodsShared.IsWindows ? "dotnet.exe" : "dotnet"; + + string dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, 2), + dotnetExeName); + if (File.Exists(dotnetExe)) { - string dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, 2), - NativeMethodsShared.IsWindows ? "dotnet.exe" : "dotnet"); - if (File.Exists(dotnetExe)) + s_currentHost = dotnetExe; + } + else + { + if (EnvironmentUtilities.ProcessPath is string processPath && + Path.GetFileName(processPath) == dotnetExeName) { - s_currentHost = dotnetExe; + // If the current process is already running in a general-purpose host, use its path. + s_currentHost = processPath; } else { - s_currentHost = EnvironmentUtilities.ProcessPath; + // Otherwise, we don't know the host. Try to infer it from the current runtime, which will be something like + // "C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.6\" on Windows. + // ^4 ^3 ^2 ^1 + dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(), 4), + dotnetExeName); + if (File.Exists(dotnetExe)) + { + s_currentHost = dotnetExe; + } + else + { + ErrorUtilities.ThrowInternalErrorUnreachable(); + } } } + } - return s_currentHost; + return s_currentHost; #else - return null; + return null; #endif - } } } From f83e5beca036e1d3b3ae6abc985b28d336c31553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 12 Jun 2025 12:04:21 +0200 Subject: [PATCH 16/51] fix test? --- src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs | 1 + src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs index 595f0d27486..1e4a1d98965 100644 --- a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs @@ -162,6 +162,7 @@ public static string ToPrint() { [InlineData(true)] public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) { + RoslynCodeTaskFactory.ClearAssemblyCache(); string text1 = $@" diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index f255d1c94f2..42a4a7cd7ae 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -86,6 +86,9 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory /// private static readonly ConcurrentDictionary CompiledAssemblyCache = new ConcurrentDictionary(); + // for tests + internal static void ClearAssemblyCache() => CompiledAssemblyCache.Clear(); + /// /// Stores the path to the directory that this assembly is located in. /// From 56baac46e68814e332b5da38aa8e88a08f11f2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 12 Jun 2025 15:01:18 +0200 Subject: [PATCH 17/51] test? --- src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs index 1e4a1d98965..af80f2adf8c 100644 --- a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs @@ -754,6 +754,7 @@ public void EmbedsGeneratedFileInBinlogWhenFailsToCompile() [InlineData(true)] public void RoslynCodeTaskFactory_UsingAPI(bool forceOutOfProc) { + RoslynCodeTaskFactory.ClearAssemblyCache(); string text = $@" From 33fbac68ac9d16337c08d009192555ca7220f453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 12 Jun 2025 16:03:44 +0200 Subject: [PATCH 18/51] remove nonsense --- .../RoslynCodeTaskFactory_Tests.cs | 22 +++++++++---------- .../RoslynCodeTaskFactory.cs | 3 --- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs index af80f2adf8c..6462a5ad8e0 100644 --- a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs @@ -162,12 +162,12 @@ public static string ToPrint() { [InlineData(true)] public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) { - RoslynCodeTaskFactory.ClearAssemblyCache(); + int num = forceOutOfProc ? 1 : 2; string text1 = $@" @@ -183,8 +183,8 @@ public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) - - + + "; @@ -193,7 +193,7 @@ public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) @@ -208,8 +208,8 @@ public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) - - + + "; @@ -229,7 +229,7 @@ public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) .BuildMessageEvents .Where(m => m.Message == "Compiling task source code") .ToArray(); - + // with broken cache we get two Compiling messages // as we fail to reuse the first assembly messages.Length.ShouldBe(1); @@ -754,12 +754,12 @@ public void EmbedsGeneratedFileInBinlogWhenFailsToCompile() [InlineData(true)] public void RoslynCodeTaskFactory_UsingAPI(bool forceOutOfProc) { - RoslynCodeTaskFactory.ClearAssemblyCache(); + int num = forceOutOfProc ? 3 : 4; string text = $@" @@ -776,7 +776,7 @@ public void RoslynCodeTaskFactory_UsingAPI(bool forceOutOfProc) - + "; diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 42a4a7cd7ae..f255d1c94f2 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -86,9 +86,6 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory, IOutOfProcTaskFactory /// private static readonly ConcurrentDictionary CompiledAssemblyCache = new ConcurrentDictionary(); - // for tests - internal static void ClearAssemblyCache() => CompiledAssemblyCache.Clear(); - /// /// Stores the path to the directory that this assembly is located in. /// From b8f8544d49b4c05c55ea093d188ee65cd5683a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Fri, 13 Jun 2025 13:59:12 +0200 Subject: [PATCH 19/51] codetaskfactory adjustment --- src/Tasks.UnitTests/CodeTaskFactoryTests.cs | 589 ++++++++++++-------- src/Tasks/CodeTaskFactory.cs | 75 ++- 2 files changed, 433 insertions(+), 231 deletions(-) diff --git a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs index 62d695d5632..4376bf8addb 100644 --- a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs +++ b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs @@ -23,28 +23,39 @@ public sealed class CodeTaskFactoryTests /// /// Test the simple case where we have a string parameter and we want to log that. /// - [Fact] - public void BuildTaskSimpleCodeFactory() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskSimpleCodeFactory(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - Log.LogMessage(MessageImportance.High, Text); - - - - - - - "; + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("Hello, World!"); + string projectFileContents = $@" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + } } /// @@ -53,29 +64,40 @@ public void BuildTaskSimpleCodeFactory() /// Microsoft.Build.Tasks.v4.0.dll is expected to NOT be in MSBuildToolsPath, that /// we will redirect under the covers to use the current tasks instead. /// - [Fact] - public void BuildTaskSimpleCodeFactory_RedirectFrom4() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskSimpleCodeFactory_RedirectFrom4(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - Log.LogMessage(MessageImportance.High, Text); - - - - - - - "; + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("Hello, World!"); - mockLogger.AssertLogDoesntContain("Microsoft.Build.Tasks.v4.0.dll"); + string projectFileContents = $@" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + mockLogger.AssertLogDoesntContain("Microsoft.Build.Tasks.v4.0.dll"); + } } /// @@ -84,29 +106,40 @@ public void BuildTaskSimpleCodeFactory_RedirectFrom4() /// Microsoft.Build.Tasks.v12.0.dll is expected to NOT be in MSBuildToolsPath, that /// we will redirect under the covers to use the current tasks instead. /// - [Fact] - public void BuildTaskSimpleCodeFactory_RedirectFrom12() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskSimpleCodeFactory_RedirectFrom12(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - Log.LogMessage(MessageImportance.High, Text); - - - - - - - "; + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("Hello, World!"); - mockLogger.AssertLogDoesntContain("Microsoft.Build.Tasks.v12.0.dll"); + string projectFileContents = $@" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + mockLogger.AssertLogDoesntContain("Microsoft.Build.Tasks.v12.0.dll"); + } } /// @@ -116,29 +149,40 @@ public void BuildTaskSimpleCodeFactory_RedirectFrom12() /// being in MSBuildToolsPath anymore, that this does NOT affect full fusion AssemblyNames -- /// it's picked up from the GAC, where it is anyway, so there's no need to redirect. /// - [Fact] - public void BuildTaskSimpleCodeFactory_NoAssemblyNameRedirect() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskSimpleCodeFactory_NoAssemblyNameRedirect(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - Log.LogMessage(MessageImportance.High, Text); - - - - - - - "; + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("Hello, World!"); - mockLogger.AssertLogContains("Microsoft.Build.Tasks.Core, Version=15.1.0.0"); + string projectFileContents = $@" + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + mockLogger.AssertLogContains("Microsoft.Build.Tasks.Core, Version=15.1.0.0"); + } } /// @@ -483,62 +527,84 @@ public void MissingCodeElement() /// /// Test the case where we have adding a using statement /// - [Fact] - public void BuildTaskSimpleCodeFactoryTestExtraUsing() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskSimpleCodeFactoryTestExtraUsing(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - - string linqString = ExpressionType.Add.ToString(); - Log.LogMessage(MessageImportance.High, linqString + Text); - - - - - - - "; + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - string linqString = nameof(System.Linq.Expressions.ExpressionType.Add); - mockLogger.AssertLogContains(linqString + ":Hello, World!"); + string projectFileContents = $@" + + + + + + + + + string linqString = ExpressionType.Add.ToString(); + Log.LogMessage(MessageImportance.High, linqString + Text); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + string linqString = nameof(System.Linq.Expressions.ExpressionType.Add); + mockLogger.AssertLogContains(linqString + ":Hello, World!"); + } } /// /// Verify setting the output tag on the parameter causes it to be an output from the perspective of the targets /// - [Fact] - public void BuildTaskDateCodeFactory() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskDateCodeFactory(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - CurrentDate = DateTime.Now.ToString(); - - - - - - - - - - "; + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("Current Date and Time:"); - mockLogger.AssertLogDoesntContain("[[]]"); + string projectFileContents = $@" + + + + + + + + CurrentDate = DateTime.Now.ToString(); + + + + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Current Date and Time:"); + mockLogger.AssertLogDoesntContain("[[]]"); + } } /// @@ -634,46 +700,56 @@ public void BuildTaskSimpleCodeFactoryTestSystemCS() /// Make sure we can pass in extra references than the automatic ones. For example the c# compiler does not pass in /// system.dll. So lets test that case /// - [Fact] - public void BuildTaskSimpleCodeFactoryTestExtraReferenceCS() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildTaskSimpleCodeFactoryTestExtraReference(bool forceOutOfProc) { - string netFrameworkDirectory = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45); - if (netFrameworkDirectory == null) + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) { - // "CouldNotFindRequiredTestDirectory" - return; - } + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + string netFrameworkDirectory = ToolLocationHelper.GetPathToDotNetFrameworkReferenceAssemblies(TargetDotNetFrameworkVersion.Version45); + if (netFrameworkDirectory == null) + { + // "CouldNotFindRequiredTestDirectory" + return; + } - string systemNETLocation = Path.Combine(netFrameworkDirectory, "System.Net.dll"); + string systemNETLocation = Path.Combine(netFrameworkDirectory, "System.Net.dll"); - if (!File.Exists(systemNETLocation)) - { - // "CouldNotFindRequiredTestFile" - return; - } + if (!File.Exists(systemNETLocation)) + { + // "CouldNotFindRequiredTestFile" + return; + } - string projectFileContents = @" - - - - - - - - - - string netString = System.Net.HttpStatusCode.OK.ToString(); - Log.LogMessage(MessageImportance.High, netString + Text); - - - - - - - "; + string projectFileContents = $@" + + + + + + + + + + string netString = System.Net.HttpStatusCode.OK.ToString(); + Log.LogMessage(MessageImportance.High, netString + Text); + + + + + + + "; - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("OK" + ":Hello, World!"); + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("OK" + ":Hello, World!"); + } } /// @@ -1099,80 +1175,135 @@ public void BuildTaskSimpleCodeFactoryTempDirectoryDoesntExist() /// /// Test the simple case where we have a string parameter and we want to log that. /// - [Fact] - public void RedundantMSBuildReferences() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RedundantMSBuildReferences(bool forceOutOfProc) { - string projectFileContents = @" - - - - - - - - + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - - Log.LogMessage(MessageImportance.High, Text); - - - - - - - "; + string projectFileContents = $@" + + + + + + + + + + + Log.LogMessage(MessageImportance.High, Text); + + + + + + + "; - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("Hello, World!"); + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("Hello, World!"); + } } - [Fact] - public void EmbedsGeneratedFromSourceFileInBinlog() + /// + /// Verify that the generated code from source is embedded in the binlog + /// + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EmbedsGeneratedFromSourceFileInBinlog(bool forceOutOfProc) { - string taskName = "HelloTask"; - - string sourceContent = $$""" - namespace InlineTask + int num = forceOutOfProc ? 1 : 0; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) { - using Microsoft.Build.Utilities; + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + + string taskName = $"HelloTask{num}"; - public class {{taskName}} : Task + string sourceContent = $$""" + namespace InlineTask { - public override bool Execute() + using Microsoft.Build.Utilities; + + public class {{taskName}} : Task { - Log.LogMessage("Hello, world!"); - return !Log.HasLoggedErrors; + public override bool Execute() + { + Log.LogMessage("Hello, world!"); + return !Log.HasLoggedErrors; + } } } - } - """; + """; - CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildFromSourceAndCheckForEmbeddedFileInBinlog( - FactoryType.CodeTaskFactory, taskName, sourceContent, true); + CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildFromSourceAndCheckForEmbeddedFileInBinlog( + FactoryType.CodeTaskFactory, taskName, sourceContent, true); + } } - [Fact] - public void EmbedsGeneratedFromSourceFileInBinlogWhenFailsToCompile() + /// + /// Verify that the generated code from source is embedded in the binlog even when it fails to compile + /// + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EmbedsGeneratedFromSourceFileInBinlogWhenFailsToCompile(bool forceOutOfProc) { - string taskName = "HelloTask"; - - string sourceContent = $$""" - namespace InlineTask + int num = forceOutOfProc ? 3 : 2; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) { - using Microsoft.Build.Utilities; + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } - public class {{taskName}} : Task + string taskName = $"HelloTask{num}"; + + string sourceContent = $$""" + namespace InlineTask { + using Microsoft.Build.Utilities; + + public class {{taskName}} : Task + { """; - CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildFromSourceAndCheckForEmbeddedFileInBinlog( - FactoryType.CodeTaskFactory, taskName, sourceContent, false); + CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildFromSourceAndCheckForEmbeddedFileInBinlog( + FactoryType.CodeTaskFactory, taskName, sourceContent, false); + } } - [Fact] - public void EmbedsGeneratedFileInBinlog() + /// + /// Verify that the generated code is embedded in the binlog + /// + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EmbedsGeneratedFileInBinlog(bool forceOutOfProc) { - string taskXml = @" + int num = forceOutOfProc ? 5 : 4; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + + string taskXml = @" "; - CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildAndCheckForEmbeddedFileInBinlog( - FactoryType.CodeTaskFactory, "HelloTask", taskXml, true); + CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildAndCheckForEmbeddedFileInBinlog( + FactoryType.CodeTaskFactory, $"HelloTask{num}", taskXml, true); + } } - [Fact] - public void EmbedsGeneratedFileInBinlogWhenFailsToCompile() + /// + /// Verify that the generated code is embedded in the binlog even when it fails to compile + /// + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void EmbedsGeneratedFileInBinlogWhenFailsToCompile(bool forceOutOfProc) { - string taskXml = @" + int num = forceOutOfProc ? 7 : 6; + using (TestEnvironment env = TestEnvironment.Create()) + { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCETASKFACTORYOUTOFPROC", "1"); + } + + string taskXml = @" "; - CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildAndCheckForEmbeddedFileInBinlog( - FactoryType.CodeTaskFactory, "HelloTask", taskXml, false); + CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildAndCheckForEmbeddedFileInBinlog( + FactoryType.CodeTaskFactory, $"HelloTask{num}", taskXml, false); + } } } #else diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index aea1c59df20..8e98a2fee9e 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -598,12 +598,17 @@ private bool HasInvalidChildNodes(XmlNode parentNode, XmlNodeType[] allowedNodeT return hasInvalidNode; } + /// + /// Stores the path to the compiled assembly when in out-of-process mode + /// + private string _assemblyPath; + /// /// Add a reference assembly to the list of references passed to the compiler. We will try and load the assembly to make sure it is found /// before sending it to the compiler. The reason we load here is that we will be using it in this appdomin anyways as soon as we are going to compile, which should be right away. /// [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] - private void AddReferenceAssemblyToReferenceList(List referenceAssemblyList, string referenceAssembly) + private void AddReferenceAssemblyToReferenceList(List referenceAssemblyList, string referenceAssembly, List directoriesToAddToManifest = null) { if (referenceAssemblyList != null) { @@ -638,6 +643,16 @@ private void AddReferenceAssemblyToReferenceList(List referenceAssemblyL if (candidateAssemblyLocation != null) { referenceAssemblyList.Add(candidateAssemblyLocation); + + // Collect directories for manifest file when in out-of-process mode + if (directoriesToAddToManifest != null && FileSystems.Default.FileExists(candidateAssemblyLocation)) + { + string directory = Path.GetDirectoryName(candidateAssemblyLocation); + if (!directoriesToAddToManifest.Contains(directory)) + { + directoriesToAddToManifest.Add(directory); + } + } } else { @@ -709,13 +724,12 @@ bool TryCacheAssemblyIdentityFromPath(string assemblyFile, out string candidateA private Assembly CompileAssembly() { // Combine our default assembly references with those specified - var finalReferencedAssemblies = CombineReferencedAssemblies(); + List directoriesToAddToManifest = Traits.Instance.ForceTaskFactoryOutOfProc ? new List() : null; + var finalReferencedAssemblies = CombineReferencedAssemblies(directoriesToAddToManifest); // Combine our default using's with those specified string[] finalUsingNamespaces = CombineUsingNamespaces(); - // for the out of proc execution - string taskAssemblyPath = null; if (Traits.Instance.ForceTaskFactoryOutOfProc) { string processSpecificInlineTaskDir = Path.Combine( @@ -723,7 +737,7 @@ private Assembly CompileAssembly() MSBuildConstants.InlineTaskTempDllSubPath, $"pid_{EnvironmentUtilities.CurrentProcessId}"); Directory.CreateDirectory(processSpecificInlineTaskDir); - taskAssemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + _assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); } // Language can be anything that has a codedom provider, in the standard naming method @@ -742,7 +756,7 @@ private Assembly CompileAssembly() IncludeDebugInformation = true, GenerateInMemory = !Traits.Instance.ForceTaskFactoryOutOfProc, - OutputAssembly = taskAssemblyPath, + OutputAssembly = _assemblyPath, // Indicates that a .dll should be generated. GenerateExecutable = false @@ -834,9 +848,26 @@ private Assembly CompileAssembly() return null; } + // Create manifest file for out-of-process execution + if (Traits.Instance.ForceTaskFactoryOutOfProc && directoriesToAddToManifest != null && directoriesToAddToManifest.Count > 0) + { + string manifestPath = _assemblyPath + ".loadmanifest"; + File.WriteAllLines(manifestPath, directoriesToAddToManifest); + } + + Assembly compiledAssembly; + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + compiledAssembly = Assembly.LoadFrom(compilerResults.PathToAssembly); + } + else + { + compiledAssembly = compilerResults.CompiledAssembly; + } + // Add to the cache. Failing to add is not a fatal error. - s_compiledTaskCache.TryAdd(fullSpec, compilerResults.CompiledAssembly); - return compilerResults.CompiledAssembly; + s_compiledTaskCache.TryAdd(fullSpec, compiledAssembly); + return compiledAssembly; } else { @@ -849,6 +880,14 @@ private Assembly CompileAssembly() /// Combine our default referenced assemblies with those explicitly specified /// private List CombineReferencedAssemblies() + { + return CombineReferencedAssemblies(null); + } + + /// + /// Combine our default referenced assemblies with those explicitly specified + /// + private List CombineReferencedAssemblies(List directoriesToAddToManifest) { List finalReferenceList = new List(s_defaultReferencedFrameworkAssemblyNames.Length + 2 + _referencedAssemblies.Count); @@ -858,7 +897,7 @@ private List CombineReferencedAssemblies() // through the magic of unification foreach (string defaultReference in s_defaultReferencedFrameworkAssemblyNames) { - AddReferenceAssemblyToReferenceList(finalReferenceList, defaultReference); + AddReferenceAssemblyToReferenceList(finalReferenceList, defaultReference, directoriesToAddToManifest); } // We also want to add references to two MSBuild assemblies: Microsoft.Build.Framework.dll and @@ -876,12 +915,28 @@ private List CombineReferencedAssemblies() finalReferenceList.Add(_msbuildFrameworkPath); finalReferenceList.Add(_msbuildUtilitiesPath); + // Collect directories for MSBuild assemblies when creating manifest + if (directoriesToAddToManifest != null) + { + string frameworkDir = Path.GetDirectoryName(_msbuildFrameworkPath); + if (!directoriesToAddToManifest.Contains(frameworkDir)) + { + directoriesToAddToManifest.Add(frameworkDir); + } + + string utilitiesDir = Path.GetDirectoryName(_msbuildUtilitiesPath); + if (!directoriesToAddToManifest.Contains(utilitiesDir)) + { + directoriesToAddToManifest.Add(utilitiesDir); + } + } + // Now for the explicitly-specified references: if (_referencedAssemblies != null) { foreach (string referenceAssembly in _referencedAssemblies) { - AddReferenceAssemblyToReferenceList(finalReferenceList, referenceAssembly); + AddReferenceAssemblyToReferenceList(finalReferenceList, referenceAssembly, directoriesToAddToManifest); } } From 4c9bf0ace8e346f9b4005c34ad7d66d7ef289e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Fri, 13 Jun 2025 14:59:03 +0200 Subject: [PATCH 20/51] nit --- src/Tasks/CodeTaskFactory.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 8e98a2fee9e..aa8c748a188 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -876,14 +876,6 @@ private Assembly CompileAssembly() } } - /// - /// Combine our default referenced assemblies with those explicitly specified - /// - private List CombineReferencedAssemblies() - { - return CombineReferencedAssemblies(null); - } - /// /// Combine our default referenced assemblies with those explicitly specified /// From af47117569a0c2d02fdf1837fac2984a3a72cef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 11:33:21 +0200 Subject: [PATCH 21/51] reflect output/required parameters in generated class --- .../RoslynCodeTaskFactory.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index f255d1c94f2..d8f290ea439 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -222,7 +222,7 @@ internal static string GetSourceCode(RoslynCodeTaskFactoryTaskInfo taskInfo, ICo foreach (TaskPropertyInfo propertyInfo in parameters) { - CreateProperty(codeTypeDeclaration, propertyInfo.Name, propertyInfo.PropertyType); + CreateProperty(codeTypeDeclaration, propertyInfo.Name, propertyInfo.PropertyType, null, propertyInfo.Output, propertyInfo.Required); } if (taskInfo.CodeType == RoslynCodeTaskFactoryCodeType.Fragment) @@ -624,9 +624,7 @@ static Assembly TryLoadAssembly(List directories, AssemblyName name) return null; } - } - - private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDeclaration, string name, Type type, object defaultValue = null) + } private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDeclaration, string name, Type type, object defaultValue = null, bool isOutput = false, bool isRequired = false) { CodeMemberField field = new CodeMemberField(new CodeTypeReference(type), "_" + name) { @@ -650,6 +648,18 @@ private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDec HasSet = true }; + // Add Output attribute if this is an output property + if (isOutput) + { + property.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Output")); + } + + // Add Required attribute if this is a required property + if (isRequired) + { + property.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Required")); + } + property.GetStatements.Add(new CodeMethodReturnStatement(fieldReference)); CodeAssignStatement fieldAssign = new CodeAssignStatement From 2af5fcdf8dc4897b8b00cc11e71a3e3351218e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 11:41:48 +0200 Subject: [PATCH 22/51] also fix codetaskfactory output params --- src/Tasks/CodeTaskFactory.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index aa8c748a188..8806df68a92 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -350,7 +350,7 @@ public void CleanupTask(ITask task) /// /// Create a property (with the corresponding private field) from the given type information /// - private static void CreateProperty(CodeTypeDeclaration ctd, string propertyName, Type propertyType, object defaultValue) + private static void CreateProperty(CodeTypeDeclaration ctd, string propertyName, Type propertyType, object defaultValue = null, bool isOutput = false, bool isRequired = false) { var field = new CodeMemberField(new CodeTypeReference(propertyType), "_" + propertyName) { @@ -372,6 +372,18 @@ private static void CreateProperty(CodeTypeDeclaration ctd, string propertyName, HasSet = true }; + // Add Output attribute if this is an output property + if (isOutput) + { + prop.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Output")); + } + + // Add Required attribute if this is a required property + if (isRequired) + { + prop.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Required")); + } + var fieldRef = new CodeFieldReferenceExpression { FieldName = field.Name }; prop.GetStatements.Add(new CodeMethodReturnStatement(fieldRef)); @@ -414,7 +426,7 @@ private static void CreateTaskBody(CodeTypeDeclaration codeTypeDeclaration, stri /// private static void CreateProperty(CodeTypeDeclaration codeTypeDeclaration, TaskPropertyInfo propInfo, object defaultValue) { - CreateProperty(codeTypeDeclaration, propInfo.Name, propInfo.PropertyType, defaultValue); + CreateProperty(codeTypeDeclaration, propInfo.Name, propInfo.PropertyType, defaultValue, propInfo.Output, propInfo.Required); } /// @@ -737,7 +749,7 @@ private Assembly CompileAssembly() MSBuildConstants.InlineTaskTempDllSubPath, $"pid_{EnvironmentUtilities.CurrentProcessId}"); Directory.CreateDirectory(processSpecificInlineTaskDir); - _assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + _assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, "inline_task.dll", false); } // Language can be anything that has a codedom provider, in the standard naming method From 7cf539d83c42e03afa942228c560ae1de1eed78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 12:33:15 +0200 Subject: [PATCH 23/51] adjust codegen tests --- ...askFactory_Tests.CSharpFragmentWithProperties.verified.txt | 4 ++++ ...ctory_Tests.VisualBasicFragmentWithProperties.verified.txt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.CSharpFragmentWithProperties.verified.txt b/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.CSharpFragmentWithProperties.verified.txt index 7918a5a0a17..1cdeef88b1d 100644 --- a/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.CSharpFragmentWithProperties.verified.txt +++ b/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.CSharpFragmentWithProperties.verified.txt @@ -22,6 +22,7 @@ namespace InlineCode { private string _Parameter1; + [Microsoft.Build.Framework.Required()] public virtual string Parameter1 { get { return _Parameter1; @@ -33,6 +34,7 @@ namespace InlineCode { private string _Parameter2; + [Microsoft.Build.Framework.Output()] public virtual string Parameter2 { get { return _Parameter2; @@ -44,6 +46,8 @@ namespace InlineCode { private string _Parameter3; + [Microsoft.Build.Framework.Output()] + [Microsoft.Build.Framework.Required()] public virtual string Parameter3 { get { return _Parameter3; diff --git a/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.VisualBasicFragmentWithProperties.verified.txt b/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.VisualBasicFragmentWithProperties.verified.txt index c5506e9fb3e..30f60736865 100644 --- a/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.VisualBasicFragmentWithProperties.verified.txt +++ b/src/Tasks.UnitTests/TaskFactorySource/RoslynCodeTaskFactory_Tests.VisualBasicFragmentWithProperties.verified.txt @@ -26,6 +26,7 @@ Namespace InlineCode Private _Parameter1 As String + _ Public Overridable Property Parameter1() As String Get Return _Parameter1 @@ -37,6 +38,7 @@ Namespace InlineCode Private _Parameter2 As String + _ Public Overridable Property Parameter2() As String Get Return _Parameter2 @@ -48,6 +50,8 @@ Namespace InlineCode Private _Parameter3 As String + _ Public Overridable Property Parameter3() As String Get Return _Parameter3 From 08b53697e737bb642ff7cf5fc4d9070d2f52f012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 13:19:33 +0200 Subject: [PATCH 24/51] fix test --- src/Tasks/CodeTaskFactory.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 8806df68a92..a5470062142 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -836,6 +836,12 @@ private Assembly CompileAssembly() var fullSpec = new FullTaskSpecification(finalReferencedAssemblies, fullCode); if (!s_compiledTaskCache.TryGetValue(fullSpec, out Assembly existingAssembly)) { + + // Note: CompileAssemblyFromSource uses Path.GetTempPath() directory, but will not create it. In some cases + // this will throw inside CompileAssemblyFromSource. To work around this, ensure the temp directory exists. + // See: https://github.com/dotnet/msbuild/issues/328 + Directory.CreateDirectory(Path.GetTempPath()); + // Invokes compilation. CompilerResults compilerResults = provider.CompileAssemblyFromSource(compilerParameters, fullCode); From 93201bd47162035c0068d93d878334c40ddc47be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 14:34:26 +0200 Subject: [PATCH 25/51] reset formatting changes in resx --- src/Build/Resources/Strings.resx | 89 +++++++++++++++++--------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 64d77f47226..41a2bb653f5 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -145,7 +145,7 @@ The operation cannot be completed because EndBuild has already been called but existing submissions have not yet completed. - + Property '{0}' with value '{1}' expanded from the environment. @@ -479,7 +479,7 @@ likely because of a programming error in the logger). When a logger dies, we cannot proceed with the build, and we throw a special exception to abort the build. - + MSB3094: "{2}" refers to {0} item(s), and "{3}" refers to {1} item(s). They must have the same number of items. {StrBegin="MSB3094: "} @@ -604,7 +604,7 @@ LOCALIZATION: "{0}" is the expression that was bad. "{1}" is a message from an FX exception that describes why the expression is bad. - + Found multiple overloads for method "{0}" with {1} parameter(s). That is currently not supported. @@ -1171,7 +1171,7 @@ LOCALIZATION: "{2}" is a localized message from a CLR/FX exception. Also, Microsoft.Build.Framework should not be localized - + MSB4181: The "{0}" task returned false but did not log an error. {StrBegin="MSB4181: "} @@ -1301,7 +1301,7 @@ MSB4067: The element <{0}> beneath element <{1}> is unrecognized. {StrBegin="MSB4067: "} - + MSB4067: The element <{0}> beneath element <{1}> is unrecognized. If you intended this to be a property, enclose it within a <PropertyGroup> element. {StrBegin="MSB4067: "} @@ -1761,7 +1761,7 @@ Utilization: {0} Average Utilization: {1:###.0} MSB4231: ProjectRootElement can't reload if it contains unsaved changes. {StrBegin="MSB4231: "} - + The parameters have been truncated beyond this point. To view all parameters, clear the MSBUILDTRUNCATETASKINPUTLOGGING environment variable. @@ -1989,9 +1989,9 @@ Utilization: {0} Average Utilization: {1:###.0} - {0} -> Cache Hit + {0} -> Cache Hit - {StrBegin="{0} -> "}LOCALIZATION: This string is used to indicate progress and matches the format for a log message from Microsoft.Common.CurrentVersion.targets. {0} is a project name. + {StrBegin="{0} -> "}LOCALIZATION: This string is used to indicate progress and matches the format for a log message from Microsoft.Common.CurrentVersion.targets. {0} is a project name. @@ -2060,7 +2060,7 @@ Utilization: {0} Average Utilization: {1:###.0} Imported files archive exceeded 2GB limit and it's not embedded. - Forward compatible reading is not supported for file format version {0} (needs >= 18). + Forward compatible reading is not supported for file format version {0} (needs >= 18). LOCALIZATION: {0} is an integer number denoting version. @@ -2179,19 +2179,19 @@ Utilization: {0} Average Utilization: {1:###.0} It is recommended to specify explicit 'Culture' metadata, or 'WithCulture=false' metadata with 'EmbeddedResource' item in order to avoid wrong or nondeterministic culture estimation. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Project {0} specifies 'EmbeddedResource' item '{1}', that has possibly a culture denoting extension ('{2}'), but explicit 'Culture' nor 'WithCulture=false' metadata are not specified. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Avoid specifying 'Always' for 'CopyToOutputDirectory' as this can lead to unnecessary copy operations during build. Use 'PreserveNewest' or 'IfDifferent' metadata value, or set the 'SkipUnchangedFilesOnCopyAlways' property to true to employ more effective copying. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. Project {0} specifies '{1}' item '{2}', that has 'CopyToOutputDirectory' set as 'Always'. Change the metadata or use 'CopyToOutputDirectory' property. - Terms in quotes are not to be translated. + Terms in quotes are not to be translated. 'TargetFramework' (singular) and 'TargetFrameworks' (plural) properties should not be specified in the scripts at the same time. @@ -2401,4 +2401,11 @@ Utilization: {0} Average Utilization: {1:###.0} Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. - \ No newline at end of file + + From fdf157f310071291906bc1db2f3da70a8fe53d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 15:08:37 +0200 Subject: [PATCH 26/51] new cleanup strategy --- .../BackEnd/BuildManager/BuildManager.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 509e049f4ac..a19f8738eaa 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1148,15 +1148,7 @@ public void EndBuild() Reset(); _buildManagerState = BuildManagerState.Idle; - if (Traits.Instance.ForceTaskFactoryOutOfProc) - { - // clean up inline tasks - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - FileUtilities.DeleteDirectoryNoThrow(processSpecificInlineTaskDir, recursive: true); - } + CleanInlineTaskCaches(); MSBuildEventSource.Log.BuildStop(); @@ -1181,8 +1173,31 @@ void SerializeCaches() LogErrorAndShutdown(errorMessage); } } + + void CleanInlineTaskCaches() + { + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + // we can't clean our own cache because we have it loaded, but we can clean caches from prior runs + string inlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath); + + if (Directory.Exists(inlineTaskDir)) + { + foreach (string dir in Directory.EnumerateDirectories(inlineTaskDir)) + { + // best effort, if it does not succeed now, it'll on a subsequent run + FileUtilities.DeleteDirectoryNoThrow(dir, recursive: true, retryCount: 1); + } + } + } + } } + + + [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads of System.Diagnostics.DiagnosticSource, TODO: when this is agreed to perf-wise enable instrumenting using activities anywhere... private void EndBuildTelemetry() { From 77ab86e3e0a23b164219d8d9226f1c82ec7f62b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 15:23:12 +0200 Subject: [PATCH 27/51] add to docs --- .../wiki/MSBuild-Environment-Variables.md | 64 +++++++++++-------- documentation/wiki/Tasks.md | 26 ++++++-- .../BackEnd/BuildManager/BuildManager.cs | 3 - 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/documentation/wiki/MSBuild-Environment-Variables.md b/documentation/wiki/MSBuild-Environment-Variables.md index 1a1ddac8187..2dd75d3e4cd 100644 --- a/documentation/wiki/MSBuild-Environment-Variables.md +++ b/documentation/wiki/MSBuild-Environment-Variables.md @@ -1,33 +1,41 @@ # MSBuild environment variables list -This document describes the environment variables that are respected in MSBuild, its purpose and usage. +This document describes the environment variables that are respected in MSBuild, its purpose and usage. Some of the env variables listed here are unsupported, meaning there is no guarantee that variable or a specific combination of multiple variables will be respected in upcoming release, so please use at your own risk. -* `MSBuildDebugEngine=1` & `MSBUILDDEBUGPATH=` - * Set this to cause any MSBuild invocation launched within this environment to emit binary logs and additional debugging information to ``. Useful when debugging build or evaluation issues when you can't directly influence the MSBuild invocation, such as in Visual Studio. More details on [capturing binary logs](./Providing-Binary-Logs.md) -* `MSBUILDTARGETOUTPUTLOGGING=1` - * Set this to enable [printing all target outputs to the log](https://learn.microsoft.com/archive/blogs/msbuild/displaying-target-output-items-using-the-console-logger). -* `MSBUILDLOGTASKINPUTS=1` - * Log task inputs (not needed if there are any diagnostic loggers already). - * `MSBUILDEMITSOLUTION=1` - * Save the generated .proj file for the .sln that is used to build the solution. The generated files are emitted into a binary log by default and their presence on disk can break subsequent builds. -* `MSBUILDENABLEALLPROPERTYFUNCTIONS=1` - * Enable [additional property functions](https://devblogs.microsoft.com/visualstudio/msbuild-property-functions/). If you need this level of detail you are generally served better with a binary log than the text log. -* `MSBUILDLOGVERBOSERARSEARCHRESULTS=1` - * In ResolveAssemblyReference task, log verbose search results. -* `MSBUILDLOGCODETASKFACTORYOUTPUT=1` - * Dump generated code for task to a .txt file in the TEMP directory -* `MSBUILDDISABLENODEREUSE=1` - * Set this to not leave MSBuild processes behind (see `/nr:false`, but the environment variable is useful to also set this for Visual Studio for example). -* `MSBUILDLOGASYNC=1` - * Enable asynchronous logging. -* `MSBUILDDEBUGONSTART=1` - * Launches debugger on build start. Works on Windows operating systems only. - * Setting the value of 2 allows for manually attaching a debugger to a process ID. This works on Windows and non-Windows operating systems. -* `MSBUILDDEBUGSCHEDULER=1` & `MSBUILDDEBUGPATH=` - * Dumps scheduler state at specified directory. - -* `MsBuildSkipEagerWildCardEvaluationRegexes` - * If specified, overrides the default behavior of glob expansion. During glob expansion, if the path with wildcards that is being processed matches one of the regular expressions provided in the [environment variable](#msbuildskipeagerwildcardevaluationregexes), the path is not processed (expanded). - * The value of the environment variable is a list of regular expressions, separated by semicolon (;). \ No newline at end of file +- `MSBuildDebugEngine=1` & `MSBUILDDEBUGPATH=` + - Set this to cause any MSBuild invocation launched within this environment to emit binary logs and additional debugging information to ``. Useful when debugging build or evaluation issues when you can't directly influence the MSBuild invocation, such as in Visual Studio. More details on [capturing binary logs](./Providing-Binary-Logs.md) +- `MSBUILDTARGETOUTPUTLOGGING=1` + - Set this to enable [printing all target outputs to the log](https://learn.microsoft.com/archive/blogs/msbuild/displaying-target-output-items-using-the-console-logger). +- `MSBUILDLOGTASKINPUTS=1` + - Log task inputs (not needed if there are any diagnostic loggers already). +- `MSBUILDEMITSOLUTION=1` + - Save the generated .proj file for the .sln that is used to build the solution. The generated files are emitted into a binary log by default and their presence on disk can break subsequent builds. +- `MSBUILDENABLEALLPROPERTYFUNCTIONS=1` + - Enable [additional property functions](https://devblogs.microsoft.com/visualstudio/msbuild-property-functions/). If you need this level of detail you are generally served better with a binary log than the text log. +- `MSBUILDLOGVERBOSERARSEARCHRESULTS=1` + - In ResolveAssemblyReference task, log verbose search results. +- `MSBUILDLOGCODETASKFACTORYOUTPUT=1` + - Dump generated code for task to a `.txt` file in the TEMP directory +- `MSBUILDDISABLENODEREUSE=1` + - Set this to not leave MSBuild processes behind (see `/nr:false`, but the environment variable is useful to also set this for Visual Studio for example). +- `MSBUILDLOGASYNC=1` + - Enable asynchronous logging. +- `MSBUILDDEBUGONSTART=1` + - Launches debugger on build start. Works on Windows operating systems only. + - Setting the value of 2 allows for manually attaching a debugger to a process ID. This works on Windows and non-Windows operating systems. +- `MSBUILDDEBUGSCHEDULER=1` & `MSBUILDDEBUGPATH=` + + - Dumps scheduler state at specified directory. + +- `MsBuildSkipEagerWildCardEvaluationRegexes` + + - If specified, overrides the default behavior of glob expansion. During glob expansion, if the path with wildcards that is being processed matches one of the regular expressions provided in the [environment variable](#msbuildskipeagerwildcardevaluationregexes), the path is not processed (expanded). + - The value of the environment variable is a list of regular expressions, separated by semicolon (;). + +- `MSBUILDFORCEALLTASKSOUTOFPROCESS` + - Set this to force all tasks to run out of process (except inline tasks). + +- `MSBUILDFORCETASKFACTORYOUTOFPROC` + - Set this to force all inline tasks to run out of process. It is not compatible with custom TaskFactories. diff --git a/documentation/wiki/Tasks.md b/documentation/wiki/Tasks.md index 76bd3f9bb14..d63ba9e1691 100644 --- a/documentation/wiki/Tasks.md +++ b/documentation/wiki/Tasks.md @@ -1,56 +1,72 @@ # Tasks + A Task is a unit of execution in a Target and a method of extensibility in MSBuild. + ## Basics + A task is a class implementing [`ITask`](https://github.com/dotnet/msbuild/blob/main/src/Framework/ITask.cs). -- The notable method of this interface is `bool Execute()`. Code in it will get executed when the task is run. + +- The notable method of this interface is `bool Execute()`. Code in it will get executed when the task is run. - A Task can have public properties that can be set by the user in the project file. - - These properties can be `string`, `bool`, `ITaskItem` (representation of a file system object), `string[]`, `bool[]`, `ITaskItem[]` - - the properties can have attributes `[Required]` which causes the engine to check that it has a value when the task is run and `[Output]` which exposes the property to be used again in XML + - These properties can be `string`, `bool`, `ITaskItem` (representation of a file system object), `string[]`, `bool[]`, `ITaskItem[]` + - the properties can have attributes `[Required]` which causes the engine to check that it has a value when the task is run and `[Output]` which exposes the property to be used again in XML + - Tasks have the `Log` property set by the engine to log messages/errors/warnings. ## Internals + - [`TaskRegistry`](https://github.com/dotnet/msbuild/blob/main/src/Build/Instance/TaskRegistry.cs) - has a list of all available tasks for the build, and resolves them. - [`TaskExecutionHost`](https://github.com/dotnet/msbuild/tree/main/src/Build/BackEnd/TaskExecutionHost) - finds task in TaskRegistry, calls TaskFactory to create an instance of the task and sets its properties using reflection from the values in the XML. Then calls Execute on the task and gathers the outputs. - TaskFactory - initializes the task, creates instance of the task - ITask class - runs `Execute()` method ## Custom Tasks + Users can implement `ITask` and compile it into a .dll. Then they can use in project file: + ```xml ``` + This uses the AssemblyTaskFactory to load the task from the .dll and create an instance of it. ## Diagram of task lifecycle ```mermaid -graph +graph I["Implement:
extend ITask interface in .dll"] --> R["Register:
<UsingTask />"] --> U["Use in XML:
<Target>
      <MyTask />
</Target>"] --> In["Initialize:
compile inline or load from assembly
(TaskFactory)"] --> S["Setup:
Set input properties
(TaskExecutionHost)"] --> E["ITask.Execute()"] --> O["Gather outputs:
(TaskExecutionHost)"] ``` ## Task Factories + Task factories create instances of tasks. They implement [`ITaskFactory`](https://github.com/dotnet/msbuild/blob/main/src/Framework/ITaskFactory.cs) or [`ITaskFactory2`](https://github.com/dotnet/msbuild/blob/main/src/Framework/ITaskFactory2.cs). This interface defines `bool Initialize(...)` and `ITask CreateTask(...)`. They are e.g. responsible for loading a task from an assembly and initializing it. +The trait `MSBUILDFORCETASKFACTORYOUTOFPROC` allows running inline tasks in an out of process TaskHost and is not compatible with custom TaskFactories. + ### Built-in Task Factories + - [`AssemblyTaskFactory`](https://github.com/dotnet/msbuild/blob/main/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs) - constructs tasks from .NET assemblies - [`RoslynCodeTaskFactory`](https://github.com/dotnet/msbuild/blob/main/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs) - inline code tasks - CodeTaskFactory, XamlTaskFactory - old, rarely used ### Custom Task Factories + This is a rarely used method of extensibility. Users can implement `ITaskFactory` to create custom task factories. Then they can use in project file: + ```xml Insides that the MyTaskFactory uses to initialize ``` -# Microsoft Learn Resources +## Microsoft Learn Resources + - [MSBuild task](https://learn.microsoft.com/visualstudio/msbuild/msbuild-task) - [Task reference](https://learn.microsoft.com/visualstudio/msbuild/msbuild-task-reference) - [Task Writing](https://learn.microsoft.com/visualstudio/msbuild/task-writing) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index a19f8738eaa..c5ce2043f46 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1195,9 +1195,6 @@ void CleanInlineTaskCaches() } } - - - [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads of System.Diagnostics.DiagnosticSource, TODO: when this is agreed to perf-wise enable instrumenting using activities anywhere... private void EndBuildTelemetry() { From 7f42c450b6b4d26bf9f8897e6bb787252c792da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 15:40:56 +0200 Subject: [PATCH 28/51] cleanup --- src/MSBuild/OutOfProcTaskHostNode.cs | 63 +++++++++++++++------------- src/Tasks/CodeTaskFactory.cs | 2 - 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 2a9e325c1cf..fc3d54e861b 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -930,36 +930,8 @@ private void RunTask(object state) string taskName = taskConfiguration.TaskName; string taskLocation = taskConfiguration.TaskLocation; - string[] directoriesToAddToAppDomain; - // if the tasklocation ends with inline_task.dll - if (taskLocation.EndsWith("inline_task.dll", StringComparison.OrdinalIgnoreCase)) - { - // load manifest of the task assembly - string manifestPath = taskLocation + ".loadmanifest"; - // read all lines to list if it exists - if (File.Exists(manifestPath)) - { - directoriesToAddToAppDomain = File.ReadAllLines(manifestPath); - if (directoriesToAddToAppDomain != null) - { - // let's do the appdomain magic here... - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => - { - var assemblyName = new AssemblyName(args.Name); - foreach (string directory in directoriesToAddToAppDomain) - { - string assemblyPath = Path.Combine(directory, assemblyName.Name + ".dll"); - if (File.Exists(assemblyPath)) - { - return Assembly.LoadFrom(assemblyPath); - } - } - return null; - }; - } - } - } + RegisterDependencyLoadHandlersFromManifest(); // We will not create an appdomain now because of a bug // As a fix, we will create the class directly without wrapping it in a domain @@ -1044,6 +1016,39 @@ private void RunTask(object state) _taskCompleteEvent.Set(); } } + + /// + /// Inline tasks may have dependencies that were resolved during TaskFactory initialization. + /// Recreate assembly resolve handlers for these dependencies. + /// + static void RegisterDependencyLoadHandlersFromManifest() + { + if (taskLocation.EndsWith("inline_task.dll", StringComparison.OrdinalIgnoreCase)) + { + string manifestPath = taskLocation + ".loadmanifest"; + if (File.Exists(manifestPath)) + { + string[] directoriesToAddToAppDomain = File.ReadAllLines(manifestPath); + if (directoriesToAddToAppDomain != null) + { + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + var assemblyName = new AssemblyName(args.Name); + foreach (string directory in directoriesToAddToAppDomain) + { + string assemblyPath = Path.Combine(directory, assemblyName.Name + ".dll"); + if (File.Exists(assemblyPath)) + { + return Assembly.LoadFrom(assemblyPath); + } + } + + return null; + }; + } + } + } + } } /// diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 5aaaf50f976..32a24d6f39a 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -372,13 +372,11 @@ private static void CreateProperty(CodeTypeDeclaration ctd, string propertyName, HasSet = true }; - // Add Output attribute if this is an output property if (isOutput) { prop.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Output")); } - // Add Required attribute if this is a required property if (isRequired) { prop.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Required")); From 2144eda35d370408185e20e65e6bf83089a9c146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 16 Jun 2025 17:19:40 +0200 Subject: [PATCH 29/51] oops --- src/MSBuild/OutOfProcTaskHostNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index fc3d54e861b..cf524b9e787 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -931,7 +931,7 @@ private void RunTask(object state) string taskName = taskConfiguration.TaskName; string taskLocation = taskConfiguration.TaskLocation; - RegisterDependencyLoadHandlersFromManifest(); + RegisterDependencyLoadHandlersFromManifest(taskLocation); // We will not create an appdomain now because of a bug // As a fix, we will create the class directly without wrapping it in a domain @@ -1021,7 +1021,7 @@ private void RunTask(object state) /// Inline tasks may have dependencies that were resolved during TaskFactory initialization. /// Recreate assembly resolve handlers for these dependencies. /// - static void RegisterDependencyLoadHandlersFromManifest() + static void RegisterDependencyLoadHandlersFromManifest(string taskLocation) { if (taskLocation.EndsWith("inline_task.dll", StringComparison.OrdinalIgnoreCase)) { From 1b844f1335158b6cbff70bf695b3490999a5df85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 23 Jun 2025 13:04:28 +0200 Subject: [PATCH 30/51] refactor duplicate assembly resolution and load manifest code --- src/Tasks/CodeTaskFactory.cs | 49 +--- .../RoslynCodeTaskFactory.cs | 56 +---- src/Tasks/XamlTaskFactory/XamlTaskFactory.cs | 7 +- .../TaskFactoryUtilities_Tests.cs | 143 ++++++++++++ src/Utilities/TaskFactoryUtilities.cs | 212 ++++++++++++++++++ 5 files changed, 372 insertions(+), 95 deletions(-) create mode 100644 src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs create mode 100644 src/Utilities/TaskFactoryUtilities.cs diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 32a24d6f39a..aa270c4fc71 100644 --- a/src/Tasks/CodeTaskFactory.cs +++ b/src/Tasks/CodeTaskFactory.cs @@ -618,7 +618,7 @@ private bool HasInvalidChildNodes(XmlNode parentNode, XmlNodeType[] allowedNodeT /// before sending it to the compiler. The reason we load here is that we will be using it in this appdomin anyways as soon as we are going to compile, which should be right away. ///
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName", Justification = "Necessary since we don't have the full assembly name. ")] - private void AddReferenceAssemblyToReferenceList(List referenceAssemblyList, string referenceAssembly, List directoriesToAddToManifest = null) + private void AddReferenceAssemblyToReferenceList(List referenceAssemblyList, string referenceAssembly) { if (referenceAssemblyList != null) { @@ -653,16 +653,6 @@ private void AddReferenceAssemblyToReferenceList(List referenceAssemblyL if (candidateAssemblyLocation != null) { referenceAssemblyList.Add(candidateAssemblyLocation); - - // Collect directories for manifest file when in out-of-process mode - if (directoriesToAddToManifest != null && FileSystems.Default.FileExists(candidateAssemblyLocation)) - { - string directory = Path.GetDirectoryName(candidateAssemblyLocation); - if (!directoriesToAddToManifest.Contains(directory)) - { - directoriesToAddToManifest.Add(directory); - } - } } else { @@ -734,20 +724,14 @@ bool TryCacheAssemblyIdentityFromPath(string assemblyFile, out string candidateA private Assembly CompileAssembly() { // Combine our default assembly references with those specified - List directoriesToAddToManifest = Traits.Instance.ForceTaskFactoryOutOfProc ? new List() : null; - var finalReferencedAssemblies = CombineReferencedAssemblies(directoriesToAddToManifest); + var finalReferencedAssemblies = CombineReferencedAssemblies(); // Combine our default using's with those specified string[] finalUsingNamespaces = CombineUsingNamespaces(); if (Traits.Instance.ForceTaskFactoryOutOfProc) { - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - Directory.CreateDirectory(processSpecificInlineTaskDir); - _assemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, "inline_task.dll", false); + _assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); } // Language can be anything that has a codedom provider, in the standard naming method @@ -865,10 +849,9 @@ private Assembly CompileAssembly() } // Create manifest file for out-of-process execution - if (Traits.Instance.ForceTaskFactoryOutOfProc && directoriesToAddToManifest != null && directoriesToAddToManifest.Count > 0) + if (Traits.Instance.ForceTaskFactoryOutOfProc) { - string manifestPath = _assemblyPath + ".loadmanifest"; - File.WriteAllLines(manifestPath, directoriesToAddToManifest); + TaskFactoryUtilities.CreateLoadManifestFromReferences(_assemblyPath, finalReferencedAssemblies); } Assembly compiledAssembly; @@ -895,7 +878,7 @@ private Assembly CompileAssembly() /// /// Combine our default referenced assemblies with those explicitly specified /// - private List CombineReferencedAssemblies(List directoriesToAddToManifest) + private List CombineReferencedAssemblies() { List finalReferenceList = new List(s_defaultReferencedFrameworkAssemblyNames.Length + 2 + _referencedAssemblies.Count); @@ -905,7 +888,7 @@ private List CombineReferencedAssemblies(List directoriesToAddTo // through the magic of unification foreach (string defaultReference in s_defaultReferencedFrameworkAssemblyNames) { - AddReferenceAssemblyToReferenceList(finalReferenceList, defaultReference, directoriesToAddToManifest); + AddReferenceAssemblyToReferenceList(finalReferenceList, defaultReference); } // We also want to add references to two MSBuild assemblies: Microsoft.Build.Framework.dll and @@ -923,28 +906,12 @@ private List CombineReferencedAssemblies(List directoriesToAddTo finalReferenceList.Add(_msbuildFrameworkPath); finalReferenceList.Add(_msbuildUtilitiesPath); - // Collect directories for MSBuild assemblies when creating manifest - if (directoriesToAddToManifest != null) - { - string frameworkDir = Path.GetDirectoryName(_msbuildFrameworkPath); - if (!directoriesToAddToManifest.Contains(frameworkDir)) - { - directoriesToAddToManifest.Add(frameworkDir); - } - - string utilitiesDir = Path.GetDirectoryName(_msbuildUtilitiesPath); - if (!directoriesToAddToManifest.Contains(utilitiesDir)) - { - directoriesToAddToManifest.Add(utilitiesDir); - } - } - // Now for the explicitly-specified references: if (_referencedAssemblies != null) { foreach (string referenceAssembly in _referencedAssemblies) { - AddReferenceAssemblyToReferenceList(finalReferenceList, referenceAssembly, directoriesToAddToManifest); + AddReferenceAssemblyToReferenceList(finalReferenceList, referenceAssembly); } } diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index d8f290ea439..6d003a35ae5 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -544,8 +544,6 @@ internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTask references = references.Union(value); } - List directoriesToAddToAppDomain = new(); - // Loop through the user specified references as well as the default references foreach (string reference in references) { @@ -554,7 +552,6 @@ internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTask { // The path could be relative like ..\Assembly.dll so we need to get the full path string fullPath = Path.GetFullPath(reference); - directoriesToAddToAppDomain.Add(Path.GetDirectoryName(fullPath)); resolvedAssemblyReferences.Add(fullPath); continue; } @@ -587,43 +584,20 @@ internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTask // Transform the list of resolved assemblies to TaskItems if they were all resolved items = hasInvalidReference ? null : resolvedAssemblyReferences.Select(i => (ITaskItem)new TaskItem(i)).ToArray(); - handlerAddedToAppDomain = (_, eventArgs) => TryLoadAssembly(directoriesToAddToAppDomain, new AssemblyName(eventArgs.Name)); + // Extract directories from resolved assemblies for assembly resolution and manifest creation + var directoriesToAddToAppDomain = TaskFactoryUtilities.ExtractUniqueDirectoriesFromAssemblyPaths(resolvedAssemblyReferences); + + handlerAddedToAppDomain = TaskFactoryUtilities.CreateAssemblyResolver(directoriesToAddToAppDomain); AppDomain.CurrentDomain.AssemblyResolve += handlerAddedToAppDomain; - // in case of taskhost we cache the resolution to a file placed next to the task assembly + // In case of taskhost we cache the resolution to a file placed next to the task assembly // so the taskhost can recreate this tryloadassembly logic if (Traits.Instance.ForceTaskFactoryOutOfProc) { - // simply serialize the directories to a file next to the task assembly - string pth = _assemblyPath + ".loadmanifest"; - File.WriteAllLines(pth, directoriesToAddToAppDomain); + TaskFactoryUtilities.CreateLoadManifest(_assemblyPath, directoriesToAddToAppDomain); } return !hasInvalidReference; - - static Assembly TryLoadAssembly(List directories, AssemblyName name) - { - foreach (string directory in directories) - { - string path; - if (!string.IsNullOrEmpty(name.CultureName)) - { - path = Path.Combine(directory, name.CultureName, name.Name + ".dll"); - if (File.Exists(path)) - { - return Assembly.LoadFrom(path); - } - } - - path = Path.Combine(directory, name.Name + ".dll"); - if (File.Exists(path)) - { - return Assembly.LoadFrom(path); - } - } - - return null; - } } private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDeclaration, string name, Type type, object defaultValue = null, bool isOutput = false, bool isRequired = false) { CodeMemberField field = new CodeMemberField(new CodeTypeReference(type), "_" + name) @@ -690,14 +664,7 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT return true; } - string _taskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - - Directory.CreateDirectory(_taskDir); - - _assemblyPath = FileUtilities.GetTemporaryFile(_taskDir, null, "inline_task.dll", false); + _assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references)) { @@ -789,14 +756,7 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT } // Return the compiled assembly - if (Traits.Instance.ForceTaskFactoryOutOfProc) - { - assembly = Assembly.LoadFrom(_assemblyPath); - } - else - { - assembly = Assembly.Load(File.ReadAllBytes(_assemblyPath)); - } + assembly = TaskFactoryUtilities.LoadTaskAssembly(_assemblyPath, Traits.Instance.ForceTaskFactoryOutOfProc); CompiledAssemblyCache.TryAdd(taskInfo, assembly); return true; diff --git a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs index 7d359ef8598..227648737f9 100644 --- a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs +++ b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs @@ -119,12 +119,7 @@ public bool Initialize(string taskName, IDictionary ta string taskAssemblyPath = null; if (Traits.Instance.ForceTaskFactoryOutOfProc) { - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - Directory.CreateDirectory(processSpecificInlineTaskDir); - taskAssemblyPath = FileUtilities.GetTemporaryFile(processSpecificInlineTaskDir, null, ".dll", false); + taskAssemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); } // create the code generator options diff --git a/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs b/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs new file mode 100644 index 00000000000..4c04f6f9ba1 --- /dev/null +++ b/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs @@ -0,0 +1,143 @@ +// 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.Collections.Generic; +using System.IO; +using System.Reflection; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + public sealed class TaskFactoryUtilities_Tests + { + [Fact] + public void CreateProcessSpecificTaskDirectory_ShouldCreateValidDirectory() + { + // Act + string directory = TaskFactoryUtilities.CreateProcessSpecificTaskDirectory(); + + // Assert + directory.ShouldNotBeNull(); + Directory.Exists(directory).ShouldBeTrue(); + directory.ShouldContain(MSBuildConstants.InlineTaskTempDllSubPath); + directory.ShouldContain($"pid_{EnvironmentUtilities.CurrentProcessId}"); + } + + [Fact] + public void GetTemporaryTaskAssemblyPath_ShouldReturnValidPath() + { + // Act + string assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); + + // Assert + assemblyPath.ShouldNotBeNull(); + assemblyPath.ShouldEndWith(".dll"); + Path.GetDirectoryName(assemblyPath).ShouldContain(MSBuildConstants.InlineTaskTempDllSubPath); + } + + [Fact] + public void GetTemporaryTaskAssemblyPath_WithCustomParameters_ShouldUseCustomValues() + { + // Arrange + string fileName = "custom_task"; + string extension = ".exe"; + + // Act + string assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(fileName, extension); + + // Assert + assemblyPath.ShouldNotBeNull(); + assemblyPath.ShouldEndWith(extension); + Path.GetFileName(assemblyPath).ShouldContain(fileName); + } + + [Fact] + public void CreateLoadManifest_ShouldCreateFileWithDirectories() + { + // Arrange + string tempAssemblyPath = Path.GetTempFileName(); + var directories = new List { "dir1", "dir2", "dir3" }; + + try + { + // Act + string manifestPath = TaskFactoryUtilities.CreateLoadManifest(tempAssemblyPath, directories); + + // Assert + manifestPath.ShouldBe(tempAssemblyPath + ".loadmanifest"); + File.Exists(manifestPath).ShouldBeTrue(); + + string[] manifestContent = File.ReadAllLines(manifestPath); + manifestContent.Length.ShouldBe(3); + manifestContent.ShouldContain("dir1"); + manifestContent.ShouldContain("dir2"); + manifestContent.ShouldContain("dir3"); + } + finally + { + // Cleanup + if (File.Exists(tempAssemblyPath)) + { + File.Delete(tempAssemblyPath); + } + if (File.Exists(tempAssemblyPath + ".loadmanifest")) + { + File.Delete(tempAssemblyPath + ".loadmanifest"); + } + } + } + + [Fact] + public void CreateLoadManifest_WithNullAssemblyPath_ShouldThrow() + { + // Arrange + var directories = new List { "dir1" }; + + // Act & Assert + Should.Throw(() => TaskFactoryUtilities.CreateLoadManifest(null, directories)); + } + + [Fact] + public void CreateLoadManifest_WithNullDirectories_ShouldThrow() + { + // Arrange + string assemblyPath = "test.dll"; + + // Act & Assert + Should.Throw(() => TaskFactoryUtilities.CreateLoadManifest(assemblyPath, null)); + } + + [Fact] + public void LoadTaskAssembly_WithNullPath_ShouldThrow() + { + // Act & Assert + Should.Throw(() => TaskFactoryUtilities.LoadTaskAssembly(null, false)); + } + + [Fact] + public void CreateAssemblyResolver_ShouldReturnValidHandler() + { + // Arrange + var directories = new List { Environment.CurrentDirectory }; + + // Act + ResolveEventHandler handler = TaskFactoryUtilities.CreateAssemblyResolver(directories); + + // Assert + handler.ShouldNotBeNull(); + } + + [Fact] + public void CreateAssemblyResolver_WithNullDirectories_ShouldThrow() + { + Should.Throw(() => TaskFactoryUtilities.CreateAssemblyResolver(null)); + } + } +} diff --git a/src/Utilities/TaskFactoryUtilities.cs b/src/Utilities/TaskFactoryUtilities.cs new file mode 100644 index 00000000000..c5ef49ccbcd --- /dev/null +++ b/src/Utilities/TaskFactoryUtilities.cs @@ -0,0 +1,212 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Utilities +{ + /// + /// Utilities class that provides common functionality for task factories such as + /// temporary assembly creation and load manifest generation. + /// + /// This class consolidates duplicate logic that was previously scattered across: + /// - RoslynCodeTaskFactory + /// - CodeTaskFactory + /// - XamlTaskFactory + /// + /// The common patterns include: + /// 1. Creating process-specific temporary directories for inline task assemblies + /// 2. Generating load manifest files for out-of-process task execution + /// 3. Loading assemblies based on execution mode (in-process vs out-of-process) + /// 4. Assembly resolution for custom reference locations + /// + public static class TaskFactoryUtilities + { + /// + /// Creates a process-specific temporary directory for inline task assemblies. + /// + /// The path to the created temporary directory. + public static string CreateProcessSpecificTaskDirectory() + { + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + MSBuildConstants.InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + + Directory.CreateDirectory(processSpecificInlineTaskDir); + return processSpecificInlineTaskDir; + } + + /// + /// Gets a temporary file path for an inline task assembly in the process-specific directory. + /// + /// The base filename (without extension) to use. If null, a random name will be generated. + /// The file extension to use (e.g., ".dll"). + /// The full path to the temporary file. + public static string GetTemporaryTaskAssemblyPath(string? fileName = null, string extension = ".dll") + { + string taskDir = CreateProcessSpecificTaskDirectory(); + return FileUtilities.GetTemporaryFile(taskDir, null, fileName ?? "inline_task" + extension, false); + } + + /// + /// Creates a load manifest file containing directories that should be added to the assembly resolution path + /// for out-of-process task execution. + /// + /// The path to the task assembly. + /// The list of directories to include in the manifest. + /// The path to the created manifest file. + public static string CreateLoadManifest(string assemblyPath, IEnumerable directoriesToAdd) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + throw new ArgumentException("Assembly path cannot be null or empty.", nameof(assemblyPath)); + } + + if (directoriesToAdd == null) + { + throw new ArgumentNullException(nameof(directoriesToAdd)); + } + + string manifestPath = assemblyPath + ".loadmanifest"; + File.WriteAllLines(manifestPath, directoriesToAdd); + return manifestPath; + } + + /// + /// Creates a load manifest file from reference assembly paths by extracting their directories. + /// This is a convenience method that extracts unique directories from assembly paths and creates the manifest. + /// + /// The path to the task assembly. + /// The list of reference assembly paths to extract directories from. + /// The path to the created manifest file, or null if no valid directories were found. + public static string? CreateLoadManifestFromReferences(string assemblyPath, IEnumerable referenceAssemblyPaths) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + throw new ArgumentException("Assembly path cannot be null or empty.", nameof(assemblyPath)); + } + + if (referenceAssemblyPaths == null) + { + throw new ArgumentNullException(nameof(referenceAssemblyPaths)); + } + + var directories = ExtractUniqueDirectoriesFromAssemblyPaths(referenceAssemblyPaths); + + if (directories.Count == 0) + { + return null; + } + + return CreateLoadManifest(assemblyPath, directories); + } + + /// + /// Extracts unique directories from a collection of assembly file paths. + /// Only includes directories for assemblies that actually exist on disk. + /// + /// The collection of assembly file paths. + /// A list of unique directory paths. + public static IList ExtractUniqueDirectoriesFromAssemblyPaths(IEnumerable assemblyPaths) + { + if (assemblyPaths == null) + { + throw new ArgumentNullException(nameof(assemblyPaths)); + } + + var directories = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string assemblyPath in assemblyPaths) + { + if (!string.IsNullOrEmpty(assemblyPath) && File.Exists(assemblyPath)) + { + string? directory = Path.GetDirectoryName(assemblyPath); + if (!string.IsNullOrEmpty(directory)) + { + directories.Add(directory); + } + } + } + + return directories.ToList(); + } + + /// + /// Loads an assembly based on whether out-of-process execution is enabled. + /// + /// The path to the assembly to load. + /// Whether to use out-of-process execution mode. + /// The loaded assembly. + public static Assembly LoadTaskAssembly(string assemblyPath, bool useOutOfProcess) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + throw new ArgumentException("Assembly path cannot be null or empty.", nameof(assemblyPath)); + } + + if (useOutOfProcess) + { + return Assembly.LoadFrom(assemblyPath); + } + else + { + return Assembly.Load(File.ReadAllBytes(assemblyPath)); + } + } + + /// + /// Creates an assembly resolution event handler that can resolve assemblies from a list of directories. + /// This is typically used for in-memory compiled task assemblies that have custom reference locations. + /// + /// The directories to search for assemblies. + /// A ResolveEventHandler that can be used with AppDomain.CurrentDomain.AssemblyResolve. + public static ResolveEventHandler CreateAssemblyResolver(IList searchDirectories) + { + if (searchDirectories == null) + { + throw new ArgumentNullException(nameof(searchDirectories)); + } + + return (sender, args) => TryLoadAssembly(searchDirectories, new AssemblyName(args.Name)); + } + + /// + /// Attempts to load an assembly by searching in the specified directories. + /// + /// The directories to search in. + /// The name of the assembly to load. + /// The loaded assembly if found, otherwise null. + private static Assembly? TryLoadAssembly(IList directories, AssemblyName assemblyName) + { + foreach (string directory in directories) + { + string path; + + // Try culture-specific path first if the assembly has a culture + if (!string.IsNullOrEmpty(assemblyName.CultureName)) + { + path = Path.Combine(directory, assemblyName.CultureName, assemblyName.Name + ".dll"); + if (File.Exists(path)) + { + return Assembly.LoadFrom(path); + } + } + + // Try the standard path + path = Path.Combine(directory, assemblyName.Name + ".dll"); + if (File.Exists(path)) + { + return Assembly.LoadFrom(path); + } + } + + return null; + } + } +} From 603649c6032cb3c0e510e49fb4a34fd4f9391331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 23 Jun 2025 14:44:52 +0200 Subject: [PATCH 31/51] cleanup --- .../TaskFactoryUtilities_Tests.cs | 16 ---------------- src/Utilities/TaskFactoryUtilities.cs | 6 ++---- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs b/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs index 4c04f6f9ba1..1799b50b90b 100644 --- a/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs +++ b/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs @@ -42,22 +42,6 @@ public void GetTemporaryTaskAssemblyPath_ShouldReturnValidPath() Path.GetDirectoryName(assemblyPath).ShouldContain(MSBuildConstants.InlineTaskTempDllSubPath); } - [Fact] - public void GetTemporaryTaskAssemblyPath_WithCustomParameters_ShouldUseCustomValues() - { - // Arrange - string fileName = "custom_task"; - string extension = ".exe"; - - // Act - string assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(fileName, extension); - - // Assert - assemblyPath.ShouldNotBeNull(); - assemblyPath.ShouldEndWith(extension); - Path.GetFileName(assemblyPath).ShouldContain(fileName); - } - [Fact] public void CreateLoadManifest_ShouldCreateFileWithDirectories() { diff --git a/src/Utilities/TaskFactoryUtilities.cs b/src/Utilities/TaskFactoryUtilities.cs index c5ef49ccbcd..45dd7c5db18 100644 --- a/src/Utilities/TaskFactoryUtilities.cs +++ b/src/Utilities/TaskFactoryUtilities.cs @@ -45,13 +45,11 @@ public static string CreateProcessSpecificTaskDirectory() /// /// Gets a temporary file path for an inline task assembly in the process-specific directory. /// - /// The base filename (without extension) to use. If null, a random name will be generated. - /// The file extension to use (e.g., ".dll"). /// The full path to the temporary file. - public static string GetTemporaryTaskAssemblyPath(string? fileName = null, string extension = ".dll") + public static string GetTemporaryTaskAssemblyPath() { string taskDir = CreateProcessSpecificTaskDirectory(); - return FileUtilities.GetTemporaryFile(taskDir, null, fileName ?? "inline_task" + extension, false); + return FileUtilities.GetTemporaryFile(taskDir, null, "inline_task.dll", false); } /// From 01ea9a994ad4fb38b39b4f2715beebc7de4e3003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 25 Jun 2025 15:27:12 +0200 Subject: [PATCH 32/51] pr feedback --- .../BackEnd/BuildManager/BuildManager.cs | 25 +----- src/Build/Microsoft.Build.csproj | 3 +- src/MSBuild/MSBuild.csproj | 1 + src/MSBuild/OutOfProcTaskHostNode.cs | 39 +-------- src/Shared/Constants.cs | 5 -- src/Shared/FileUtilities.cs | 31 ++++++- .../TaskFactoryUtilities.cs | 83 +++++++++++++++---- .../TaskFactoryUtilities_Tests.cs | 30 ++----- src/Tasks/Microsoft.Build.Tasks.csproj | 1 + .../RoslynCodeTaskFactory.cs | 6 +- 10 files changed, 122 insertions(+), 102 deletions(-) rename src/{Utilities => Shared}/TaskFactoryUtilities.cs (72%) rename src/{Utilities.UnitTests => Tasks.UnitTests}/TaskFactoryUtilities_Tests.cs (79%) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c5ce2043f46..529d56dfc5b 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1148,7 +1148,10 @@ public void EndBuild() Reset(); _buildManagerState = BuildManagerState.Idle; - CleanInlineTaskCaches(); + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + TaskFactoryUtilities.CleanInlineTaskCaches(); + } MSBuildEventSource.Log.BuildStop(); @@ -1173,26 +1176,6 @@ void SerializeCaches() LogErrorAndShutdown(errorMessage); } } - - void CleanInlineTaskCaches() - { - if (Traits.Instance.ForceTaskFactoryOutOfProc) - { - // we can't clean our own cache because we have it loaded, but we can clean caches from prior runs - string inlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath); - - if (Directory.Exists(inlineTaskDir)) - { - foreach (string dir in Directory.EnumerateDirectories(inlineTaskDir)) - { - // best effort, if it does not succeed now, it'll on a subsequent run - FileUtilities.DeleteDirectoryNoThrow(dir, recursive: true, retryCount: 1); - } - } - } - } } [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads of System.Diagnostics.DiagnosticSource, TODO: when this is agreed to perf-wise enable instrumenting using activities anywhere... diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 144e09f128a..17bdeca8be7 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -43,7 +43,7 @@ or when a .NET 10 SDK is used (NuGet Package Pruning eliminates netstandard1.x dependencies). --> - + @@ -96,6 +96,7 @@ Collections\ReadOnlyEmptyCollection.cs + diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 686e78276ab..85d9cb6c02a 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -111,6 +111,7 @@ + diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index cf524b9e787..b554bc04096 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -930,9 +930,9 @@ private void RunTask(object state) string taskName = taskConfiguration.TaskName; string taskLocation = taskConfiguration.TaskLocation; - - RegisterDependencyLoadHandlersFromManifest(taskLocation); - +#if !CLR2COMPATIBILITY + TaskFactoryUtilities.RegisterAssemblyResolveHandlersFromManifest(taskLocation); +#endif // We will not create an appdomain now because of a bug // As a fix, we will create the class directly without wrapping it in a domain _taskWrapper = new OutOfProcTaskAppDomainWrapper(); @@ -1016,39 +1016,6 @@ private void RunTask(object state) _taskCompleteEvent.Set(); } } - - /// - /// Inline tasks may have dependencies that were resolved during TaskFactory initialization. - /// Recreate assembly resolve handlers for these dependencies. - /// - static void RegisterDependencyLoadHandlersFromManifest(string taskLocation) - { - if (taskLocation.EndsWith("inline_task.dll", StringComparison.OrdinalIgnoreCase)) - { - string manifestPath = taskLocation + ".loadmanifest"; - if (File.Exists(manifestPath)) - { - string[] directoriesToAddToAppDomain = File.ReadAllLines(manifestPath); - if (directoriesToAddToAppDomain != null) - { - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => - { - var assemblyName = new AssemblyName(args.Name); - foreach (string directory in directoriesToAddToAppDomain) - { - string assemblyPath = Path.Combine(directory, assemblyName.Name + ".dll"); - if (File.Exists(assemblyPath)) - { - return Assembly.LoadFrom(assemblyPath); - } - } - - return null; - }; - } - } - } - } } /// diff --git a/src/Shared/Constants.cs b/src/Shared/Constants.cs index 9f2f75fb0eb..3418ad8b214 100644 --- a/src/Shared/Constants.cs +++ b/src/Shared/Constants.cs @@ -114,11 +114,6 @@ internal static class MSBuildConstants /// internal const string ProjectReferenceTargetsOrDefaultTargetsMarker = ".projectReferenceTargetsOrDefaultTargets"; - /// - /// The sub-path within the temporary directory where compiled inline tasks are located. - /// - internal const string InlineTaskTempDllSubPath = nameof(InlineTaskTempDllSubPath); - // One-time allocations to avoid implicit allocations for Split(), Trim(). internal static readonly char[] SemicolonChar = [';']; internal static readonly char[] SpaceChar = [' ']; diff --git a/src/Shared/FileUtilities.cs b/src/Shared/FileUtilities.cs index 71259e9d412..754ba47c614 100644 --- a/src/Shared/FileUtilities.cs +++ b/src/Shared/FileUtilities.cs @@ -731,6 +731,35 @@ internal static string GetDirectory(string fileSpec) return directory; } +#if !CLR2COMPATIBILITY + /// + /// Deletes all subdirectories within the specified directory without throwing exceptions. + /// This method enumerates all subdirectories in the given directory and attempts to delete + /// each one recursively. If any IO-related exceptions occur during enumeration or deletion, + /// they are silently ignored. + /// + /// The directory whose subdirectories should be deleted. + /// + /// This method is useful for cleanup operations where partial failure is acceptable. + /// It will not delete the root directory itself, only its subdirectories. + /// IO exceptions during directory enumeration or deletion are caught and ignored. + /// + internal static void DeleteSubdirectoriesNoThrow(string directory) + { + try + { + foreach (string dir in Directory.EnumerateDirectories(directory)) + { + DeleteDirectoryNoThrow(dir, recursive: true, retryCount: 1); + } + } + catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex)) + { + // If we can't enumerate the directories, ignore. Other cases should be handled by DeleteDirectoryNoThrow. + } + } +#endif + /// /// Determines whether the given assembly file name has one of the listed extensions. /// @@ -1603,4 +1632,4 @@ internal static void ReadExactly(this Stream stream, byte[] buffer, int offset, } } } -#endif \ No newline at end of file +#endif diff --git a/src/Utilities/TaskFactoryUtilities.cs b/src/Shared/TaskFactoryUtilities.cs similarity index 72% rename from src/Utilities/TaskFactoryUtilities.cs rename to src/Shared/TaskFactoryUtilities.cs index 45dd7c5db18..b25da2ee231 100644 --- a/src/Utilities/TaskFactoryUtilities.cs +++ b/src/Shared/TaskFactoryUtilities.cs @@ -4,11 +4,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; -using Microsoft.Build.Shared; -namespace Microsoft.Build.Utilities +namespace Microsoft.Build.Shared { /// /// Utilities class that provides common functionality for task factories such as @@ -25,8 +23,17 @@ namespace Microsoft.Build.Utilities /// 3. Loading assemblies based on execution mode (in-process vs out-of-process) /// 4. Assembly resolution for custom reference locations /// - public static class TaskFactoryUtilities + internal static class TaskFactoryUtilities { + + /// + /// The sub-path within the temporary directory where compiled inline tasks are located. + /// + public const string InlineTaskTempDllSubPath = nameof(InlineTaskTempDllSubPath); + public const string InlineTaskSuffix = "inline_task.dll"; + public const string InlineTaskLoadManifestSuffix = ".loadmanifest"; + + /// /// Creates a process-specific temporary directory for inline task assemblies. /// @@ -35,7 +42,7 @@ public static string CreateProcessSpecificTaskDirectory() { string processSpecificInlineTaskDir = Path.Combine( FileUtilities.TempFileDirectory, - MSBuildConstants.InlineTaskTempDllSubPath, + InlineTaskTempDllSubPath, $"pid_{EnvironmentUtilities.CurrentProcessId}"); Directory.CreateDirectory(processSpecificInlineTaskDir); @@ -59,7 +66,7 @@ public static string GetTemporaryTaskAssemblyPath() /// The path to the task assembly. /// The list of directories to include in the manifest. /// The path to the created manifest file. - public static string CreateLoadManifest(string assemblyPath, IEnumerable directoriesToAdd) + public static string CreateLoadManifest(string assemblyPath, List directoriesToAdd) { if (string.IsNullOrEmpty(assemblyPath)) { @@ -71,7 +78,7 @@ public static string CreateLoadManifest(string assemblyPath, IEnumerable throw new ArgumentNullException(nameof(directoriesToAdd)); } - string manifestPath = assemblyPath + ".loadmanifest"; + string manifestPath = assemblyPath + InlineTaskLoadManifestSuffix; File.WriteAllLines(manifestPath, directoriesToAdd); return manifestPath; } @@ -83,7 +90,7 @@ public static string CreateLoadManifest(string assemblyPath, IEnumerable /// The path to the task assembly. /// The list of reference assembly paths to extract directories from. /// The path to the created manifest file, or null if no valid directories were found. - public static string? CreateLoadManifestFromReferences(string assemblyPath, IEnumerable referenceAssemblyPaths) + public static string? CreateLoadManifestFromReferences(string assemblyPath, List referenceAssemblyPaths) { if (string.IsNullOrEmpty(assemblyPath)) { @@ -110,29 +117,30 @@ public static string CreateLoadManifest(string assemblyPath, IEnumerable /// Only includes directories for assemblies that actually exist on disk. /// /// The collection of assembly file paths. - /// A list of unique directory paths. - public static IList ExtractUniqueDirectoriesFromAssemblyPaths(IEnumerable assemblyPaths) + /// A list of unique directory paths in order of first occurrence. + public static List ExtractUniqueDirectoriesFromAssemblyPaths(List assemblyPaths) { if (assemblyPaths == null) { throw new ArgumentNullException(nameof(assemblyPaths)); } - var directories = new HashSet(StringComparer.OrdinalIgnoreCase); + var directories = new List(); + var seenDirectories = new HashSet(FileUtilities.PathComparer); foreach (string assemblyPath in assemblyPaths) { if (!string.IsNullOrEmpty(assemblyPath) && File.Exists(assemblyPath)) { string? directory = Path.GetDirectoryName(assemblyPath); - if (!string.IsNullOrEmpty(directory)) + if (!string.IsNullOrEmpty(directory) && seenDirectories.Add(directory)) { directories.Add(directory); } } } - return directories.ToList(); + return directories; } /// @@ -158,13 +166,49 @@ public static Assembly LoadTaskAssembly(string assemblyPath, bool useOutOfProces } } + /// + /// Registers assembly resolution handlers for inline tasks based on their load manifest file. + /// This enables out-of-process task execution to resolve dependencies that were identified + /// during TaskFactory initialization. + /// + /// The path to the task assembly. + /// True if a manifest was found and handlers were registered, false otherwise. + public static bool RegisterAssemblyResolveHandlersFromManifest(string taskLocation) + { + if (string.IsNullOrEmpty(taskLocation)) + { + throw new ArgumentException("Task location cannot be null or empty.", nameof(taskLocation)); + } + + if (!taskLocation.EndsWith(InlineTaskSuffix, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string manifestPath = taskLocation + InlineTaskLoadManifestSuffix; + if (!File.Exists(manifestPath)) + { + return false; + } + + string[] directories = File.ReadAllLines(manifestPath); + if (directories?.Length > 0) + { + var resolver = CreateAssemblyResolver(new List(directories)); + AppDomain.CurrentDomain.AssemblyResolve += resolver; + return true; + } + + return false; + } + /// /// Creates an assembly resolution event handler that can resolve assemblies from a list of directories. /// This is typically used for in-memory compiled task assemblies that have custom reference locations. /// /// The directories to search for assemblies. /// A ResolveEventHandler that can be used with AppDomain.CurrentDomain.AssemblyResolve. - public static ResolveEventHandler CreateAssemblyResolver(IList searchDirectories) + public static ResolveEventHandler CreateAssemblyResolver(List searchDirectories) { if (searchDirectories == null) { @@ -174,13 +218,22 @@ public static ResolveEventHandler CreateAssemblyResolver(IList searchDir return (sender, args) => TryLoadAssembly(searchDirectories, new AssemblyName(args.Name)); } + public static void CleanInlineTaskCaches() + { + // we can't clean our own cache because we have it loaded, but we can clean caches from prior runs + string inlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + InlineTaskTempDllSubPath); + FileUtilities.DeleteSubdirectoriesNoThrow(inlineTaskDir); + } + /// /// Attempts to load an assembly by searching in the specified directories. /// /// The directories to search in. /// The name of the assembly to load. /// The loaded assembly if found, otherwise null. - private static Assembly? TryLoadAssembly(IList directories, AssemblyName assemblyName) + private static Assembly? TryLoadAssembly(List directories, AssemblyName assemblyName) { foreach (string directory in directories) { diff --git a/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs b/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs similarity index 79% rename from src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs rename to src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs index 1799b50b90b..53a153494af 100644 --- a/src/Utilities.UnitTests/TaskFactoryUtilities_Tests.cs +++ b/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs @@ -26,7 +26,7 @@ public void CreateProcessSpecificTaskDirectory_ShouldCreateValidDirectory() // Assert directory.ShouldNotBeNull(); Directory.Exists(directory).ShouldBeTrue(); - directory.ShouldContain(MSBuildConstants.InlineTaskTempDllSubPath); + directory.ShouldContain(TaskFactoryUtilities.InlineTaskTempDllSubPath); directory.ShouldContain($"pid_{EnvironmentUtilities.CurrentProcessId}"); } @@ -39,23 +39,23 @@ public void GetTemporaryTaskAssemblyPath_ShouldReturnValidPath() // Assert assemblyPath.ShouldNotBeNull(); assemblyPath.ShouldEndWith(".dll"); - Path.GetDirectoryName(assemblyPath).ShouldContain(MSBuildConstants.InlineTaskTempDllSubPath); + Path.GetDirectoryName(assemblyPath).ShouldContain(TaskFactoryUtilities.InlineTaskTempDllSubPath); } [Fact] public void CreateLoadManifest_ShouldCreateFileWithDirectories() { - // Arrange - string tempAssemblyPath = Path.GetTempFileName(); - var directories = new List { "dir1", "dir2", "dir3" }; - - try + using (var env = TestEnvironment.Create()) { + // Arrange + var tempAssemblyFile = env.CreateFile(".dll"); + var directories = new List { "dir1", "dir2", "dir3" }; + // Act - string manifestPath = TaskFactoryUtilities.CreateLoadManifest(tempAssemblyPath, directories); + string manifestPath = TaskFactoryUtilities.CreateLoadManifest(tempAssemblyFile.Path, directories); // Assert - manifestPath.ShouldBe(tempAssemblyPath + ".loadmanifest"); + manifestPath.ShouldBe(tempAssemblyFile.Path + TaskFactoryUtilities.InlineTaskLoadManifestSuffix); File.Exists(manifestPath).ShouldBeTrue(); string[] manifestContent = File.ReadAllLines(manifestPath); @@ -64,18 +64,6 @@ public void CreateLoadManifest_ShouldCreateFileWithDirectories() manifestContent.ShouldContain("dir2"); manifestContent.ShouldContain("dir3"); } - finally - { - // Cleanup - if (File.Exists(tempAssemblyPath)) - { - File.Delete(tempAssemblyPath); - } - if (File.Exists(tempAssemblyPath + ".loadmanifest")) - { - File.Delete(tempAssemblyPath + ".loadmanifest"); - } - } } [Fact] diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index 48d3df90c79..b55cb830f0e 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -160,6 +160,7 @@ PlatformNegotiation.cs + diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 6d003a35ae5..fea1382744b 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -585,7 +585,7 @@ internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTask items = hasInvalidReference ? null : resolvedAssemblyReferences.Select(i => (ITaskItem)new TaskItem(i)).ToArray(); // Extract directories from resolved assemblies for assembly resolution and manifest creation - var directoriesToAddToAppDomain = TaskFactoryUtilities.ExtractUniqueDirectoriesFromAssemblyPaths(resolvedAssemblyReferences); + var directoriesToAddToAppDomain = TaskFactoryUtilities.ExtractUniqueDirectoriesFromAssemblyPaths(resolvedAssemblyReferences.ToList()); handlerAddedToAppDomain = TaskFactoryUtilities.CreateAssemblyResolver(directoriesToAddToAppDomain); AppDomain.CurrentDomain.AssemblyResolve += handlerAddedToAppDomain; @@ -598,7 +598,9 @@ internal bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTask } return !hasInvalidReference; - } private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDeclaration, string name, Type type, object defaultValue = null, bool isOutput = false, bool isRequired = false) + } + + private static CodeMemberProperty CreateProperty(CodeTypeDeclaration codeTypeDeclaration, string name, Type type, object defaultValue = null, bool isOutput = false, bool isRequired = false) { CodeMemberField field = new CodeMemberField(new CodeTypeReference(type), "_" + name) { From 89ed92d97d4e871c8c675353c418d89ce247a207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 26 Jun 2025 15:35:10 +0200 Subject: [PATCH 33/51] rm tmp directories inproc mode --- src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index fea1382744b..dc2751a7159 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -777,7 +777,8 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT if (!Traits.Instance.ForceTaskFactoryOutOfProc && FileSystems.Default.FileExists(_assemblyPath)) { - File.Delete(_assemblyPath); + // also delete the process-specific temporary directory containing the assembly + Directory.Delete(Path.GetDirectoryName(_assemblyPath), true); } } } From 2cf138d2d698546a9f366dccc59c79742644fe1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 26 Jun 2025 20:26:56 +0200 Subject: [PATCH 34/51] Revert "rm tmp directories inproc mode" This reverts commit 89ed92d97d4e871c8c675353c418d89ce247a207. --- src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index dc2751a7159..fea1382744b 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -777,8 +777,7 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT if (!Traits.Instance.ForceTaskFactoryOutOfProc && FileSystems.Default.FileExists(_assemblyPath)) { - // also delete the process-specific temporary directory containing the assembly - Directory.Delete(Path.GetDirectoryName(_assemblyPath), true); + File.Delete(_assemblyPath); } } } From c6e012c783c9ce9b3aec15494cc66275cc6a6cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 26 Jun 2025 20:38:30 +0200 Subject: [PATCH 35/51] revert dll location when not opted in --- .../RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index fea1382744b..45b61e2de88 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -666,8 +666,6 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT return true; } - _assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); - if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references)) { return false; @@ -677,6 +675,15 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT // the temp folder as well as the assembly. After build, the source code and assembly are deleted. string sourceCodePath = FileUtilities.GetTemporaryFileName(".tmp"); + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + _assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); // in a temp directory for this process, persisted until the end of build + } + else + { + _assemblyPath = FileUtilities.GetTemporaryFileName(".dll"); // dll in the root of the temp directory, removed immediately after compilation + } + // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT // is set (which allows for debugging problems) bool deleteSourceCodeFile = Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") == null; From 58246627c94bd7357c912dbe727051388fe57b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 1 Jul 2025 13:42:04 +0200 Subject: [PATCH 36/51] fix --- .../RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index 45b61e2de88..baf647a3e5f 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs @@ -666,11 +666,6 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT return true; } - if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references)) - { - return false; - } - // The source code cannot actually be compiled "in memory" so instead the source code is written to disk in // the temp folder as well as the assembly. After build, the source code and assembly are deleted. string sourceCodePath = FileUtilities.GetTemporaryFileName(".tmp"); @@ -684,6 +679,11 @@ private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryT _assemblyPath = FileUtilities.GetTemporaryFileName(".dll"); // dll in the root of the temp directory, removed immediately after compilation } + if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references)) + { + return false; + } + // Delete the code file unless compilation failed or the environment variable MSBUILDLOGCODETASKFACTORYOUTPUT // is set (which allows for debugging problems) bool deleteSourceCodeFile = Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") == null; From 0f28704ff081fa76aa848c3ef8a59bb3191571c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Thu, 3 Jul 2025 17:36:44 +0200 Subject: [PATCH 37/51] feedback --- .../wiki/MSBuild-Environment-Variables.md | 7 +---- documentation/wiki/Tasks.md | 2 +- src/Build/Resources/Strings.resx | 2 +- src/Build/Resources/xlf/Strings.cs.xlf | 4 +-- src/Build/Resources/xlf/Strings.de.xlf | 4 +-- src/Build/Resources/xlf/Strings.es.xlf | 4 +-- src/Build/Resources/xlf/Strings.fr.xlf | 4 +-- src/Build/Resources/xlf/Strings.it.xlf | 4 +-- src/Build/Resources/xlf/Strings.ja.xlf | 4 +-- src/Build/Resources/xlf/Strings.ko.xlf | 4 +-- src/Build/Resources/xlf/Strings.pl.xlf | 4 +-- src/Build/Resources/xlf/Strings.pt-BR.xlf | 4 +-- src/Build/Resources/xlf/Strings.ru.xlf | 4 +-- src/Build/Resources/xlf/Strings.tr.xlf | 4 +-- src/Build/Resources/xlf/Strings.zh-Hans.xlf | 4 +-- src/Build/Resources/xlf/Strings.zh-Hant.xlf | 4 +-- src/Framework/Traits.cs | 2 +- src/Shared/TaskFactoryUtilities.cs | 29 +++++++++++-------- src/Tasks.UnitTests/CodeTaskFactoryTests.cs | 24 +++++++-------- .../RoslynCodeTaskFactory_Tests.cs | 10 +++---- .../TaskFactoryUtilities_Tests.cs | 13 --------- src/Tasks/CodeTaskFactory.cs | 7 +---- 22 files changed, 65 insertions(+), 83 deletions(-) diff --git a/documentation/wiki/MSBuild-Environment-Variables.md b/documentation/wiki/MSBuild-Environment-Variables.md index 2dd75d3e4cd..76b73f435f4 100644 --- a/documentation/wiki/MSBuild-Environment-Variables.md +++ b/documentation/wiki/MSBuild-Environment-Variables.md @@ -26,16 +26,11 @@ Some of the env variables listed here are unsupported, meaning there is no guara - Launches debugger on build start. Works on Windows operating systems only. - Setting the value of 2 allows for manually attaching a debugger to a process ID. This works on Windows and non-Windows operating systems. - `MSBUILDDEBUGSCHEDULER=1` & `MSBUILDDEBUGPATH=` - - Dumps scheduler state at specified directory. - - `MsBuildSkipEagerWildCardEvaluationRegexes` - - If specified, overrides the default behavior of glob expansion. During glob expansion, if the path with wildcards that is being processed matches one of the regular expressions provided in the [environment variable](#msbuildskipeagerwildcardevaluationregexes), the path is not processed (expanded). - The value of the environment variable is a list of regular expressions, separated by semicolon (;). - - `MSBUILDFORCEALLTASKSOUTOFPROCESS` - Set this to force all tasks to run out of process (except inline tasks). - -- `MSBUILDFORCETASKFACTORYOUTOFPROC` +- `MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC` - Set this to force all inline tasks to run out of process. It is not compatible with custom TaskFactories. diff --git a/documentation/wiki/Tasks.md b/documentation/wiki/Tasks.md index d63ba9e1691..1869ceb401c 100644 --- a/documentation/wiki/Tasks.md +++ b/documentation/wiki/Tasks.md @@ -45,7 +45,7 @@ Task factories create instances of tasks. They implement [`ITaskFactory`](https: This interface defines `bool Initialize(...)` and `ITask CreateTask(...)`. They are e.g. responsible for loading a task from an assembly and initializing it. -The trait `MSBUILDFORCETASKFACTORYOUTOFPROC` allows running inline tasks in an out of process TaskHost and is not compatible with custom TaskFactories. +The trait `MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC` allows running inline tasks in an out of process TaskHost and is not compatible with custom TaskFactories. ### Built-in Task Factories diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 41a2bb653f5..62410e4cf35 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2399,7 +2399,7 @@ Utilization: {0} Average Utilization: {1:###.0} Loading telemetry libraries failed with exception: {0}. - Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. + Custom TaskFactory '{0}' for Task '{1}' does not support out of process TaskHost execution. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files.