StuDev.AotAnywhere
1.0.5
dotnet add package StuDev.AotAnywhere --version 1.0.5
NuGet\Install-Package StuDev.AotAnywhere -Version 1.0.5
<PackageReference Include="StuDev.AotAnywhere" Version="1.0.5" />
<PackageVersion Include="StuDev.AotAnywhere" Version="1.0.5" />
<PackageReference Include="StuDev.AotAnywhere" />
paket add StuDev.AotAnywhere --version 1.0.5
#r "nuget: StuDev.AotAnywhere, 1.0.5"
#:package StuDev.AotAnywhere@1.0.5
#addin nuget:?package=StuDev.AotAnywhere&version=1.0.5
#tool nuget:?package=StuDev.AotAnywhere&version=1.0.5
AotAnywhere
Cross-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.
<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="docs/assets/platform-matrix-dark.svg"> <img alt="Diagram: builds on Windows (x64, x86), macOS (arm64, x64) and Linux (x64, arm64) hosts, through AotAnywhere (dotnet publish -r <rid>), and runs on Linux glibc and musl (x64, arm64, arm), macOS (osx-arm64, osx-x64) and Windows (win-x64, win-arm64). Every combination is CI-tested: 5 hosts × 10 target RIDs." src="docs/assets/platform-matrix-light.svg" width="880"> </picture> </p>
Quick start
In a project that already uses Native AOT, add an
Sdkreference toStuDev.AotAnywhereinside the<Project>element:<Project Sdk="Microsoft.NET.Sdk"> <Sdk Name="StuDev.AotAnywhere" Version="1.0.5" />(Or omit the
Versionand pin it once inglobal.jsonundermsbuild-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.
Supported platforms
Host machines (where you run dotnet publish):
- Windows (x86, x64)
- macOS (x64, arm64)
- Linux (x64, arm64)
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. |
Things to be aware of
ARMv7 needs .NET 9+. The
linux-armandlinux-musl-armtargets require anet9.0or 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.Windows on ARM64 is not supported as a host. zig 0.16's aarch64-windows code generation is broken, so Zig can't reliably run there. It will be revisited when a newer Zig fixes it. (win-arm64 as a target is fully supported.)
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.
Runtime dependencies on target Linux systems
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.
Packing .NET tools for every platform (.NET 11+)
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:
- Requires a .NET 11 SDK with the extensibility point; on older SDKs the hook is inert and the SDK's own host-capability rules apply.
- Every requested RID is attempted, unfiltered — a RID this package can't build fails its inner publish with a clear error rather than being silently dropped from the set of produced packages.
- Set
AotAnywhereMultiRidToolPackaging=falseto opt out and restore the SDK's default host-capability selection.
Compressing the output with UPX
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 onPATH.AotAnywhereUpxLzma=true— LZMA compression: smaller output, slower startup.AotAnywhereUpxArgs— extra arguments appended to the UPX command line (the default is--best).
Things to know:
- Works for Linux and Windows x64/x86 targets. UPX cannot pack macOS binaries (its Mach-O support was removed; macOS 13+ refuses to run packed binaries) or win-arm64 (no ARM64 PE support) — publishing those with the option set fails with an error rather than producing broken output.
- Windows antivirus false positives. UPX-packed executables are a common malware wrapper, so some scanners flag them. Weigh that before shipping compressed Windows binaries.
- Executables only — native libraries (
NativeLib) cannot be packed.
Documentation
- macOS targets — Apple linker stubs, and signing & notarizing for distribution
- Windows targets — how win-x64/win-arm64 cross-linking works
- Advanced configuration — how linking works (MSBuild takeovers + managed tasks), and using your own Zig
- Cross-platform validation — the CI matrix that tests every host → target combination
Credits
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.
License
MIT — see LICENSE.TXT.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.5 | 84 | 8/19/2026 |
| 1.0.4 | 595 | 7/11/2026 |
| 1.0.3 | 110 | 7/10/2026 |
| 1.0.2 | 116 | 7/10/2026 |
| 1.0.1 | 111 | 7/9/2026 |
| 1.0.0 | 176 | 7/6/2026 |
| 1.0.0-preview.2 | 121 | 7/6/2026 |
| 1.0.0-preview.1 | 109 | 7/6/2026 |