This is a NuGet package with MSBuild targets to enable cross-compilation with Native AOT from macOS to both Windows and Linux. It helps resolve the following error:
$ dotnet publish -r win-x64
Microsoft.NETCore.Native.Publish.targets(59,5): error : Cross-OS native compilation is not supported.This package provides two cross-compilation toolchains:
- Windows targets: Uses
lld-link+ xwin for Windows SDK - Linux targets: Uses Zig as a unified cross-compilation toolchain
Enabling cross-compilation to win-x64/win-arm64/win-x86 and linux-x64/linux-arm64/linux-musl-* from a macOS machine.
We provide automated setup scripts for easy installation:
# Install complete environment (Windows + Linux)
curl -fsSL https://raw.githubusercontent.com/interface95/PublishAotCross.macOS/main/setup-cross-all.sh | bash
# Or install Windows cross-compilation only
curl -fsSL https://raw.githubusercontent.com/interface95/PublishAotCross.macOS/main/setup-cross-windows.sh | bash
# Or install Linux cross-compilation only
curl -fsSL https://raw.githubusercontent.com/interface95/PublishAotCross.macOS/main/setup-cross-linux.sh | bash
⚠️ Security Note:
- DO NOT use
sudowith the curl command- The script will prompt for your password when needed (Homebrew installation)
- For better security, review the script first:
curl -fsSL https://raw.githubusercontent.com/interface95/PublishAotCross.macOS/main/setup-cross-all.sh -o setup.sh cat setup.sh # Review the content bash setup.sh
Or clone and run locally (safer):
git clone https://github.com/interface95/PublishAotCross.macOS.git
cd PublishAotCross.macOS
# Complete environment
./setup-cross-all.sh
# Windows only
./setup-cross-windows.sh
# Linux only
./setup-cross-linux.sh-
Install lld-link (via Homebrew):
brew install lld # Add to PATH (choose based on your Mac) # For Apple Silicon (M1/M2/M3): export PATH="/opt/homebrew/opt/lld/bin:$PATH" # For Intel Mac: # export PATH="/usr/local/opt/lld/bin:$PATH" # Or use this universal command: export PATH="$(brew --prefix lld)/bin:$PATH"
-
Install xwin:
cargo install --locked xwin
-
Download Windows SDK:
mkdir -p $HOME/.local/share/xwin-sdk xwin --accept-license \ --cache-dir $HOME/.local/share/xwin-sdk \ --arch x86_64,aarch64 \ splat --preserve-ms-arch-notation
💡 Note: .NET 9+ doesn't require additional C/C++ runtime libraries, making setup much simpler than .NET 8.
-
Add this package to your Native AOT project:
<PackageReference Include="PublishAotCross.macOS" Version="1.0.2-preview" />
-
Publish for Windows:
# ⚠️ 重要:确保 lld-link 在 PATH 中 export PATH="$(brew --prefix lld)/bin:$PATH" # 编译发布 dotnet publish -r win-x64 -c Release
💡 提示:建议将
export PATH="$(brew --prefix lld)/bin:$PATH"添加到~/.zshrc或~/.bash_profile中,这样就不需要每次都手动设置了。
-
Install Zig (via Homebrew):
brew install zig
-
Add this package to your Native AOT project (same as above):
<PackageReference Include="PublishAotCross.macOS" Version="1.0.2-preview" />
-
Publish for Linux:
Due to MSBuild property evaluation order, you need to specify the linker via command line:
# glibc-based (Ubuntu, Debian, etc.) dotnet publish -r linux-x64 -c Release /p:StripSymbols=false dotnet publish -r linux-arm64 -c Release /p:StripSymbols=false # musl-based (Alpine Linux) dotnet publish -r linux-musl-x64 -c Release /p:StripSymbols=false dotnet publish -r linux-musl-arm64 -c Release /p:StripSymbols=false
💡 Note: The
/p:StripSymbols=falseparameter is required becausellvm-objcopyis typically not installed. If you install LLVM and add it to PATH, you can omit this parameter.
By default, Linux binaries include debug symbols, resulting in larger file sizes. You can reduce binary size by ~80% by installing LLVM and enabling symbol stripping:
-
Install LLVM (includes
llvm-objcopy):brew install llvm
-
Create
objcopysymlink:mkdir -p ~/.local/bin ln -sf $(brew --prefix llvm)/bin/llvm-objcopy ~/.local/bin/objcopy
-
Add to PATH (add to
~/.zshrcfor persistence):export PATH="$HOME/.local/bin:$(brew --prefix llvm)/bin:$PATH"
-
Publish with symbol stripping enabled:
dotnet publish -r linux-x64 -c Release /p:StripSymbols=true
Size comparison:
| Target | Without Stripping | With Stripping | Reduction |
|---|---|---|---|
| linux-x64 | 7.4 MB | 1.5 MB | ~80% |
| linux-arm64 | 7.8 MB | 1.6 MB | ~81% |
| linux-musl-x64 | 8.8 MB | 1.8 MB | ~84% |
💡 Tip: Symbol stripping is recommended for production deployments to reduce binary size. For development/debugging, keep symbols intact by using
/p:StripSymbols=false.
📖 Detailed Linux cross-compilation guide: See QUICKSTART-LINUX.md
The package uses the following MSBuild properties:
XWinCache: Path to the Windows SDK downloaded by xwin
Default:$(HOME)/.local/share/xwin-sdk/
You can override it in your project file:
<PropertyGroup>
<XWinCache>/custom/path/to/xwin-sdk/</XWinCache>
</PropertyGroup>Note: Advanced users can also set
PublishAotCrossPathto use a locally cloned version of the repository instead of the NuGet-bundled targets. This is usually not needed.
win-x64win-arm64win-x86
linux-x64(glibc)linux-arm64(glibc)linux-musl-x64(Alpine Linux)linux-musl-arm64(Alpine Linux)
.NET Native AOT binaries require the ICU library on the target Linux system:
# Ubuntu/Debian
sudo apt-get install -y libicu-dev
# CentOS/RHEL/Fedora
sudo yum install -y icu
# Alpine Linux
apk add --no-cache icu-libsFROM ubuntu:22.04
RUN apt-get update && apt-get install -y libicu-dev
COPY YourApp /app/
CMD ["/app/YourApp"]If you don't need internationalization:
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>📖 More details: See QUICKSTART-LINUX.md for complete Linux deployment guide.
This package is a port of PublishAotCrossXWin (which targets Linux → Windows) adapted for macOS → Windows cross-compilation.
Key components:
- lld-link: A cross-platform linker from LLVM that can generate Windows PE executables on macOS
- xwin: Downloads the Windows SDK and CRT, creating a "sysroot" for cross-compilation
- MSBuild Targets: Hooks into the Native AOT build process to:
- Set
DisableUnsupportedError=trueto bypass .NET's cross-OS restriction - Replace the default linker with
lld-link - Inject Windows SDK paths using
/vctoolsdirand/winsdkdirflags
- Set
Technical flow:
.NET Compiler (macOS) → IL Code → Native AOT Compiler → Object Files (.obj)
↓
lld-link (LLVM Linker on macOS)
↓
Uses xwin-provided SDK/CRT
↓
Windows PE Executable (.exe)
See the test/ directory for a simple example.
- macOS (tested on Apple Silicon and Intel)
- .NET 9.0 SDK or later (.NET 9, 10+ supported)
- Homebrew (for installing tools)
- LLVM (
lld-linklinker) - Rust/Cargo (for installing xwin)
- ~1.5GB disk space for Windows SDK
- Zig (~200MB, includes everything needed)
- No additional downloads - Zig includes built-in sysroot!
- Dynamic linking only: The generated executable requires Windows runtime DLLs
- MSVC ABI: The linker uses MSVC's ABI, ensuring compatibility with Windows
- No LTCG support:
lld-linkcannot link MSVC's Link-Time Code Generation (LTCG) objects, so full static linking with some libraries is not possible
Ensure LLVM is installed and in your PATH:
brew install lld
# Use universal command that works for both Intel and Apple Silicon
export PATH="$(brew --prefix lld)/bin:$PATH"
# Verify installation
which lld-link
lld-link --versionInstall xwin via Cargo:
cargo install --locked xwinMake sure your project has:
<PublishAot>true</PublishAot>
<AcceptVSBuildToolsLicense>true</AcceptVSBuildToolsLicense>And that the package is properly referenced.
Verify the SDK was downloaded:
ls -lh $HOME/.local/share/xwin-sdk/splat/If empty, re-run:
xwin --accept-license \
--cache-dir $HOME/.local/share/xwin-sdk \
--arch x86_64,aarch64 \
splat --preserve-ms-arch-notationThis happens because macOS has a case-sensitive file system, but Windows SDK uses mixed case (e.g., OleAut32.Lib).
Solution: Create lowercase copies in the SDK library directory:
cd $HOME/.local/share/xwin-sdk/splat/sdk/lib/um/x64 && \
for f in *.Lib; do
lower=$(echo "$f" | tr '[:upper:]' '[:lower:]')
[ "$f" != "$lower" ] && ln -sf "$f" "$lower"
doneMIT License - see LICENSE for details.
- Original PublishAotCrossXWin by @Windows10CE
- xwin by @Jake-Shadle
- LLVM lld
These projects together form a complete .NET Native AOT cross-compilation ecosystem:
-
PublishAotCross - Windows → Linux
Uses Zig as linker, supports linux-x64/arm64 and musl variants -
PublishAotCrossXWin - Linux → Windows
Uses lld-link + xwin, supports win-x64/arm64/x86 -
PublishAotCross.macOS (this project) - macOS → Windows/Linux
Combines both approaches for comprehensive cross-compilation from macOS
| Source ↓ / Target → | Windows | Linux | macOS |
|---|---|---|---|
| Windows | Native | ✅ PublishAotCross | ❌ |
| Linux | ✅ PublishAotCrossXWin | Native | ❌ |
| macOS | ✅ This project | ✅ This project | Native |
💡 macOS users get the best of both worlds - cross-compile to both Windows and Linux from a single machine!