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 |