Skip to content

Commit ae4609e

Browse files
YuliiaKovalovarainersigwaldCopilot
authored
Add MSBuild app host design (#12857)
The PR contains a spec to describe App host design for MSBuild in sdk. Part of #12995. --------- Co-authored-by: Rainer Sigwald <raines@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a7172ba commit ae4609e

File tree

1 file changed

+204
-0
lines changed

1 file changed

+204
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# MSBuild App Host Support
2+
3+
## Purpose
4+
5+
Enable MSBuild to be invoked directly as a native executable (`MSBuild.exe` / `MSBuild`) instead of through `dotnet MSBuild.dll`, providing:
6+
7+
- Better process identification (processes show as "MSBuild" not "dotnet")
8+
- Win32 manifest embedding support (**COM interop**)
9+
- Consistency with Roslyn compilers (`csc`, `vbc`) which already use app hosts
10+
- Simplified invocation model
11+
12+
### Important consideration
13+
The .NET SDK currently invokes MSBuild in multiple ways:
14+
15+
| Mode | Current Behavior | After App Host |
16+
|------|------------------|----------------|
17+
| **In-proc** | SDK loads `MSBuild.dll` directly | No change |
18+
| **Out-of-proc (exec)** | SDK launches `dotnet exec MSBuild.dll` | SDK *can* launch `MSBuild.exe` |
19+
| **Out-of-proc (direct)** | SDK launches `dotnet MSBuild.dll` (no `exec`) | SDK *can* launch `MSBuild.exe` |
20+
21+
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.
22+
23+
**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 *can* opt to use the new app host (`MSBuild.exe`) when available, but this is **not required**—the existing `dotnet MSBuild.dll` invocation pattern continues to work. Switching to the app host in the SDK is a simplification/cleanup opportunity enabled by this work, not a forced change.
24+
25+
### Critical: COM Manifest for Out-of-Proc Host Objects
26+
27+
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.
28+
29+
**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.
30+
31+
**Required manifest additions for `MSBuild.exe.manifest`:**
32+
33+
```xml
34+
<!-- Location of the tlb, must be in same directory as MSBuild.exe -->
35+
<file name="Microsoft.Build.Framework.tlb">
36+
<typelib
37+
tlbid="{D8A9BA71-4724-481D-9CA7-0DA23A1D615C}"
38+
version="15.1"
39+
helpdir=""/>
40+
</file>
41+
42+
<!-- Registration-free COM for ITaskHost -->
43+
<comInterfaceExternalProxyStub
44+
iid="{9049A481-D0E9-414f-8F92-D4F67A0359A6}"
45+
name="ITaskHost"
46+
tlbid="{D8A9BA71-4724-481D-9CA7-0DA23A1D615C}"
47+
proxyStubClsid32="{00020424-0000-0000-C000-000000000046}" />
48+
```
49+
50+
**Related interfaces:**
51+
- `ITaskHost` - **must be configured via MSBuild's manifest** (registration-free)
52+
This is part of the work for [allowing out-of-proc tasks to access unsaved changes](https://github.com/dotnet/project-system/issues/4406).
53+
54+
## Background
55+
56+
An **app host** is a small native executable that:
57+
1. Finds the .NET runtime
58+
2. Loads the CLR
59+
3. Calls the managed entry point (e.g., `MSBuild.dll`)
60+
61+
It is functionally equivalent to `dotnet.exe MSBuild.dll`, but as a standalone executable.
62+
63+
**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).
64+
65+
### Reference Implementation
66+
67+
Roslyn added app host support in [PR #80026](https://github.com/dotnet/roslyn/pull/80026).
68+
69+
## Changes Required
70+
71+
### 1. MSBuild Repository
72+
73+
**Remove `UseAppHost=false` from `src/MSBuild/MSBuild.csproj`:**
74+
75+
```xml
76+
<!-- REMOVE THIS LINE -->
77+
<UseAppHost>false</UseAppHost>
78+
```
79+
80+
The SDK will then produce both `MSBuild.dll` and `MSBuild.exe` (Windows) / `MSBuild` (Unix).
81+
82+
### 2. VMR Changes (dotnet/dotnet - SDK component)
83+
The app host creation happens in the SDK layout targets within the VMR. Changes are made in the `sdk` component of `dotnet/dotnet` to simplify integration and avoid coordinated arcade SDK changes. Similar to how Roslyn app hosts are created (PR https://github.com/dotnet/dotnet/pull/3180).
84+
85+
### 3. Node Launching Logic
86+
87+
Update node provider to launch `MSBuild.exe` instead of `dotnet MSBuild.dll`:
88+
The path resolution logic remains the same, since MSBuild.exe will be shipped in every SDK version.
89+
90+
### 4. Backward Compatibility (Critical)
91+
92+
Because VS supports older SDKs, node launching must handle both scenarios:
93+
94+
```csharp
95+
var appHostPath = Path.Combine(sdkPath, $"MSBuild{RuntimeHostInfo.ExeExtension}");
96+
97+
if (File.Exists(appHostPath))
98+
{
99+
// New: Use app host directly
100+
return (appHostPath, arguments);
101+
}
102+
else
103+
{
104+
// Fallback: Use dotnet (older SDKs)
105+
return (dotnetPath, $"\"{msbuildDllPath}\" {arguments}");
106+
}
107+
```
108+
109+
**Handshake consideration**: The packet version can be bumped to negotiate between old/new node launching during handshake.
110+
MSBuild knows how to handle it starting from https://github.com/dotnet/msbuild/pull/12753
111+
112+
## Runtime Discovery (the problem is solved in Roslyn app host this way)
113+
114+
### The Problem
115+
116+
App hosts find the runtime by checking (in order):
117+
1. `DOTNET_ROOT_X64` / `DOTNET_ROOT_X86` / `DOTNET_ROOT_ARM64`
118+
2. `DOTNET_ROOT`
119+
3. Well-known locations (`C:\Program Files\dotnet`, etc.)
120+
121+
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.
122+
123+
### Solution
124+
125+
Before launching an app host process, set `DOTNET_ROOT` in the `ProcessStartInfo.Environment`.
126+
127+
**Note**: This solution applies to MSBuild's internal node launching (worker nodes, task hosts). The SDK's entry-point invocation (`dotnet MSBuild.dll`, `dotnet exec MSBuild.dll`, or eventually `MSBuild.exe`) is a separate concern—if the SDK continues using `dotnet.exe MSBuild.dll` with `DOTNET_CLI_RUN_MSBUILD_OUTOFPROC`, that path still works and doesn't require `DOTNET_ROOT` handling (since `dotnet.exe` handles runtime discovery itself).
128+
```csharp
129+
// Derive DOTNET_ROOT from DOTNET_HOST_PATH
130+
var dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
131+
132+
if (string.IsNullOrEmpty(dotnetHostPath))
133+
{
134+
// DOTNET_HOST_PATH should always be set when running under the SDK.
135+
// If not set, fail fast rather than guessing - this indicates an unexpected environment.
136+
throw new InvalidOperationException("DOTNET_HOST_PATH is not set. Cannot determine runtime location.");
137+
}
138+
139+
var dotnetRoot = Path.GetDirectoryName(dotnetHostPath);
140+
141+
var startInfo = new ProcessStartInfo(appHostPath, arguments);
142+
143+
// Set DOTNET_ROOT for the app host to find the runtime
144+
startInfo.Environment["DOTNET_ROOT"] = dotnetRoot;
145+
146+
// Clear architecture-specific overrides that would take precedence over DOTNET_ROOT
147+
startInfo.Environment.Remove("DOTNET_ROOT_X64");
148+
startInfo.Environment.Remove("DOTNET_ROOT_X86");
149+
startInfo.Environment.Remove("DOTNET_ROOT_ARM64");
150+
```
151+
152+
**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.
153+
154+
### DOTNET_ROOT Propagation to Child Processes
155+
156+
**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.
157+
158+
**Solution**: The worker node (and out-of-proc task host nodes) should explicitly clear `DOTNET_ROOT` (and architecture-specific variants) after startup, restoring the original entry-point environment.
159+
160+
**Applies to**:
161+
- Worker nodes (`OutOfProcNode`)
162+
- Out-of-proc task host nodes (`OutOfProcTaskHostNode`)
163+
164+
```csharp
165+
// In OutOfProcNode.HandleNodeConfiguration (and similar location in OutOfProcTaskHostNode),
166+
// after setting BuildProcessEnvironment:
167+
168+
// Clear DOTNET_ROOT variants that were set only for app host bootstrap.
169+
// These should not leak to tools executed by this worker node.
170+
// Only clear if NOT present in the original build process environment.
171+
string[] dotnetRootVars = ["DOTNET_ROOT", "DOTNET_ROOT_X64", "DOTNET_ROOT_X86", "DOTNET_ROOT_ARM64"];
172+
foreach (string varName in dotnetRootVars)
173+
{
174+
if (!_buildParameters.BuildProcessEnvironment.ContainsKey(varName))
175+
{
176+
Environment.SetEnvironmentVariable(varName, null);
177+
}
178+
}
179+
```
180+
181+
**Why this works**:
182+
183+
1. `BuildProcessEnvironment` captures the environment from the **entry-point process** (e.g. VS).
184+
2. If the entry-point had `DOTNET_ROOT` set, the worker should also have it (passed via `BuildProcessEnvironment`).
185+
3. If the entry-point did NOT have `DOTNET_ROOT`, it was only added for app host bootstrap and should be cleared.
186+
187+
**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.
188+
189+
**Implementation note**: Add a comment in the node-launching code explaining why `DOTNET_ROOT` is set and that the worker will clear it:
190+
191+
```csharp
192+
// Set DOTNET_ROOT for app host bootstrap only.
193+
// The worker node will clear this after startup if it wasn't in the original BuildProcessEnvironment.
194+
// See OutOfProcNode.HandleNodeConfiguration.
195+
startInfo.Environment["DOTNET_ROOT"] = dotnetRoot;
196+
```
197+
198+
### Edge Cases
199+
200+
| Issue | Solution |
201+
|-------|----------|
202+
| `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. |
203+
| Architecture-specific vars override `DOTNET_ROOT` | Clear `DOTNET_ROOT_X64`, `DOTNET_ROOT_X86`, `DOTNET_ROOT_ARM64` in `ProcessStartInfo.Environment` (see code above) |
204+
| App host doesn't exist | Fall back to `dotnet MSBuild.dll` and **log a message** indicating fallback (e.g., for debugging older SDK scenarios) |

0 commit comments

Comments
 (0)