HostWriter.CreateAppHost output differs from dotnet publish on macOS (missing ad-hoc code signature)
- Dominant language
- C#
- Stars
- 3.2k
- Forks
- 1.3k
- PR merge metrics
- PR metrics pending
Description
### Is your feature request related to a problem?
### Is there an existing issue for this?
I searched but couldn't find one describing this specific scenario.
---
### Describe the bug
When using `Microsoft.NET.HostModel.HostWriter.CreateAppHost()` directly (outside the MSBuild `dotnet build/publish` pipeline) on macOS ARM64, the generated apphost is **not immediately runnable**.
The generated executable behaves differently from the one produced by `dotnet publish`.
The missing step is an ad-hoc code signature.
Running:
```bash
codesign --force --sign - MyExecutable
```
immediately fixes the problem.
This behavior is not obvious from the `HostWriter` API and is easy to miss when building custom compilers or language toolchains.
---
### Expected behavior
Either:
* `HostWriter.CreateAppHost()` should produce the same runnable apphost that `dotnet publish` produces on macOS,
or
* the API documentation should explicitly state that callers are responsible for performing an ad-hoc `codesign` after creating the apphost.
---
### Actual behavior
Without
```bash
codesign --force --sign - MyExecutable
```
the generated apphost does not behave like the SDK-generated one.
After signing, the executable works correctly.
---
### Environment
- macOS ARM64
- .NET 10 Preview
- HostWriter.CreateAppHost()
- Microsoft.NET.HostModel
---
### Investigation
Initially I suspected the problem was in `PersistedAssemblyBuilder`, the generated IL, or the PE metadata.
After several experiments I ruled those out.
The following experiment demonstrates that the managed assembly is valid:
1. Build a normal C# application with `dotnet publish`.
2. Replace **only** the generated managed DLL with the DLL produced by my compiler.
3. Leave the apphost, runtimeconfig.json and deps.json untouched.
The application runs correctly.
This proves the generated managed assembly is valid and that the difference lies in the apphost generation pipeline.
---
### Root cause
Comparing the executable produced by `dotnet publish` with the executable produced by `HostWriter.CreateAppHost()` showed that the SDK performs an additional ad-hoc code signing step.
Running
```bash
codesign --force --sign - MyExecutable
```
on the HostWriter-generated executable makes it behave exactly like the SDK-generated executable.
---
### Suggestion
If this behavior is intentional, it would be very helpful to document it.
Something similar to:
> When using `HostWriter.CreateAppHost()` directly on macOS, callers are responsible for applying an ad-hoc code signature (or another valid signature) to the generated executable. The .NET SDK performs this step automatically during build/publish.
This would save developers implementing custom language compilers or build systems many hours of debugging.
### Describe the solution you'd like
Ideally, `HostWriter.CreateAppHost()` should produce an apphost that behaves the same as the one generated by `dotnet publish` on macOS.
If this is not possible because code signing is intentionally handled by MSBuild, then the API documentation should explicitly state that callers are responsible for performing an ad-hoc code signature after calling `HostWriter.CreateAppHost()`.
For example, the documentation could include something like:
> On macOS, executables generated with `HostWriter.CreateAppHost()` must be signed (at least with an ad-hoc signature) before execution. The .NET SDK performs this step automatically during `dotnet build` and `dotnet publish`, but callers using `HostWriter` directly are responsible for signing the generated executable.
This would make the API behavior consistent with developer expectations and prevent a significant amount of debugging for anyone implementing custom compilers or build systems.
Since `HostWriter.CreateAppHost()` is a public API intended for tooling scenarios, it should either encapsulate all required platform-specific steps or clearly document which additional steps the caller must perform.
### Alternatives you've considered
I considered several alternatives before identifying the root cause:
1. Using `dotnet publish`, which produces a working executable, but it is significantly slower and not suitable for my compiler, which generates assemblies directly using `PersistedAssemblyBuilder`.
2. Executing the generated assembly with `dotnet exec`, which works correctly, but requires invoking the .NET host explicitly instead of producing a standalone executable.
3. Using `PersistedAssemblyBuilder.GenerateMetadata()` together with `ManagedPEBuilder`, but this currently isn't viable because metadata tokens are not finalized until `Save()`, making it impossible to reliably specify the entry point.
4. Investigating the generated PE and metadata, assuming the issue was caused by the emitted assembly. This was ruled out by replacing the managed DLL inside a working application produced by `dotnet publish`; the application executed correctly, proving that the managed assembly was valid.
Ultimately, the only missing step was applying an ad-hoc code signature to the executable produced by `HostWriter.CreateAppHost()`.
### Additional context
_No response_
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at Microsoft.NET.HostModel.HostWriter.CreateAppHost and compare its macOS output with the signing step used by the dotnet publish pipeline. Verify the behavior with a HostWriter-generated executable and codesign, then determine whether the completed work should make the output runnable or document the caller's signing responsibility.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, macos
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100