StuDevLabs/AotAnywhere

NuGet package to help you cross-compile Native AOT to different OSes/architectures

C#

98

178 commits

updated Oct 15, 2026

See the code

README

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.

Diagram: builds on Windows (x64, x86, arm64), 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: 6 hosts × 10 target RIDs.

Quick start

  1. 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.)

  2. 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 Sdk reference and not a PackageReference? 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 the Sdk form fetches everything the first time with no further setup. A plain PackageReference still works if you restore Zig yourself — see Advanced configuration.

Supported platforms

Host machines (where you run dotnet publish):

  • Windows (x86, x64, arm64)
  • macOS (x64, arm64)
  • Linux (x64, arm64)

Targets (what you can publish for), from any supported host:

TargetRIDsNotes
Linuxlinux-x64, linux-arm64, linux-arm, linux-musl-x64, linux-musl-arm64, linux-musl-armglibc and musl. The arm/ARMv7 RIDs require a net9.0+ target framework.
macOSosx-x64, osx-arm64Links against bundled Apple linker stubs. See macOS targets.
Windowswin-x64, win-arm64From 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-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.

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=false to 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 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:

  • 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

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.

StuDevLabs/AotAnywhere

NuGet package to help you cross-compile Native AOT to different OSes/architectures

C#

98

178 commits

updated Oct 15, 2026

See the code

README

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.

Diagram: builds on Windows (x64, x86, arm64), 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: 6 hosts × 10 target RIDs.

Quick start

  1. 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.)

  2. 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 Sdk reference and not a PackageReference? 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 the Sdk form fetches everything the first time with no further setup. A plain PackageReference still works if you restore Zig yourself — see Advanced configuration.

Supported platforms

Host machines (where you run dotnet publish):

  • Windows (x86, x64, arm64)
  • macOS (x64, arm64)
  • Linux (x64, arm64)

Targets (what you can publish for), from any supported host:

TargetRIDsNotes
Linuxlinux-x64, linux-arm64, linux-arm, linux-musl-x64, linux-musl-arm64, linux-musl-armglibc and musl. The arm/ARMv7 RIDs require a net9.0+ target framework.
macOSosx-x64, osx-arm64Links against bundled Apple linker stubs. See macOS targets.
Windowswin-x64, win-arm64From 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-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.

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=false to 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 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:

  • 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

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.

Significant stargazers

Drew Noakes

606 followers · starred Jul 2026

Filip Navara

221 followers · starred Jul 2026

Weihan Li

607 followers · starred Jul 2026

Alexandre Mutel

1,717 followers · starred Jul 2026