NuGet package to help you cross-compile Native AOT to different OSes/architectures
See the codeCross-compile Native AOT .NET apps to Linux, macOS and Windows — from any of them.
PublishAot normally refuses to build for another OS:
$ dotnet publish -r linux-x64
Microsoft.NETCore.Native.Publish.targets(59,5): error : Cross-OS native compilation is not supported.
AotAnywhere is a NuGet package that lifts that restriction. Add it to your
project and dotnet publish -r <rid> just works for Linux, macOS and Windows
RIDs, from a Windows, macOS or Linux machine. It uses Zig
as the linker and sysroot, and brings everything it needs with it — no extra
SDKs, cross toolchains or system packages to install on the build machine.
In a project that already uses Native AOT, add an Sdk reference to
StuDev.AotAnywhere
inside the <Project> element:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="StuDev.AotAnywhere" Version="1.1.0" />
(Or omit the Version and pin it once in global.json under
msbuild-sdks.)
Publish for one of the newly available RIDs:
dotnet publish -r linux-x64 # or linux-arm64, linux-arm*
dotnet publish -r linux-musl-x64 # or linux-musl-arm64, linux-musl-arm*
dotnet publish -r osx-x64 # or osx-arm64
dotnet publish -r win-x64 # or win-arm64
That's it — no other tools required, including symbol stripping (the package handles that itself, no LLVM install needed).
Why an
Sdkreference and not aPackageReference? The package pulls in its Zig linker toolchain through a second, host-specific NuGet package, and NuGet cannot restore package references declared inside a package's build targets (NuGet/Home#4790). SDK props are evaluated during restore, so theSdkform fetches everything the first time with no further setup. A plainPackageReferencestill works if you restore Zig yourself — see Advanced configuration.
Host machines (where you run dotnet publish):
Targets (what you can publish for), from any supported host:
| Target | RIDs | Notes |
|---|---|---|
| Linux | linux-x64, linux-arm64, linux-arm, linux-musl-x64, linux-musl-arm64, linux-musl-arm | glibc and musl. The arm/ARMv7 RIDs require a net9.0+ target framework. |
| macOS | osx-x64, osx-arm64 | Links against bundled Apple linker stubs. See macOS targets. |
| Windows | win-x64, win-arm64 | From Linux/macOS hosts, via zig's MinGW-w64 import libraries. On a Windows host the SDK links these natively with MSVC. See Windows targets. |
ARMv7 needs .NET 9+. The linux-arm and linux-musl-arm targets require a
net9.0 or later target framework — .NET only ships ILCompiler runtime packs
for them from .NET 9 onwards.
macOS binaries need signing before you distribute them. Out of the box the output only runs locally (osx-arm64 gets an ad-hoc signature, osx-x64 is unsigned). To hand it to other people you must sign it with a Developer ID certificate and notarize it — both doable from any host, no Mac required. See Signing and notarizing.
Windows output skips some MSVC hardening. Control Flow Guard and CET
markers (/CETCOMPAT) are not carried over. For maximum-hardening release
builds, link on Windows with MSVC. Details in Windows targets.
Linux ICU dependency. A cross-compiled binary may need the ICU library on the target machine, the same as any globalization-enabled .NET app. See Runtime dependencies on Linux below.
When running the cross-compiled binaries on Linux, you may hit a missing ICU library:
Process terminated. Couldn't find a valid ICU package installed on the system. Please install libicu (or icu-libs) using your package manager and try again.
Solution 1 (recommended): install ICU on the target system:
# Ubuntu/Debian
sudo apt-get install libicu-dev
# CentOS/RHEL/Fedora
sudo yum install libicu-devel # or: sudo dnf install libicu-devel
# Alpine Linux
sudo apk add icu-libs
Solution 2: build with invariant globalization (no ICU dependency):
dotnet publish -r linux-x64 /p:InvariantGlobalization=true
Note that invariant globalization disables culture-specific formatting, sorting, and other globalization features.
Since .NET 10, a .NET tool
can ship native AOT binaries as one small "pointer" package plus one
RID-specific package per platform — dotnet tool install downloads just the
binary matching the user's machine. A single dotnet pack builds all of them,
but the base SDK only packs the RIDs the host's native toolchain can build
(e.g. a linux-x64 machine packs only linux-x64).
With AotAnywhere referenced, that limit goes away: dotnet pack on one
machine produces packages for every RID listed in
ToolPackageRuntimeIdentifiers (or RuntimeIdentifiers), via the SDK's
tool-packaging extensibility point
(dotnet/sdk#55250).
<PropertyGroup>
<PackAsTool>true</PackAsTool>
<PublishAot>true</PublishAot>
<ToolPackageRuntimeIdentifiers>linux-x64;linux-arm64;linux-musl-x64;linux-musl-arm64;osx-x64;osx-arm64;win-x64;win-arm64</ToolPackageRuntimeIdentifiers>
</PropertyGroup>
dotnet pack # one host, one command, a package per RID
Things to know:
AotAnywhereMultiRidToolPackaging=false to opt out and restore the
SDK's default host-capability selection.The published binary can optionally be compressed with UPX (the same idea as PublishAotCompressed), typically halving its on-disk size. UPX produces a self-extracting executable that unpacks itself in memory at launch; the startup cost is usually not observable.
Install UPX on the build machine (from the
releases page, or
apt install upx-ucl / brew install upx / winget install upx), then:
dotnet publish -r linux-x64 /p:AotAnywhereUpxCompress=true
Knobs:
AotAnywhereUpxPath — path to the UPX executable if it is not on PATH.AotAnywhereUpxLzma=true — LZMA compression: smaller output, slower startup.AotAnywhereUpxArgs — extra arguments appended to the UPX command line
(the default is --best).Things to know:
NativeLib) cannot be packed.AotAnywhere began as a fork of Michal Strehovsky's
PublishAotCross, which
first demonstrated that Zig could stand in as the linker and sysroot to make
Native AOT cross-compilation work. It has since been substantially rewritten —
the native shim is gone, links run directly through zig cc and managed MSBuild
tasks, and it ships bundled Apple sysroot stubs, symbol stripping, and a full
host × target CI matrix — but the original insight and inspiration are Michal's.
Thank you.
MIT — see LICENSE.TXT.
606 followers · starred Jul 2026
221 followers · starred Jul 2026
607 followers · starred Jul 2026
1,717 followers · starred Jul 2026
NuGet package to help you cross-compile Native AOT to different OSes/architectures
See the codeCross-compile Native AOT .NET apps to Linux, macOS and Windows — from any of them.
PublishAot normally refuses to build for another OS:
$ dotnet publish -r linux-x64
Microsoft.NETCore.Native.Publish.targets(59,5): error : Cross-OS native compilation is not supported.
AotAnywhere is a NuGet package that lifts that restriction. Add it to your
project and dotnet publish -r <rid> just works for Linux, macOS and Windows
RIDs, from a Windows, macOS or Linux machine. It uses Zig
as the linker and sysroot, and brings everything it needs with it — no extra
SDKs, cross toolchains or system packages to install on the build machine.
In a project that already uses Native AOT, add an Sdk reference to
StuDev.AotAnywhere
inside the <Project> element:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="StuDev.AotAnywhere" Version="1.1.0" />
(Or omit the Version and pin it once in global.json under
msbuild-sdks.)
Publish for one of the newly available RIDs:
dotnet publish -r linux-x64 # or linux-arm64, linux-arm*
dotnet publish -r linux-musl-x64 # or linux-musl-arm64, linux-musl-arm*
dotnet publish -r osx-x64 # or osx-arm64
dotnet publish -r win-x64 # or win-arm64
That's it — no other tools required, including symbol stripping (the package handles that itself, no LLVM install needed).
Why an
Sdkreference and not aPackageReference? The package pulls in its Zig linker toolchain through a second, host-specific NuGet package, and NuGet cannot restore package references declared inside a package's build targets (NuGet/Home#4790). SDK props are evaluated during restore, so theSdkform fetches everything the first time with no further setup. A plainPackageReferencestill works if you restore Zig yourself — see Advanced configuration.
Host machines (where you run dotnet publish):
Targets (what you can publish for), from any supported host:
| Target | RIDs | Notes |
|---|---|---|
| Linux | linux-x64, linux-arm64, linux-arm, linux-musl-x64, linux-musl-arm64, linux-musl-arm | glibc and musl. The arm/ARMv7 RIDs require a net9.0+ target framework. |
| macOS | osx-x64, osx-arm64 | Links against bundled Apple linker stubs. See macOS targets. |
| Windows | win-x64, win-arm64 | From Linux/macOS hosts, via zig's MinGW-w64 import libraries. On a Windows host the SDK links these natively with MSVC. See Windows targets. |
ARMv7 needs .NET 9+. The linux-arm and linux-musl-arm targets require a
net9.0 or later target framework — .NET only ships ILCompiler runtime packs
for them from .NET 9 onwards.
macOS binaries need signing before you distribute them. Out of the box the output only runs locally (osx-arm64 gets an ad-hoc signature, osx-x64 is unsigned). To hand it to other people you must sign it with a Developer ID certificate and notarize it — both doable from any host, no Mac required. See Signing and notarizing.
Windows output skips some MSVC hardening. Control Flow Guard and CET
markers (/CETCOMPAT) are not carried over. For maximum-hardening release
builds, link on Windows with MSVC. Details in Windows targets.
Linux ICU dependency. A cross-compiled binary may need the ICU library on the target machine, the same as any globalization-enabled .NET app. See Runtime dependencies on Linux below.
When running the cross-compiled binaries on Linux, you may hit a missing ICU library:
Process terminated. Couldn't find a valid ICU package installed on the system. Please install libicu (or icu-libs) using your package manager and try again.
Solution 1 (recommended): install ICU on the target system:
# Ubuntu/Debian
sudo apt-get install libicu-dev
# CentOS/RHEL/Fedora
sudo yum install libicu-devel # or: sudo dnf install libicu-devel
# Alpine Linux
sudo apk add icu-libs
Solution 2: build with invariant globalization (no ICU dependency):
dotnet publish -r linux-x64 /p:InvariantGlobalization=true
Note that invariant globalization disables culture-specific formatting, sorting, and other globalization features.
Since .NET 10, a .NET tool
can ship native AOT binaries as one small "pointer" package plus one
RID-specific package per platform — dotnet tool install downloads just the
binary matching the user's machine. A single dotnet pack builds all of them,
but the base SDK only packs the RIDs the host's native toolchain can build
(e.g. a linux-x64 machine packs only linux-x64).
With AotAnywhere referenced, that limit goes away: dotnet pack on one
machine produces packages for every RID listed in
ToolPackageRuntimeIdentifiers (or RuntimeIdentifiers), via the SDK's
tool-packaging extensibility point
(dotnet/sdk#55250).
<PropertyGroup>
<PackAsTool>true</PackAsTool>
<PublishAot>true</PublishAot>
<ToolPackageRuntimeIdentifiers>linux-x64;linux-arm64;linux-musl-x64;linux-musl-arm64;osx-x64;osx-arm64;win-x64;win-arm64</ToolPackageRuntimeIdentifiers>
</PropertyGroup>
dotnet pack # one host, one command, a package per RID
Things to know:
AotAnywhereMultiRidToolPackaging=false to opt out and restore the
SDK's default host-capability selection.The published binary can optionally be compressed with UPX (the same idea as PublishAotCompressed), typically halving its on-disk size. UPX produces a self-extracting executable that unpacks itself in memory at launch; the startup cost is usually not observable.
Install UPX on the build machine (from the
releases page, or
apt install upx-ucl / brew install upx / winget install upx), then:
dotnet publish -r linux-x64 /p:AotAnywhereUpxCompress=true
Knobs:
AotAnywhereUpxPath — path to the UPX executable if it is not on PATH.AotAnywhereUpxLzma=true — LZMA compression: smaller output, slower startup.AotAnywhereUpxArgs — extra arguments appended to the UPX command line
(the default is --best).Things to know:
NativeLib) cannot be packed.AotAnywhere began as a fork of Michal Strehovsky's
PublishAotCross, which
first demonstrated that Zig could stand in as the linker and sysroot to make
Native AOT cross-compilation work. It has since been substantially rewritten —
the native shim is gone, links run directly through zig cc and managed MSBuild
tasks, and it ships bundled Apple sysroot stubs, symbol stripping, and a full
host × target CI matrix — but the original insight and inspiration are Michal's.
Thank you.
MIT — see LICENSE.TXT.
606 followers · starred Jul 2026
221 followers · starred Jul 2026
607 followers · starred Jul 2026
1,717 followers · starred Jul 2026