Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions docs/core/deploying/macos.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: Publish .NET apps for macOS
description: Learn how to publish .NET applications for macOS, including signing, notarization, and app entitlements.
description: Learn how to publish .NET applications for macOS, including signing, notarization, app entitlements, and universal binaries.
author: agocke
ms.author: angocke
ms.date: 10/22/2025
ms.date: 07/31/2026
ms.topic: how-to
ai-usage: ai-assisted
---
Expand Down Expand Up @@ -45,3 +45,53 @@ The following entitlements enable additional debugging and diagnostic capabiliti

> [!WARNING]
> Failing to sign and notarize your app might result in the application crashing while executing a restricted operation.

## Universal binaries

macOS supports *universal binaries*: a single Mach-O file that contains machine code for more than one CPU architecture. macOS selects the slice that matches the current processor, so one file runs natively on both Apple silicon (arm64) and Intel (x64) machines.

.NET doesn't produce universal binaries directly. Instead, you publish your app once per architecture and merge the two native executables with the Apple `lipo` tool. Two .NET publishing modes produce a self-contained native executable that you can merge this way:

- [Single-file apps](single-file/overview.md) (`PublishSingleFile`)
- [Native AOT deployment](native-aot/index.md) (`PublishAot`)

### Publish a single-file universal binary

Publish the app once per architecture into separate directories:

```bash
dotnet publish -c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true -o out/osx-arm64

dotnet publish -c Release -r osx-x64 --self-contained true \
-p:PublishSingleFile=true -o out/osx-x64
```

Then merge the two executables and re-sign the result:

```bash
mkdir -p out/universal
lipo -create -output out/universal/MyApp out/osx-arm64/MyApp out/osx-x64/MyApp
codesign --force --sign - out/universal/MyApp
```

A self-contained single-file publish embeds the runtime, the managed assemblies, and the native libraries in the executable itself, so the merged file is the complete app. The universal binary is roughly the sum of the two inputs.

Copy any remaining files from one of the per-architecture output directories (for example, `appsettings.json` or files excluded from the bundle) next to the merged executable.

### Publish a Native AOT universal binary

The steps are the same as for single-file, but replace the single-file properties with `PublishAot`:

```bash
dotnet publish -c Release -r osx-arm64 -p:PublishAot=true -o out/osx-arm64
dotnet publish -c Release -r osx-x64 -p:PublishAot=true -o out/osx-x64

mkdir -p out/universal
lipo -create -output out/universal/MyApp out/osx-arm64/MyApp out/osx-x64/MyApp
codesign --force --sign - out/universal/MyApp
```

Native AOT produces much smaller output than single-file publishing, so the universal binary stays small as well.

Native AOT also emits a `MyApp.dSYM` debug symbol bundle per architecture. To keep symbols for both slices, keep the per-architecture directories, or merge the `DWARF` binaries inside the bundles with `lipo`.
Loading