Skip to content

Instantly share code, notes, and snippets.

@Cloudef
Last active February 19, 2024 18:36
Show Gist options
  • Save Cloudef/acb74ff9e36ab41709479240596ab501 to your computer and use it in GitHub Desktop.
Save Cloudef/acb74ff9e36ab41709479240596ab501 to your computer and use it in GitHub Desktop.
Zig based cross-compiling toolchain
{ 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