From 702ad7b9bd98c70a627a4f9607deeeea553c09bc Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:55:28 +0100 Subject: [PATCH 1/5] Update MSBuild app host documentation Clarify COM interop support and manifest requirements for MSBuild app host. --- documentation/specs/msbuild-apphost.md | 150 +++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 documentation/specs/msbuild-apphost.md diff --git a/documentation/specs/msbuild-apphost.md b/documentation/specs/msbuild-apphost.md new file mode 100644 index 00000000000..432d9cca253 --- /dev/null +++ b/documentation/specs/msbuild-apphost.md @@ -0,0 +1,150 @@ +# MSBuild App Host Support + +## Purpose + +Enable MSBuild to be invoked directly as a native executable (`MSBuild.exe` / `MSBuild`) instead of through `dotnet MSBuild.dll`, providing: + +- Better process identification (processes show as "MSBuild" not "dotnet") +- Win32 manifest embedding support (**COM interop**) +- Consistency with Roslyn compilers (`csc`, `vbc`) which already use app hosts +- Simplified invocation model + +### Critical: COM Manifest for Out-of-Proc Host Objects + +A key driver for this work is enabling **registration-free COM** for out-of-proc task host objects. Currently, when running via `dotnet.exe`, we cannot embed the required manifest declarations - and even if we could, it would be the wrong level of abstraction for `dotnet.exe` to contain MSBuild-specific COM interface definitions. + +**Background**: Remote host objects (e.g., for accessing unsaved file changes from VS) must be registered in the [Running Object Table (ROT)](https://docs.microsoft.com/en-us/windows/desktop/api/objidl/nn-objidl-irunningobjecttable). The `ITaskHost` interface requires registration-free COM configuration in the MSBuild executable manifest. + +**Required manifest additions for `MSBuild.exe.manifest`:** + +```xml + + + + + + + +``` + +**Related interfaces:** +- `ITaskHost` - **must be configured via MSBuild's manifest** (registration-free) +This is part of the work for [allowing out-of-proc tasks to access unsaved changes](https://github.com/dotnet/project-system/issues/4406). + +## Background + +An **app host** is a small native executable that: +1. Finds the .NET runtime +2. Loads the CLR +3. Calls the managed entry point (e.g., `MSBuild.dll`) + +It is functionally equivalent to `dotnet.exe MSBuild.dll`, but as a standalone executable. + +**Note**: The app host does NOT include .NET CLI functionality. (e.g. MSBuild.exe nuget add` wouldn't work — those are CLI features, not app host features). + +### Reference Implementation + +Roslyn added app host support in [PR #80026](https://github.com/dotnet/roslyn/pull/80026). + +## Changes Required + +### 1. MSBuild Repository + +**Remove `UseAppHost=false` from `src/MSBuild/MSBuild.csproj`:** + +```xml + +false +``` + +The SDK will then produce both `MSBuild.dll` and `MSBuild.exe` (Windows) / `MSBuild` (Unix). + +### 2. Packaging 2. Installer Repository (dotnet/dotnet VMR) +The app host creation happens in the installer/layout targets, similar to how Roslyn app hosts are created (PR https://github.com/dotnet/dotnet/pull/3180). + +### 3. Node Launching Logic + +Update node provider to launch `MSBuild.exe` instead of `dotnet MSBuild.dll`: +The path resolution logic remains the same, since MSBuild.exe will be shipped in every sdk version. + +### 4. Backward Compatibility (Critical) + +Because VS supports older SDKs, node launching must handle both scenarios: + +```csharp +var appHostPath = Path.Combine(sdkPath, $"MSBuild{RuntimeHostInfo.ExeExtension}"); + +if (File.Exists(appHostPath)) +{ + // New: Use app host directly + return (appHostPath, arguments); +} +else +{ + // Fallback: Use dotnet (older SDKs) + return (dotnetPath, $"\"{msbuildDllPath}\" {arguments}"); +} +``` + +**Handshake consideration**: The packet version can be bumped to negotiate between old/new node launching during handshake. +MSBuild knows how to handle it starting from https://github.com/dotnet/msbuild/pull/12753 + +## Runtime Discovery (the problem is solved in Roslyn app host this way) + +### The Problem + +App hosts find the runtime by checking (in order): +1. `DOTNET_ROOT_X64` / `DOTNET_ROOT_X86` / `DOTNET_ROOT_ARM64` +2. `DOTNET_ROOT` +3. Well-known locations (`C:\Program Files\dotnet`, etc.) + +When running under the SDK, the runtime may be in a non-standard location. The SDK sets `DOTNET_HOST_PATH` to indicate which `dotnet` it's using. + +### Solution + +Before launching an app host process, set `DOTNET_ROOT`: + +```csharp +// Derive DOTNET_ROOT from DOTNET_HOST_PATH +var dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); +var dotnetRoot = Path.GetDirectoryName(dotnetHostPath); + +Environment.SetEnvironmentVariable("DOTNET_ROOT", dotnetRoot); +``` + +### Edge Cases + +| Issue | Solution | +|-------|----------| +| `DOTNET_HOST_PATH` not set | Search `PATH` for `dotnet` executable | +| Architecture-specific vars override `DOTNET_ROOT` | Unset `DOTNET_ROOT_X64`, `DOTNET_ROOT_X86`, `DOTNET_ROOT_ARM64` before launch | +| Multi-threaded env var access | Use locking + save/restore pattern | +| App host doesn't exist | Fall back to `dotnet MSBuild.dll` | + +## Expected Result + +### SDK Directory Structure + +``` +sdk// +├── MSBuild.dll # Managed assembly +├── MSBuild.exe # Windows app host (NEW) +├── MSBuild # Unix app host (NEW, no extension) +├── MSBuild.deps.json +├── MSBuild.runtimeconfig.json +└── ... +``` + +### Invocation + +| Before | After | +|--------|-------| +| `dotnet /sdk/MSBuild.dll proj.csproj` | `/sdk/MSBuild proj.csproj` | +| Process name: `dotnet` | Process name: `MSBuild` | From a4a8e0a78fd427c62a8e92b174b1854c545d0d7a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:25:47 +0100 Subject: [PATCH 2/5] Document AppHost considerations for MSBuild integration Added important considerations regarding MSBuild invocation modes and their behavior with AppHost. --- documentation/specs/msbuild-apphost.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/documentation/specs/msbuild-apphost.md b/documentation/specs/msbuild-apphost.md index 432d9cca253..95df5639a2c 100644 --- a/documentation/specs/msbuild-apphost.md +++ b/documentation/specs/msbuild-apphost.md @@ -9,6 +9,17 @@ Enable MSBuild to be invoked directly as a native executable (`MSBuild.exe` / `M - Consistency with Roslyn compilers (`csc`, `vbc`) which already use app hosts - Simplified invocation model +### Important consideration +The .NET SDK currently invokes MSBuild in two modes: + +| Mode | Current Behavior | After App Host | +|------|------------------|----------------| +| **In-proc** | SDK loads `MSBuild.dll` directly | No change | +| **Out-of-proc** | SDK launches `dotnet exec MSBuild.dll` | No change in v1 | + +The AppHost introduction does not break SDK integration since we are not modifying the in-proc flow. The SDK will continue to load `MSBuild.dll` directly for in-proc scenarios right away. +The transition to in-proc task-host will be handled later in coordination with SDK team. + ### Critical: COM Manifest for Out-of-Proc Host Objects A key driver for this work is enabling **registration-free COM** for out-of-proc task host objects. Currently, when running via `dotnet.exe`, we cannot embed the required manifest declarations - and even if we could, it would be the wrong level of abstraction for `dotnet.exe` to contain MSBuild-specific COM interface definitions. From 0d234368fa77f63a3e1ff0cab0966b7d7a233b7c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:23:01 +0100 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Rainer Sigwald Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- documentation/specs/msbuild-apphost.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/specs/msbuild-apphost.md b/documentation/specs/msbuild-apphost.md index 95df5639a2c..73e5ed3cec7 100644 --- a/documentation/specs/msbuild-apphost.md +++ b/documentation/specs/msbuild-apphost.md @@ -24,7 +24,7 @@ The transition to in-proc task-host will be handled later in coordination with S A key driver for this work is enabling **registration-free COM** for out-of-proc task host objects. Currently, when running via `dotnet.exe`, we cannot embed the required manifest declarations - and even if we could, it would be the wrong level of abstraction for `dotnet.exe` to contain MSBuild-specific COM interface definitions. -**Background**: Remote host objects (e.g., for accessing unsaved file changes from VS) must be registered in the [Running Object Table (ROT)](https://docs.microsoft.com/en-us/windows/desktop/api/objidl/nn-objidl-irunningobjecttable). The `ITaskHost` interface requires registration-free COM configuration in the MSBuild executable manifest. +**Background**: Remote host objects (e.g., for accessing unsaved file changes from VS) must be registered in the [Running Object Table (ROT)](https://docs.microsoft.com/windows/desktop/api/objidl/nn-objidl-irunningobjecttable). The `ITaskHost` interface requires registration-free COM configuration in the MSBuild executable manifest. **Required manifest additions for `MSBuild.exe.manifest`:** @@ -58,7 +58,7 @@ An **app host** is a small native executable that: It is functionally equivalent to `dotnet.exe MSBuild.dll`, but as a standalone executable. -**Note**: The app host does NOT include .NET CLI functionality. (e.g. MSBuild.exe nuget add` wouldn't work — those are CLI features, not app host features). +**Note**: The app host does NOT include .NET CLI functionality. (e.g. `MSBuild.exe nuget add` wouldn't work — those are CLI features, not app host features). ### Reference Implementation @@ -77,13 +77,13 @@ Roslyn added app host support in [PR #80026](https://github.com/dotnet/roslyn/pu The SDK will then produce both `MSBuild.dll` and `MSBuild.exe` (Windows) / `MSBuild` (Unix). -### 2. Packaging 2. Installer Repository (dotnet/dotnet VMR) +### 2. Installer Repository (dotnet/dotnet VMR) The app host creation happens in the installer/layout targets, similar to how Roslyn app hosts are created (PR https://github.com/dotnet/dotnet/pull/3180). ### 3. Node Launching Logic Update node provider to launch `MSBuild.exe` instead of `dotnet MSBuild.dll`: -The path resolution logic remains the same, since MSBuild.exe will be shipped in every sdk version. +The path resolution logic remains the same, since MSBuild.exe will be shipped in every SDK version. ### 4. Backward Compatibility (Critical) From d774ca41ebffd42984b9b6fe06c44e5103759d59 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 8 Dec 2025 12:47:37 +0100 Subject: [PATCH 4/5] fix review comments --- documentation/specs/msbuild-apphost.md | 58 ++++++++++++-------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/documentation/specs/msbuild-apphost.md b/documentation/specs/msbuild-apphost.md index 95df5639a2c..59ee0f0d911 100644 --- a/documentation/specs/msbuild-apphost.md +++ b/documentation/specs/msbuild-apphost.md @@ -15,10 +15,11 @@ The .NET SDK currently invokes MSBuild in two modes: | Mode | Current Behavior | After App Host | |------|------------------|----------------| | **In-proc** | SDK loads `MSBuild.dll` directly | No change | -| **Out-of-proc** | SDK launches `dotnet exec MSBuild.dll` | No change in v1 | +| **Out-of-proc** | SDK launches `dotnet exec MSBuild.dll` | SDK will launch `MSBuild.exe` | -The AppHost introduction does not break SDK integration since we are not modifying the in-proc flow. The SDK will continue to load `MSBuild.dll` directly for in-proc scenarios right away. -The transition to in-proc task-host will be handled later in coordination with SDK team. +The AppHost introduction does not break SDK integration since we are not modifying the in-proc flow. The SDK will continue to load `MSBuild.dll` directly for in-proc scenarios. + +**SDK out-of-proc consideration**: The SDK can be configured to run MSBuild out-of-proc today via `DOTNET_CLI_RUN_MSBUILD_OUTOFPROC`, and this pattern will likely become more common as AOT work progresses for CLI commands that wrap MSBuild invocations. When the SDK does launch MSBuild out-of-proc, it will use the new app host (`MSBuild.exe`) when available. ### Critical: COM Manifest for Out-of-Proc Host Objects @@ -120,42 +121,37 @@ When running under the SDK, the runtime may be in a non-standard location. The S ### Solution -Before launching an app host process, set `DOTNET_ROOT`: - +Before launching an app host process, set `DOTNET_ROOT` in the `ProcessStartInfo.Environment`: ```csharp // Derive DOTNET_ROOT from DOTNET_HOST_PATH var dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); -var dotnetRoot = Path.GetDirectoryName(dotnetHostPath); - -Environment.SetEnvironmentVariable("DOTNET_ROOT", dotnetRoot); -``` -### Edge Cases +if (string.IsNullOrEmpty(dotnetHostPath)) +{ + // DOTNET_HOST_PATH should always be set when running under the SDK. + // If not set, fail fast rather than guessing - this indicates an unexpected environment. + throw new InvalidOperationException("DOTNET_HOST_PATH is not set. Cannot determine runtime location."); +} -| Issue | Solution | -|-------|----------| -| `DOTNET_HOST_PATH` not set | Search `PATH` for `dotnet` executable | -| Architecture-specific vars override `DOTNET_ROOT` | Unset `DOTNET_ROOT_X64`, `DOTNET_ROOT_X86`, `DOTNET_ROOT_ARM64` before launch | -| Multi-threaded env var access | Use locking + save/restore pattern | -| App host doesn't exist | Fall back to `dotnet MSBuild.dll` | +var dotnetRoot = Path.GetDirectoryName(dotnetHostPath); -## Expected Result +var startInfo = new ProcessStartInfo(appHostPath, arguments); -### SDK Directory Structure +// Set DOTNET_ROOT for the app host to find the runtime +startInfo.Environment["DOTNET_ROOT"] = dotnetRoot; -``` -sdk// -├── MSBuild.dll # Managed assembly -├── MSBuild.exe # Windows app host (NEW) -├── MSBuild # Unix app host (NEW, no extension) -├── MSBuild.deps.json -├── MSBuild.runtimeconfig.json -└── ... +// Clear architecture-specific overrides that would take precedence over DOTNET_ROOT +startInfo.Environment.Remove("DOTNET_ROOT_X64"); +startInfo.Environment.Remove("DOTNET_ROOT_X86"); +startInfo.Environment.Remove("DOTNET_ROOT_ARM64"); ``` -### Invocation +**Note**: Using `ProcessStartInfo.Environment` is thread-safe and scoped to the child process only, avoiding any need for locking or save/restore patterns on the parent process environment. -| Before | After | -|--------|-------| -| `dotnet /sdk/MSBuild.dll proj.csproj` | `/sdk/MSBuild proj.csproj` | -| Process name: `dotnet` | Process name: `MSBuild` | +### Edge Cases + +| Issue | Solution | +|-------|----------| +| `DOTNET_HOST_PATH` not set | Fail with clear error. This should always be set by the SDK; if missing, it indicates an unexpected/unsupported environment. | +| Architecture-specific vars override `DOTNET_ROOT` | Clear `DOTNET_ROOT_X64`, `DOTNET_ROOT_X86`, `DOTNET_ROOT_ARM64` in `ProcessStartInfo.Environment` (see code above) | +| App host doesn't exist | Fall back to `dotnet MSBuild.dll` and **log a message** indicating fallback (e.g., for debugging older SDK scenarios) | \ No newline at end of file From 4981dfb0e9f39c4266fe15007412904d6fb7b11a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 5 Jan 2026 13:16:41 +0100 Subject: [PATCH 5/5] add clarification for DOTNET_ROOT --- documentation/specs/msbuild-apphost.md | 41 +++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/documentation/specs/msbuild-apphost.md b/documentation/specs/msbuild-apphost.md index ce86e774787..3dcea07ce2f 100644 --- a/documentation/specs/msbuild-apphost.md +++ b/documentation/specs/msbuild-apphost.md @@ -84,7 +84,7 @@ The app host creation happens in the installer/layout targets, similar to how Ro ### 3. Node Launching Logic Update node provider to launch `MSBuild.exe` instead of `dotnet MSBuild.dll`: -The path resolution logic remains the same, since MSBuild.exe will be shipped in every SDK version. +The path resolution logic remains the same, since MSBuild.exe will be shipped in every SDK version. ### 4. Backward Compatibility (Critical) @@ -148,6 +148,45 @@ startInfo.Environment.Remove("DOTNET_ROOT_ARM64"); **Note**: Using `ProcessStartInfo.Environment` is thread-safe and scoped to the child process only, avoiding any need for locking or save/restore patterns on the parent process environment. +### DOTNET_ROOT Propagation to Child Processes + +**Concern**: When MSBuild sets `DOTNET_ROOT` to launch a worker node, that environment variable propagates to any tools the worker node executes. This could change tool behavior if the tool relies on `DOTNET_ROOT` to find its runtime. + +**Solution**: The worker node should explicitly clear `DOTNET_ROOT` (and architecture-specific variants) after startup, restoring the original entry-point environment: + +```csharp +// In OutOfProcNode.HandleNodeConfiguration, after setting BuildProcessEnvironment: + +// Clear DOTNET_ROOT variants that were set only for app host bootstrap. +// These should not leak to tools executed by this worker node. +// Only clear if NOT present in the original build process environment. +string[] dotnetRootVars = ["DOTNET_ROOT", "DOTNET_ROOT_X64", "DOTNET_ROOT_X86", "DOTNET_ROOT_ARM64"]; +foreach (string varName in dotnetRootVars) +{ + if (!_buildParameters.BuildProcessEnvironment.ContainsKey(varName)) + { + Environment.SetEnvironmentVariable(varName, null); + } +} +``` + +**Why this works**: + +1. `BuildProcessEnvironment` captures the environment from the **entry-point process** (e.g. VS). +2. If the entry-point had `DOTNET_ROOT` set, the worker should also have it (passed via `BuildProcessEnvironment`). +3. If the entry-point did NOT have `DOTNET_ROOT`, it was only added for app host bootstrap and should be cleared. + +**Alternative considered**: We could modify `NodeLauncher` to not inherit the parent environment and explicitly pass only `BuildProcessEnvironment` + `DOTNET_ROOT`. However, this is a larger change and may break other scenarios where environment inheritance is expected. + +**Implementation note**: Add a comment in the node-launching code explaining why `DOTNET_ROOT` is set and that the worker will clear it: + +```csharp +// Set DOTNET_ROOT for app host bootstrap only. +// The worker node will clear this after startup if it wasn't in the original BuildProcessEnvironment. +// See OutOfProcNode.HandleNodeConfiguration. +startInfo.Environment["DOTNET_ROOT"] = dotnetRoot; +``` + ### Edge Cases | Issue | Solution |