diff --git a/documentation/wiki/MSBuild-Environment-Variables.md b/documentation/wiki/MSBuild-Environment-Variables.md index 1a1ddac8187..76b73f435f4 100644 --- a/documentation/wiki/MSBuild-Environment-Variables.md +++ b/documentation/wiki/MSBuild-Environment-Variables.md @@ -1,33 +1,36 @@ # 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). +- `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 76bd3f9bb14..a8fd0ea67d8 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 `MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC` forces inline tasks in an out of process TaskHost. It 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 8842719bacb..6168b53a970 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1148,6 +1148,11 @@ public void EndBuild() Reset(); _buildManagerState = BuildManagerState.Idle; + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + } + MSBuildEventSource.Log.BuildStop(); _threadException?.Throw(); diff --git a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs index da42433edec..585f4a64d44 100644 --- a/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs +++ b/src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs @@ -980,14 +980,37 @@ private ITask InstantiateTask(IDictionary taskIdentityParameters else { TaskFactoryLoggingHost loggingHost = new TaskFactoryLoggingHost(_buildEngine.IsRunningMultipleNodes, _taskLocation, _taskLoggingContext); + bool isTaskHost = false; try { - task = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? - taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : - _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + // Check if we should force out-of-process execution for non-AssemblyTaskFactory instances + // 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 outOfProcTaskFactory) + { + _taskLoggingContext.LogError( + new BuildEventFileInfo(_taskLocation), + "CustomTaskFactoryOutOfProcNotSupported", + _taskFactoryWrapper.TaskFactory.FactoryName, + _taskName); + return null; + } + + task = CreateTaskHostTaskForOutOfProcFactory(taskIdentityParameters, loggingHost, outOfProcTaskFactory); + isTaskHost = true; + } + else + { + // Normal in-process execution for custom task factories + task = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? + taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : + _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + } - // Track telemetry for non-AssemblyTaskFactory task factories. No task can go to the task host. - _taskLoggingContext?.TargetLoggingContext?.ProjectLoggingContext?.ProjectTelemetry?.AddTaskExecution(_taskFactoryWrapper.TaskFactory.GetType().FullName, isTaskHost: false); + // Track telemetry for non-AssemblyTaskFactory task factories + _taskLoggingContext?.TargetLoggingContext?.ProjectLoggingContext?.ProjectTelemetry?.AddTaskExecution(_taskFactoryWrapper.TaskFactory.GetType().FullName, isTaskHost); } finally { @@ -1703,5 +1726,78 @@ 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. } } + + /// + /// Creates a wrapper to run a non-AssemblyTaskFactory task out of process. + /// This is used when Traits.Instance.ForceTaskFactoryOutOfProc is true to ensure + /// non-AssemblyTaskFactory tasks run in isolation. + /// + /// Task identity parameters. + /// The logging host to use for the task. + /// The out-of-process task factory instance. + /// A TaskHostTask that will execute the inner task out of process, or null if task creation fails. + private ITask CreateTaskHostTaskForOutOfProcFactory(IDictionary taskIdentityParameters, TaskFactoryLoggingHost loggingHost, IOutOfProcTaskFactory outOfProcTaskFactory) + { + ITask innerTask; + + innerTask = _taskFactoryWrapper.TaskFactory is ITaskFactory2 taskFactory2 ? + taskFactory2.CreateTask(loggingHost, taskIdentityParameters) : + _taskFactoryWrapper.TaskFactory.CreateTask(loggingHost); + + if (innerTask == null) + { + return null; + } + + // Create a LoadedType for the actual task type so we can wrap it in TaskHostTask + Type taskType = innerTask.GetType(); + + // For out-of-process inline tasks, get the assembly path from the factory + // (Assembly.Location is typically empty for inline tasks loaded from bytes) + string resolvedAssemblyLocation = outOfProcTaskFactory.GetAssemblyPath(); + + // This should never happen - if the factory can create a task, it should know where the assembly is + ErrorUtilities.VerifyThrow(!string.IsNullOrEmpty(resolvedAssemblyLocation), + $"IOutOfProcTaskFactory {_taskFactoryWrapper.TaskFactory.FactoryName} created a task but returned null/empty assembly path"); + + LoadedType taskLoadedType = new LoadedType( + taskType, + AssemblyLoadInfo.Create(null, resolvedAssemblyLocation), + taskType.Assembly, + typeof(ITaskItem)); + + // Default task host parameters for out-of-process execution for inline tasks + Dictionary taskHostParameters = new Dictionary + { + [XMakeAttributes.runtime] = XMakeAttributes.GetCurrentMSBuildRuntime(), + [XMakeAttributes.architecture] = XMakeAttributes.GetCurrentMSBuildArchitecture() + }; + + // Merge with any existing task identity parameters + if (taskIdentityParameters?.Count > 0) + { + 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(innerTask); + +#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter + return new TaskHostTask( + _taskLocation, + _taskLoggingContext, + _buildComponentHost, + taskHostParameters, + taskLoadedType, + true +#if FEATURE_APPDOMAIN + , AppDomainSetup +#endif + ); +#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter + } } } diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index be7a41298e9..a53a52f7c56 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -277,6 +277,13 @@ public bool Execute() ErrorUtilities.VerifyThrowInternalNull(_taskHostProvider, "taskHostProvider"); } + string taskLocation = AssemblyUtilities.GetAssemblyLocation(_taskType.Type.GetTypeInfo().Assembly); + if (string.IsNullOrEmpty(taskLocation)) + { + // fall back to the AssemblyLoadInfo location for inline tasks loaded from bytes + taskLocation = _taskType?.Assembly?.AssemblyLocation ?? string.Empty; + } + TaskHostConfiguration hostConfiguration = new TaskHostConfiguration( _buildComponentHost.BuildParameters.NodeId, @@ -292,7 +299,7 @@ public bool Execute() BuildEngine.ProjectFileOfTaskNode, BuildEngine.ContinueOnError, _taskType.Type.FullName, - AssemblyUtilities.GetAssemblyLocation(_taskType.Type.GetTypeInfo().Assembly), + taskLocation, _buildComponentHost.BuildParameters.LogTaskInputs, _setParameters, new Dictionary(_buildComponentHost.BuildParameters.GlobalProperties), diff --git a/src/Build/Instance/TaskRegistry.cs b/src/Build/Instance/TaskRegistry.cs index ef60ac899ce..acbc494faae 100644 --- a/src/Build/Instance/TaskRegistry.cs +++ b/src/Build/Instance/TaskRegistry.cs @@ -1277,7 +1277,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. diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index a1cae1ad8cb..09689ae406e 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -95,6 +95,7 @@ Collections\ReadOnlyEmptyCollection.cs + diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 83dd0ac7d07..ba039159363 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2433,6 +2433,9 @@ 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + The task failed to load because it requires the MSBuild .NET Runtime Task Host, but the .NET Runtime Task Host could not be found for the specified version. See https://aka.ms/nettaskhost for details on how to resolve this error. diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index 38887c86af2..2ac5ddb9379 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 8d391df1fee..a9632bd55ed 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 fb48065a368..d983ca07a3d 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 35383a1e6af..20844d3c2f9 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 1bc1a5b494f..b6e21c9f47e 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 0a3aecf356a..63e0e5a0b80 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 c4de33c7e66..ef035360206 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 f7833c69dbf..84784a7fc5a 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 f2518068b1a..9f2abb2afcf 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 caf1ee1d68d..7cc0df5c95b 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 fd1c26bb59a..9f46107b8eb 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 4e0b060d4b3..4bee356aee1 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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 e2ae7fa5983..d85f6c9305b 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. Turn off the multithreaded build mode or remove the custom TaskFactory from your <UsingTask> definitions in project files. + 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. + + 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/Framework/IOutOfProcTaskFactory.cs b/src/Framework/IOutOfProcTaskFactory.cs new file mode 100644 index 00000000000..357fb72c5b6 --- /dev/null +++ b/src/Framework/IOutOfProcTaskFactory.cs @@ -0,0 +1,21 @@ +// 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; + +/// +/// Marker interface for Task Factory which creates tasks in a way compatible with out-of-process execution. +/// Currently only TaskFactories shipped with MSBuild support out-of-process execution. +/// They are marked with this intrerface to distinguish them from exterally defined TaskFactories. +/// +internal interface IOutOfProcTaskFactory +{ + /// + /// Returns the file system path of the task assembly produced by the factory, if available. + /// + /// + /// Implementations should return an absolute path when out-of-proc execution is enabled. When not applicable, + /// they may return . + /// + string? GetAssemblyPath(); +} diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 141694a0093..8cbf21feef1 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -142,6 +142,11 @@ public Traits() public readonly bool InProcNodeDisabled = Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") == "1"; + /// + /// Forces execution of tasks coming from a different TaskFactory than AssemblyTaskFactory out of proc. + /// + public readonly bool ForceTaskFactoryOutOfProc = Environment.GetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC") == "1"; + /// /// Variables controlling opt out at the level of not initializing telemetry infrastructure. Set to "1" or "true" to opt out. /// mirroring diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index cc3aab3c9cd..2af449ea80a 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -108,6 +108,7 @@ + diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 3c0ef2d9ed7..1d4d088a30d 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -944,7 +944,9 @@ private void RunTask(object state) string taskName = taskConfiguration.TaskName; string taskLocation = taskConfiguration.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(); diff --git a/src/Shared/AssemblyLoadInfo.cs b/src/Shared/AssemblyLoadInfo.cs index be467ff2a20..4ced73e1360 100644 --- a/src/Shared/AssemblyLoadInfo.cs +++ b/src/Shared/AssemblyLoadInfo.cs @@ -63,6 +63,14 @@ internal abstract string AssemblyLocation get; } + /// + /// Gets whether this assembly is an inline task assembly that should be loaded from bytes to avoid file locking. + /// + internal abstract bool IsInlineTask + { + get; + } + /// /// Computes a hashcode for this assembly info, so this object can be used as a key into /// a hash table. @@ -158,6 +166,15 @@ internal override string AssemblyLocation { get { return _assemblyName; } } + + /// + /// Gets whether this assembly is an inline task assembly. + /// Assembly names are never inline tasks. + /// + internal override bool IsInlineTask + { + get { return false; } + } } /// @@ -204,6 +221,19 @@ internal override string AssemblyLocation { get { return _assemblyFile; } } + + /// + /// Gets whether this assembly is an inline task assembly. + /// Detects inline tasks by checking if the file path ends with the inline task suffix. + /// + internal override bool IsInlineTask + { +#if !NET35 + get { return _assemblyFile?.EndsWith(TaskFactoryUtilities.InlineTaskSuffix, StringComparison.OrdinalIgnoreCase) == true; } +#else + get { return false; } +#endif + } } } } 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/Shared/LoadedType.cs b/src/Shared/LoadedType.cs index ee5b7d4e490..27405b89265 100644 --- a/src/Shared/LoadedType.cs +++ b/src/Shared/LoadedType.cs @@ -36,7 +36,12 @@ internal LoadedType(Type type, AssemblyLoadInfo assemblyLoadInfo, Assembly loade HasSTAThreadAttribute = CheckForHardcodedSTARequirement(); LoadedAssemblyName = loadedAssembly.GetName(); - Path = loadedAssembly.Location; + + // For inline tasks loaded from bytes, Assembly.Location is empty, so use the original path + Path = string.IsNullOrEmpty(loadedAssembly.Location) + ? assemblyLoadInfo.AssemblyLocation + : loadedAssembly.Location; + LoadedAssembly = loadedAssembly; #if !NET35 diff --git a/src/Shared/TaskFactoryUtilities.cs b/src/Shared/TaskFactoryUtilities.cs new file mode 100644 index 00000000000..618822b3bb1 --- /dev/null +++ b/src/Shared/TaskFactoryUtilities.cs @@ -0,0 +1,289 @@ +// 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; + +namespace Microsoft.Build.Shared +{ + /// + /// 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 + /// + 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"; + + /// + /// Represents a cached assembly entry for task factories with validation support. + /// + public readonly struct CachedAssemblyEntry + { + public CachedAssemblyEntry(Assembly assembly, string assemblyPath) + { + Assembly = assembly; + AssemblyPath = assemblyPath; + } + + public Assembly Assembly { get; } + + public string AssemblyPath { get; } + + /// + /// Validates that the cached assembly is still usable. + /// For out-of-process scenarios (when AssemblyPath is specified), validates the file exists. + /// For in-process scenarios (when AssemblyPath is empty), always considers valid. + /// + public bool IsValid => string.IsNullOrEmpty(AssemblyPath) || FileUtilities.FileExistsNoThrow(AssemblyPath); + } + + + /// + /// Creates a process-specific temporary directory for inline task assemblies. + /// + /// The path to the created temporary directory. + public static string CreateProcessSpecificTemporaryTaskDirectory() + { + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + 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 full path to the temporary file. + public static string GetTemporaryTaskAssemblyPath() + { + string taskDir = CreateProcessSpecificTemporaryTaskDirectory(); + return FileUtilities.GetTemporaryFile(taskDir, fileName: null, extension: "inline_task.dll", createFile: 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, List 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 + InlineTaskLoadManifestSuffix; + 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, List 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 in order of first occurrence. + public static List ExtractUniqueDirectoriesFromAssemblyPaths(List assemblyPaths) + { + if (assemblyPaths == null) + { + throw new ArgumentNullException(nameof(assemblyPaths)); + } + + 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) && seenDirectories.Add(directory)) + { + directories.Add(directory); + } + } + } + + return directories; + } + + /// + /// Loads an assembly from the specified path. + /// + /// The path to the assembly to load. + /// The loaded assembly. + public static Assembly LoadTaskAssembly(string assemblyPath) + { + if (string.IsNullOrEmpty(assemblyPath)) + { + throw new ArgumentException("Assembly path cannot be null or empty.", nameof(assemblyPath)); + } + + // Load the assembly from bytes so we don't lock the file and record its original path for out-of-proc hosts + Assembly assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); + return assembly; + } + + + /// + /// 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. + public static void 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; + } + + string manifestPath = taskLocation + InlineTaskLoadManifestSuffix; + if (!File.Exists(manifestPath)) + { + return; + } + + string[] directories = File.ReadAllLines(manifestPath); + if (directories?.Length > 0) + { + ResolveEventHandler resolver = CreateAssemblyResolver([.. directories]); + AppDomain.CurrentDomain.AssemblyResolve += resolver; + } + } + + /// + /// 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(List searchDirectories) + { + if (searchDirectories == null) + { + throw new ArgumentNullException(nameof(searchDirectories)); + } + + return (sender, args) => TryLoadAssembly(searchDirectories, new AssemblyName(args.Name)); + } + + /// + /// Cleans up the current process's inline task directory by deleting the temporary directory + /// and its contents used for inline task assemblies for this specific process. + /// This should be called at the end of a build to prevent dangling DLL files. + /// + /// + /// On Windows platforms, this may fail to delete files that are still locked by the current process. + /// However, it will clean up any files that are no longer in use. + /// + public static void CleanCurrentProcessInlineTaskDirectory() + { + string processSpecificInlineTaskDir = Path.Combine( + FileUtilities.TempFileDirectory, + InlineTaskTempDllSubPath, + $"pid_{EnvironmentUtilities.CurrentProcessId}"); + + if (Directory.Exists(processSpecificInlineTaskDir)) + { + FileUtilities.DeleteDirectoryNoThrow(processSpecificInlineTaskDir, recursive: true); + } + } + + /// + /// 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(List 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.Load(File.ReadAllBytes(path)); + } + } + + // Try the standard path + path = Path.Combine(directory, assemblyName.Name + ".dll"); + if (File.Exists(path)) + { + return Assembly.Load(File.ReadAllBytes(path)); + } + } + + return null; + } + } +} diff --git a/src/Shared/TypeLoader.cs b/src/Shared/TypeLoader.cs index f96c218f22b..a4bd6f6ec28 100644 --- a/src/Shared/TypeLoader.cs +++ b/src/Shared/TypeLoader.cs @@ -163,6 +163,11 @@ private static Assembly LoadAssembly(AssemblyLoadInfo assemblyLoadInfo) { return Assembly.Load(assemblyLoadInfo.AssemblyName); } + else if (assemblyLoadInfo.IsInlineTask) + { + // Load inline task assemblies from bytes and register the path + return TaskFactoryUtilities.LoadTaskAssembly(assemblyLoadInfo.AssemblyFile); + } else { #if !FEATURE_ASSEMBLYLOADCONTEXT diff --git a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs index c278fc1bf81..078daa6021e 100644 --- a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs +++ b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs @@ -26,28 +26,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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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!"); + } } /// @@ -56,29 +67,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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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"); + } } /// @@ -87,29 +109,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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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"); + } } /// @@ -119,29 +152,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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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"); + } } /// @@ -486,62 +530,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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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("[[]]"); + } } /// @@ -637,46 +703,113 @@ 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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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); + + + + + + + "; + + MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); + mockLogger.AssertLogContains("OK" + ":Hello, World!"); } + } - string projectFileContents = @" - - - - - - - - - - string netString = System.Net.HttpStatusCode.OK.ToString(); - Log.LogMessage(MessageImportance.High, netString + Text); - - - - - - - "; + [Fact] + public void OutOfProcCodeTaskFactoryCachesAssemblyPath() + { + using var env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); - MockLogger mockLogger = Helpers.BuildProjectWithNewOMExpectSuccess(projectFileContents); - mockLogger.AssertLogContains("OK" + ":Hello, World!"); + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + + try + { + const string taskElementContents = @" + Log.LogMessage(""inline execution""); + return true; + "; + + var firstFactory = new Microsoft.Build.Tasks.CodeTaskFactory(); + var firstEngine = new MockEngine(); + bool initialized = firstFactory.Initialize( + "CachedCodeInlineTask", + new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase), + taskElementContents, + firstEngine); + initialized.ShouldBeTrue(firstEngine.Log); + + ITask firstTask = firstFactory.CreateTask(firstEngine); + firstTask.ShouldNotBeNull(); + + string firstAssemblyPath = ((IOutOfProcTaskFactory)firstFactory).GetAssemblyPath(); + firstAssemblyPath.ShouldNotBeNullOrEmpty(); + File.Exists(firstAssemblyPath).ShouldBeTrue(); + + firstFactory.CleanupTask(firstTask); + + var secondFactory = new Microsoft.Build.Tasks.CodeTaskFactory(); + var secondEngine = new MockEngine(); + bool initializedAgain = secondFactory.Initialize( + "CachedCodeInlineTask", + new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase), + taskElementContents, + secondEngine); + initializedAgain.ShouldBeTrue(secondEngine.Log); + + ITask secondTask = secondFactory.CreateTask(secondEngine); + secondTask.ShouldNotBeNull(); + + string reusedAssemblyPath = ((IOutOfProcTaskFactory)secondFactory).GetAssemblyPath(); + reusedAssemblyPath.ShouldBe(firstAssemblyPath); + File.Exists(reusedAssemblyPath).ShouldBeTrue(); + + secondFactory.CleanupTask(secondTask); + } + finally + { + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + } } /// @@ -1102,80 +1235,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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + } + + string taskName = $"HelloTask{num}"; - public class {{taskName}} : Task + 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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + } + + string taskXml = @" "; - CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildAndCheckForEmbeddedFileInBinlog( - FactoryType.CodeTaskFactory, "HelloTask", taskXml, false); + CodeTaskFactoryEmbeddedFileInBinlogTestHelper.BuildAndCheckForEmbeddedFileInBinlog( + FactoryType.CodeTaskFactory, $"HelloTask{num}", taskXml, false); + } } [Fact] diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs index ab44840f780..655480b8efb 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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "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("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + } + TransientTestFolder folder = env.CreateFolder(createFolder: true); TransientTestFile assemblyProj = env.CreateFile(folder, "5106.csproj", @$" @@ -143,14 +157,17 @@ public static string ToPrint() { } } - [Fact] - public void RoslynCodeTaskFactory_ReuseCompilation() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) { + int num = forceOutOfProc ? 1 : 2; string text1 = $@" @@ -166,8 +183,8 @@ public void RoslynCodeTaskFactory_ReuseCompilation() - - + + "; @@ -176,7 +193,7 @@ public void RoslynCodeTaskFactory_ReuseCompilation() @@ -191,13 +208,17 @@ public void RoslynCodeTaskFactory_ReuseCompilation() - - + + "; using var env = TestEnvironment.Create(); + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + } var p2 = env.CreateTestProjectWithFiles("p2.proj", text2); text1 = text1.Replace("p2.proj", p2.ProjectFile); @@ -214,6 +235,63 @@ public void RoslynCodeTaskFactory_ReuseCompilation() messages.Length.ShouldBe(1); } + [Fact] + public void OutOfProcRoslynTaskFactoryCachesAssemblyPath() + { + using var env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + + try + { + const string taskBody = @" + + Log.LogMessage(""inline execution""); + "; + + var firstFactory = new RoslynCodeTaskFactory(); + var firstEngine = new MockEngine(); + bool initialized = firstFactory.Initialize( + "CachedRoslynInlineTask", + new Dictionary(StringComparer.OrdinalIgnoreCase), + taskBody, + firstEngine); + initialized.ShouldBeTrue(firstEngine.Log); + + ITask firstTask = firstFactory.CreateTask(firstEngine); + firstTask.ShouldNotBeNull(); + + string firstAssemblyPath = ((IOutOfProcTaskFactory)firstFactory).GetAssemblyPath(); + firstAssemblyPath.ShouldNotBeNullOrEmpty(); + File.Exists(firstAssemblyPath).ShouldBeTrue(); + + firstFactory.CleanupTask(firstTask); + + var secondFactory = new RoslynCodeTaskFactory(); + var secondEngine = new MockEngine(); + bool initializedAgain = secondFactory.Initialize( + "CachedRoslynInlineTask", + new Dictionary(StringComparer.OrdinalIgnoreCase), + taskBody, + secondEngine); + initializedAgain.ShouldBeTrue(secondEngine.Log); + + ITask secondTask = secondFactory.CreateTask(secondEngine); + secondTask.ShouldNotBeNull(); + + string reusedAssemblyPath = ((IOutOfProcTaskFactory)secondFactory).GetAssemblyPath(); + reusedAssemblyPath.ShouldBe(firstAssemblyPath); + File.Exists(reusedAssemblyPath).ShouldBeTrue(); + + secondFactory.CleanupTask(secondTask); + } + finally + { + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + } + } + [Fact] public void VisualBasicFragment() { @@ -666,8 +744,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"; @@ -701,6 +781,10 @@ public override bool Execute() using (TestEnvironment env = TestEnvironment.Create()) { + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + } TransientTestProjectWithFiles proj = env.CreateTestProjectWithFiles(projectContent); var logger = proj.BuildProjectExpectFailure(); logger.AssertLogContains(errorMessage); @@ -783,14 +867,17 @@ 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) { + int num = forceOutOfProc ? 3 : 4; string text = $@" @@ -807,12 +894,17 @@ public void RoslynCodeTaskFactory_UsingAPI() - + "; using var env = TestEnvironment.Create(); + if (forceOutOfProc) + { + env.SetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + } + RunnerUtilities.ApplyDotnetHostPathEnvironmentVariable(env); var dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); 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 diff --git a/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs b/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs new file mode 100644 index 00000000000..1bc4386371e --- /dev/null +++ b/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs @@ -0,0 +1,68 @@ +// 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 GetTemporaryTaskAssemblyPath_ShouldReturnValidPath() + { + // Act + string assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); + + // Assert + assemblyPath.ShouldNotBeNull(); + assemblyPath.ShouldEndWith(".dll"); + Path.GetDirectoryName(assemblyPath).ShouldContain(TaskFactoryUtilities.InlineTaskTempDllSubPath); + } + + [Fact] + public void CreateLoadManifest_ShouldCreateFileWithDirectories() + { + using (var env = TestEnvironment.Create()) + { + // Arrange + var tempAssemblyFile = env.CreateFile(".dll"); + var directories = new List { "dir1", "dir2" }; + + // Act + string manifestPath = TaskFactoryUtilities.CreateLoadManifest(tempAssemblyFile.Path, directories); + + // Assert + manifestPath.ShouldBe(tempAssemblyFile.Path + TaskFactoryUtilities.InlineTaskLoadManifestSuffix); + File.Exists(manifestPath).ShouldBeTrue(); + + string[] manifestContent = File.ReadAllLines(manifestPath); + manifestContent.Length.ShouldBe(2); + manifestContent.ShouldContain("dir1"); + manifestContent.ShouldContain("dir2"); + } + } + + [Fact] + public void CreateAssemblyResolver_ShouldReturnValidHandler() + { + // Arrange + var directories = new List { Environment.CurrentDirectory }; + + // Act + ResolveEventHandler handler = TaskFactoryUtilities.CreateAssemblyResolver(directories); + + // Assert + handler.ShouldNotBeNull(); + } + } +} diff --git a/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs b/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs index 8fcd77f7d89..efb91810d5b 100644 --- a/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs @@ -12,6 +12,9 @@ using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Tasks.Xaml; +using Microsoft.Build.Tasks; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; using Microsoft.CSharp; using Shouldly; using Xunit; @@ -444,6 +447,46 @@ public void TestStringArrayPropertyWithDataSource_DataSourceIsItem() public class CompilationTests { + [Fact] + public void OutOfProcXamlTaskFactoryProvidesAssemblyPath() + { + using TestEnvironment env = TestEnvironment.Create(); + env.SetEnvironmentVariable("MSBUILDFORCEINLINETASKFACTORIESOUTOFPROC", "1"); + + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + + try + { + const string taskElementContents = @" + + + +"; + + var factory = new Microsoft.Build.Tasks.XamlTaskFactory(); + var loggingHost = new MockEngine(); + bool initialized = factory.Initialize( + "FakeTask", + new Dictionary(StringComparer.OrdinalIgnoreCase), + taskElementContents, + loggingHost); + initialized.ShouldBeTrue(loggingHost.Log); + + ITask task = factory.CreateTask(loggingHost); + task.ShouldNotBeNull(); + + string assemblyPath = factory.GetAssemblyPath(); + assemblyPath.ShouldNotBeNullOrEmpty(); + File.Exists(assemblyPath).ShouldBeTrue(); + + factory.CleanupTask(task); + } + finally + { + TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); + } + } + /// /// Tests to see if the generated stream compiles /// Code must be compilable on its own. diff --git a/src/Tasks/CodeTaskFactory.cs b/src/Tasks/CodeTaskFactory.cs index 07310c0c740..9b07f991da8 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 @@ -84,7 +84,7 @@ private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEve /// A collection of task assemblies which have been instantiated by any CodeTaskFactory. Used to prevent us from creating /// duplicate assemblies. /// - private static readonly ConcurrentDictionary s_compiledTaskCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_compiledTaskCache = new ConcurrentDictionary(); /// /// Merged set of assembly reference paths (default + specified) @@ -161,6 +161,8 @@ private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEve /// public Type TaskType { get; private set; } + public string GetAssemblyPath() => _assemblyPath; + /// /// Get the type information for all task parameters. /// @@ -271,7 +273,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. @@ -350,7 +352,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 +374,16 @@ private static void CreateProperty(CodeTypeDeclaration ctd, string propertyName, HasSet = true }; + if (isOutput) + { + prop.CustomAttributes.Add(new CodeAttributeDeclaration("Microsoft.Build.Framework.Output")); + } + + 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); } /// @@ -598,6 +610,11 @@ 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. @@ -706,8 +723,10 @@ 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() { + _assemblyPath = null; + // Combine our default assembly references with those specified var finalReferencedAssemblies = CombineReferencedAssemblies(); @@ -729,8 +748,8 @@ private Assembly CompileInMemoryAssembly() // We don't need debug information IncludeDebugInformation = true, - // Not a file based assembly - GenerateInMemory = true, + GenerateInMemory = !Traits.Instance.ForceTaskFactoryOutOfProc, + OutputAssembly = _assemblyPath, // Indicates that a .dll should be generated. GenerateExecutable = false @@ -792,44 +811,113 @@ private Assembly CompileInMemoryAssembly() string fullCode = codeBuilder.ToString(); var fullSpec = new FullTaskSpecification(finalReferencedAssemblies, fullCode); - if (!s_compiledTaskCache.TryGetValue(fullSpec, out Assembly existingAssembly)) + + // Try to get from cache + if (s_compiledTaskCache.TryGetValue(fullSpec, out TaskFactoryUtilities.CachedAssemblyEntry cachedEntry)) { - // Invokes compilation. - CompilerResults compilerResults = provider.CompileAssemblyFromSource(compilerParameters, fullCode); - - // Embed generated file in the binlog - string fileNameInBinlog = $"{Guid.NewGuid()}-{_nameOfTask}-compilation-file.tmp"; - _log.LogIncludeGeneratedFile(fileNameInBinlog, fullCode); - - string outputPath = null; - if (compilerResults.Errors.Count > 0 || Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") != null) + Assembly existingAssembly = cachedEntry.Assembly; + + if (Traits.Instance.ForceTaskFactoryOutOfProc) { - string tempDirectory = FileUtilities.TempFileDirectory; - string fileName = Guid.NewGuid().ToString() + ".txt"; - outputPath = Path.Combine(tempDirectory, fileName); - File.WriteAllText(outputPath, fullCode); + string cachedPath = cachedEntry.AssemblyPath; + if (!string.IsNullOrEmpty(cachedPath)) + { + // For out-of-process, validate the assembly file still exists + if (!FileUtilities.FileExistsNoThrow(cachedPath)) + { + // Cached assembly file was deleted, remove from cache and recompile + s_compiledTaskCache.TryRemove(fullSpec, out _); + } + else + { + _assemblyPath = cachedPath; + // Assembly exists, assume manifest exists too if it was created + // If manifest is missing, out-of-process execution will handle the error gracefully + return existingAssembly; + } + } + else + { + // This should never happen: we're in out-of-process mode but have a cached entry without a file path. + // When in out-of-process mode, _assemblyPath is always set before compilation. + ErrorUtilities.ThrowInternalError("Cached assembly entry has no file path in out-of-process mode"); + } } - - if (compilerResults.NativeCompilerReturnValue != 0 && compilerResults.Errors.Count > 0) + else { - _log.LogErrorWithCodeFromResources("CodeTaskFactory.FindSourceFileAt", outputPath); + // In-process scenario - always use cached assembly without file validation + return existingAssembly; + } + } - foreach (CompilerError e in compilerResults.Errors) - { - _log.LogErrorWithCodeFromResources("CodeTaskFactory.CompilerError", e.ToString()); - } + // Proceed with compilation + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + _assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); + compilerParameters.OutputAssembly = _assemblyPath; + } + + // 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); - return null; + // Embed generated file in the binlog + string fileNameInBinlog = $"{Guid.NewGuid()}-{_nameOfTask}-compilation-file.tmp"; + _log.LogIncludeGeneratedFile(fileNameInBinlog, fullCode); + + string outputPath = null; + if (compilerResults.Errors.Count > 0 || Environment.GetEnvironmentVariable("MSBUILDLOGCODETASKFACTORYOUTPUT") != null) + { + string tempDirectory = FileUtilities.TempFileDirectory; + string fileName = Guid.NewGuid().ToString() + ".txt"; + outputPath = Path.Combine(tempDirectory, fileName); + File.WriteAllText(outputPath, fullCode); + } + + if (compilerResults.NativeCompilerReturnValue != 0 && compilerResults.Errors.Count > 0) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.FindSourceFileAt", outputPath); + + foreach (CompilerError error in compilerResults.Errors) + { + _log.LogErrorWithCodeFromResources("CodeTaskFactory.CompilerError", error.ToString()); } + + return null; + } + + // Log the location of the code file. + if (outputPath != null) + { + _log.LogMessageFromResources(MessageImportance.Low, "CodeTaskFactory.FindSourceFileAt", outputPath); + } - // Add to the cache. Failing to add is not a fatal error. - s_compiledTaskCache.TryAdd(fullSpec, compilerResults.CompiledAssembly); - return compilerResults.CompiledAssembly; + Assembly assembly; + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + if (!string.IsNullOrEmpty(_assemblyPath)) + { + TaskFactoryUtilities.CreateLoadManifestFromReferences(_assemblyPath, finalReferencedAssemblies); + assembly = TaskFactoryUtilities.LoadTaskAssembly(_assemblyPath); + } + else + { + assembly = compilerResults.CompiledAssembly; + } } else { - return existingAssembly; + assembly = compilerResults.CompiledAssembly; } + + // Add to the cache. Failing to add is not a fatal error. + string cachedAssemblyPath = Traits.Instance.ForceTaskFactoryOutOfProc ? _assemblyPath : string.Empty; + s_compiledTaskCache.TryAdd(fullSpec, new TaskFactoryUtilities.CachedAssemblyEntry(assembly, cachedAssemblyPath)); + return assembly; } } diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index add19b10093..e6b2992c0d7 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -161,6 +161,7 @@ PlatformNegotiation.cs + diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactory.cs index ceebdc11c2c..7304e5debd5 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 @@ -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. @@ -96,6 +96,8 @@ public sealed class RoslynCodeTaskFactory : ITaskFactory /// private TaskLoggingHelper _log; + private string _assemblyPath; + /// /// Stores functions that were added to the current app domain. Should be removed once we're finished. /// @@ -142,6 +144,9 @@ public TaskPropertyInfo[] GetTaskParameters() return _parameters; } + /// + public string GetAssemblyPath() => _assemblyPath; + /// public bool Initialize(string taskName, IDictionary parameterGroup, string taskBody, IBuildEngine taskFactoryLoggingHost) { @@ -162,7 +167,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 Assembly assembly)) { return false; } @@ -220,7 +225,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) @@ -542,8 +547,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) { @@ -552,7 +555,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; } @@ -585,37 +587,23 @@ 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)); - AppDomain.CurrentDomain.AssemblyResolve += handlerAddedToAppDomain; + // Extract directories from resolved assemblies for assembly resolution and manifest creation + var directoriesToAddToAppDomain = TaskFactoryUtilities.ExtractUniqueDirectoriesFromAssemblyPaths(resolvedAssemblyReferences.ToList()); - return !hasInvalidReference; + handlerAddedToAppDomain = TaskFactoryUtilities.CreateAssemblyResolver(directoriesToAddToAppDomain); + AppDomain.CurrentDomain.AssemblyResolve += handlerAddedToAppDomain; - static Assembly TryLoadAssembly(List directories, AssemblyName name) + // 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) { - 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; + TaskFactoryUtilities.CreateLoadManifest(_assemblyPath, directoriesToAddToAppDomain); } + + return !hasInvalidReference; } - 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) { @@ -639,6 +627,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 @@ -655,18 +655,43 @@ 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) + /// true if the source code could be compiled and loaded, otherwise false. + private bool TryCompileAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) { - // First attempt to get a compiled assembly from the cache - if (CompiledAssemblyCache.TryGetValue(taskInfo, out assembly)) + // Try to get from cache + if (CompiledAssemblyCache.TryGetValue(taskInfo, out TaskFactoryUtilities.CachedAssemblyEntry cachedEntry)) { - return true; + // For out-of-process scenarios, validate the file still exists + if (!string.IsNullOrEmpty(cachedEntry.AssemblyPath) && !FileUtilities.FileExistsNoThrow(cachedEntry.AssemblyPath)) + { + // Cached assembly file was deleted, remove from cache and recompile + CompiledAssemblyCache.TryRemove(taskInfo, out _); + } + else + { + // Cache entry is valid, use it + assembly = cachedEntry.Assembly; + _assemblyPath = cachedEntry.AssemblyPath; + return true; + } + } + + assembly = null; + // Prepare for compilation + 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 } if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references)) @@ -674,11 +699,6 @@ private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTask 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 compilation, the source code and assembly are deleted. - string sourceCodePath = FileUtilities.GetTemporaryFileName(".tmp"); - string assemblyPath = FileUtilities.GetTemporaryFileName(".dll"); - // 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; @@ -735,7 +755,7 @@ private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTask 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.NoWarn = "1701;1702"; @@ -760,12 +780,11 @@ 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); + // Return the compiled assembly + assembly = TaskFactoryUtilities.LoadTaskAssembly(_assemblyPath); + string cachedAssemblyPath = Traits.Instance.ForceTaskFactoryOutOfProc ? _assemblyPath : string.Empty; + CompiledAssemblyCache.TryAdd(taskInfo, new TaskFactoryUtilities.CachedAssemblyEntry(assembly, cachedAssemblyPath)); return true; } catch (Exception e) @@ -775,14 +794,15 @@ private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTask } finally { - if (FileSystems.Default.FileExists(assemblyPath)) + if (deleteSourceCodeFile && sourceCodePath is not null && FileSystems.Default.FileExists(sourceCodePath)) { - File.Delete(assemblyPath); + File.Delete(sourceCodePath); } - if (deleteSourceCodeFile && FileSystems.Default.FileExists(sourceCodePath)) + if (!Traits.Instance.ForceTaskFactoryOutOfProc && !string.IsNullOrEmpty(_assemblyPath) && FileSystems.Default.FileExists(_assemblyPath)) { - File.Delete(sourceCodePath); + File.Delete(_assemblyPath); + _assemblyPath = null; } } } diff --git a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactoryTaskInfo.cs b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactoryTaskInfo.cs index 72993020895..66e9dbce7f6 100644 --- a/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactoryTaskInfo.cs +++ b/src/Tasks/RoslynCodeTaskFactory/RoslynCodeTaskFactoryTaskInfo.cs @@ -53,7 +53,9 @@ public bool Equals(RoslynCodeTaskFactoryTaskInfo other) return true; } - return String.Equals(SourceCode, other.SourceCode, StringComparison.OrdinalIgnoreCase) && References.SetEquals(other.References); + return String.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) + && String.Equals(SourceCode, other.SourceCode, StringComparison.OrdinalIgnoreCase) + && References.SetEquals(other.References); } public override bool Equals(object obj) @@ -68,8 +70,8 @@ public override bool Equals(object obj) public override int GetHashCode() { - // This is good enough to avoid most collisions, no need to hash References - return SourceCode.GetHashCode(); + // Include both Name and SourceCode to avoid cache collisions between different task names + return HashCode.Combine(Name?.GetHashCode() ?? 0, SourceCode?.GetHashCode() ?? 0); } } } diff --git a/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs b/src/Tasks/XamlTaskFactory/XamlTaskFactory.cs index 18e0ce17d1d..f065cd18b3b 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. @@ -35,11 +35,20 @@ public class XamlTaskFactory : ITaskFactory /// The compiled task assembly. /// private Assembly _taskAssembly; + + + private string _assemblyPath; /// /// The task type. /// private Type _taskType; + + /// + /// Location of the assembly for out of proc taskhosts. + /// + public string GetAssemblyPath() => _assemblyPath; + /// /// The name of the task pulled from the XAML. @@ -115,6 +124,12 @@ 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 + if (Traits.Instance.ForceTaskFactoryOutOfProc) + { + _assemblyPath = TaskFactoryUtilities.GetTemporaryTaskAssemblyPath(); + } + // create the code generator options // Since we are running msbuild 12.0 these had better load. var compilerParameters = new CompilerParameters( @@ -125,7 +140,8 @@ public bool Initialize(string taskName, IDictionary ta Path.Combine(pathToMSBuildBinaries, "Microsoft.Build.Tasks.Core.dll") ]) { - GenerateInMemory = true, + GenerateInMemory = !Traits.Instance.ForceTaskFactoryOutOfProc, + OutputAssembly = _assemblyPath, TreatWarningsAsErrors = false };