Last active
February 19, 2024 18:36
-
-
Save Cloudef/acb74ff9e36ab41709479240596ab501 to your computer and use it in GitHub Desktop.
Zig based cross-compiling toolchain
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ pkgs ? import <nixpkgs> {}, lib ? pkgs.lib, stdenv ? pkgs.stdenv, zig ? pkgs.zig, static ? true, target }: | |
with lib; | |
with builtins; | |
# Zig based cross-compiling toolchain | |
# | |
# Major known issues: | |
# - Zig seems to randomly fail with parallel builds with 'error: unable to build ... CRT file: BuildingLibCObjectFailed' | |
# This seems like race condition with the cache? | |
# https://github.com/ziglang/zig/issues/13160 | |
# https://github.com/ziglang/zig/issues/9711 | |
# - Rust and zig fight for the ownership of libcompiler_rt | |
# https://github.com/ziglang/zig/issues/5320 | |
# | |
# Why zig for cross-compiling? | |
# | |
# Zig can cross-compile out of the box to many target without having to bootstrap the whole cross-compiler and various libcs | |
# This means builds are very fast | |
# | |
# TODO: static == true && glibc target should result in evaluation error | |
# TODO: provide zig-overlay.nix | |
let | |
zig-target = let | |
unknown-linux-mapper = x: concatStringsSep "-" (remove "unknown" (splitString "-" x)); | |
to-zig-target = { | |
"armv7l-multiplatform" = "arm-linux-gnueabi"; | |
"armv7l-hf-multiplatform" = "arm-linux-gnueabihf"; | |
"armv7l-unknown-linux-musleabi" = "arm-linux-musleabi"; | |
"armv7l-unknown-linux-musleabihf" = "arm-linux-musleabihf"; | |
"armv7b-multiplatform" = "armeb-linux-gnueabi"; | |
"armv7b-hf-multiplatform" = "armeb-linux-gnueabihf"; | |
"armv7b-unknown-linux-musleabi" = "armeb-linux-musleabi"; | |
"armv7b-unknown-linux-musleabihf" = "armeb-linux-musleabihf"; | |
"wasm32-freestanding-musl" = "wasm32-freestanding-musl"; | |
"wasm32-unknown-wasi" = "wasm32-wasi-musl"; | |
"aarch64-darwin" = "aarch64-macos-gnu"; | |
"x86_64-darwin" = "x86_64-macos-gnu"; | |
"aarch64-windows" = "aarch64-windows-gnu"; | |
"aarch64_be-windows" = "aarch64_be-windows-gnu"; | |
"arm-windows" = "arm-windows-gnu"; | |
"armeb-windows" = "armeb-windows-gnu"; | |
"i386-windows" = "i386-windows-gnu"; | |
"x86_64-windows" = "x86_64-windows-gnu"; | |
}; | |
in to-zig-target."${target}" or unknown-linux-mapper target; | |
zig-stdenv = let | |
write-zig-wrapper = cmd: pkgs.writeShellScript "zig-${cmd}" ''${zig}/bin/zig ${cmd} "$@"''; | |
zig-cc = write-zig-wrapper "cc"; | |
zig-cxx = write-zig-wrapper "c++"; | |
zig-ar = write-zig-wrapper "ar"; | |
zig-ranlib = write-zig-wrapper "ranlib"; | |
zig-dlltool = write-zig-wrapper "dlltool"; | |
zig-lib = write-zig-wrapper "lib"; | |
# XXX: Consider LLVM binutils instead? | |
bintools = pkgs.bintoolsNoLibc; | |
symlinks = [ | |
{ name = "bin/clang"; path = zig-cc; } | |
{ name = "bin/clang++"; path = zig-cxx; } | |
{ name = "bin/cc"; path = zig-cc; } | |
{ name = "bin/c++"; path = zig-cxx; } | |
{ name = "bin/ld"; path = zig-cc; } | |
{ name = "bin/gold"; path = zig-cc; } | |
{ name = "bin/ar"; path = zig-ar; } | |
{ name = "bin/ranlib"; path = zig-ranlib; } | |
{ name = "bin/dlltool"; path = zig-dlltool; } | |
{ name = "bin/lib"; path = zig-lib; } | |
] ++ map (x: { name = "bin/${x}"; path = "${bintools}/bin/${x}"; }) [ | |
"addr2line" "c++filt" "elfedit" "gprof" "gprofng" "nlmconv" "nm" "as" | |
"objcopy" "objdump" "readelf" "size" "strings" "windmc" "windres" "strip" | |
]; | |
zig-toolchain = pkgs.linkFarm "zig-toolchain" (symlinks | |
++ map (x: let y = last (splitString "/" x.name); in { name = "bin/${target}-${y}"; path = y; }) symlinks) // { | |
inherit (pkgs.llvmPackages.libclang) version; | |
pname = "zig-toolchain"; | |
isClang = true; | |
libllvm.out = ""; | |
}; | |
# Bunch of overrides to make sure we don't ever start bootstrapping another cross-compiler | |
overrides = self: super: let | |
to-override-set = x: genAttrs x (x: zig-toolchain); | |
llvmPackages = to-override-set [ "clang" "libclang" "lld" "llvm" "libllvm" "compiler-rt" "libunwind" "libstdcxx" "libcxx" "libcxxapi" "openmp" ]; | |
in (to-override-set [ | |
"gcc" "libgcc" "glibc" "bintools" "bintoolsNoLibc" "binutils" "binutilsNoLibc" | |
"gccCrossStageStatic" "preLibcCrossHeaders" "glibcCross" "muslCross" "libcCross" "threadsCross" | |
]) // { | |
inherit llvmPackages; | |
inherit (llvmPackages) clang libclang lld llvm libllvm libunwind libstdcxx libcxx libcxxapi openmp; | |
gccForLibs = null; # also disables --gcc-toolchain passed to compiler | |
}; | |
cross0 = import pkgs.path { | |
crossSystem.config = target; | |
localSystem.config = pkgs.buildPlatform.config; | |
crossOverlays = [overrides]; | |
}; | |
static-stdenv-maybe = x: if static then cross0.makeStatic x else x; | |
zig-stdenv0 = static-stdenv-maybe (cross0.overrideCC cross0.stdenv (cross0.wrapCCWith rec { | |
inherit (pkgs) gnugrep coreutils; | |
cc = zig-toolchain; libc = cc; libcxx = cc; | |
bintools = cross0.wrapBintoolsWith { inherit libc gnugrep coreutils; bintools = cc; }; | |
# XXX: -march is not compatible | |
# https://github.com/ziglang/zig/issues/4911 | |
extraBuildCommands = '' | |
substituteInPlace $out/nix-support/add-local-cc-cflags-before.sh --replace "${target}" "${zig-target}" | |
sed -i 's/-march[^ ]* *//g' $out/nix-support/cc-cflags-before | |
''; | |
})); | |
# Aaand .. finally our final stdenv :) | |
in zig-stdenv0.override { | |
inherit overrides; | |
# XXX: Zig doesn't support response file. Nixpkgs wants to use this for clang | |
# while zig cc is basically clang, it's still not 100% compatible. | |
# Probably should report this as a bug to zig upstream though. | |
preHook = '' | |
${cross0.stdenv.preHook} | |
export NIX_CC_USE_RESPONSE_FILE=0 | |
export ZIG_LOCAL_CACHE_DIR="$TMPDIR/zig-cache-${target}" | |
export ZIG_GLOBAL_CACHE_DIR="$ZIG_LOCAL_CACHE_DIR" | |
''; | |
}; | |
# Rust specific fixes, this lambda is exported so it can be used to wrap any rust toolchain | |
wrapRustToolchain = rust-toolchain: let | |
rust-config = let | |
to-rust-target = { | |
"aarch64-darwin" = "aarch64-apple-darwin"; | |
"x86_64-darwin" = "x86_64-macos-gnu"; | |
"aarch64-windows" = "aarch64-windows-gnu"; | |
"aarch64_be-windows" = "aarch64_be-windows-gnu"; | |
"arm-windows" = "arm-windows-gnu"; | |
"armeb-windows" = "armeb-windows-gnu"; | |
"i386-windows" = "i386-windows-gnu"; | |
"x86_64-windows" = "x86_64-windows-gnu"; | |
}; | |
in { | |
target = rec { | |
name = to-rust-target."${target}" or target; | |
cargo = stringAsChars (x: if x == "-" then "_" else x) (toUpper name); | |
# FIXME: libcompiler_rt.a removal for rust is a ugly hack that I eventually want to get rid of | |
# https://github.com/ziglang/zig/issues/5320 | |
linker = pkgs.writeShellScript "cc-target" '' | |
args=() | |
for v in "$@"; do | |
[[ "$v" == *self-contained/crt1.o ]] && continue | |
[[ "$v" == *self-contained/crti.o ]] && continue | |
[[ "$v" == *self-contained/crtn.o ]] && continue | |
[[ "$v" == *self-contained/crtend.o ]] && continue | |
[[ "$v" == *self-contained/crtbegin.o ]] && continue | |
[[ "$v" == *self-contained/libc.a ]] && continue | |
[[ "$v" == -lc ]] && continue | |
args+=("$v") | |
done | |
export ZIG_LOCAL_CACHE_DIR="$TMPDIR/zig-cache-${target}-rust" | |
export ZIG_GLOBAL_CACHE_DIR="$ZIG_LOCAL_CACHE_DIR" | |
if ! cc "''${args[@]}"; then | |
find "$ZIG_LOCAL_CACHE_DIR" -name libcompiler_rt.a | while read -r f; do | |
rm -f "$f"; touch "$f" | |
done | |
cc "''${args[@]}" | |
fi | |
''; | |
flags = if static then "-C target-feature=+crt-static" else ""; | |
}; | |
host = rec { | |
name = to-rust-target."${stdenv.buildPlatform.system}" or target; | |
cargo = stringAsChars (x: if x == "-" then "_" else x) (toUpper name); | |
# XXX: The fact darwin does not set libiconv linker path may be a bug | |
# darwin.cctools / codesign_allocate is a known issue | |
# ideally we could just set this to stdenv.cc though | |
# https://github.com/NixOS/nixpkgs/pull/148282 | |
linker = if stdenv.isDarwin then pkgs.writeShellScript "cc-host" | |
''PATH="${pkgs.darwin.cctools}/bin:$PATH" ${stdenv.cc}/bin/cc "$@" -L${pkgs.libiconv}/lib'' | |
else stdenv.cc; | |
}; | |
}; | |
# XXX: wrap rustc instead? | |
wrapped-cargo = pkgs.writeShellScript "wrapped-cargo" '' | |
export CARGO_BUILD_TARGET=${rust-config.target.name} | |
export CARGO_TARGET_${rust-config.host.cargo}_LINKER=${rust-config.host.linker} | |
export CARGO_TARGET_${rust-config.target.cargo}_LINKER=${rust-config.target.linker} | |
export CARGO_TARGET_${rust-config.target.cargo}_RUSTFLAGS="${rust-config.target.flags}" | |
${rust-toolchain}/bin/cargo "$@" | |
''; | |
in pkgs.symlinkJoin { | |
name = "${rust-toolchain.name}-wrapped"; | |
paths = [ rust-toolchain ]; | |
postBuild = '' | |
rm $out/bin/cargo | |
ln -s ${wrapped-cargo} $out/bin/cargo | |
''; | |
}; | |
cross = (import pkgs.path { | |
crossSystem.config = target; | |
localSystem.config = pkgs.buildPlatform.config; | |
crossOverlays = [(self: super: { | |
stdenv = zig-stdenv; | |
rust = wrapRustToolchain super.rust; | |
# XXX: libsepol issue on darwin, should be fixed upstrem instead | |
libsepol = super.libsepol.overrideAttrs (old: { | |
nativeBuildInputs = with pkgs; old.nativeBuildInputs ++ optionals (stdenv.isDarwin) [ | |
(writeShellScriptBin "gln" ''${pkgs.coreutils}/bin/ln "$@"'') | |
]; | |
}); | |
})]; | |
}); | |
in { | |
inherit target wrapRustToolchain; | |
stdenv = zig-stdenv; | |
pkgs = cross; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment