This approach was originally verified by @luislavena and first posted here, here and here, big thanks to him for proving this solution work, and he showed a little bit of this in following videos:
Building CLIs with Crystal - Quick cross-compilation - Part 1 Building CLIs with Crystal - Quick cross-compilation - Part 2
Here are some links explain how zig cc
link work:
zig cc
: a Powerful Drop-In Replacement for GCC/Clang
You can use a system-independent
way to generate (static) bianry for the following platforms.
╰─ $ zig targets | jq .libc
[
"aarch64_be-linux-gnu",
"aarch64_be-linux-musl",
"aarch64_be-windows-gnu",
"aarch64-linux-gnu",
"aarch64-linux-musl",
"aarch64-windows-gnu",
"aarch64-macos-none",
"aarch64-macos-none",
"aarch64-macos-none",
"armeb-linux-gnueabi",
"armeb-linux-gnueabihf",
"armeb-linux-musleabi",
"armeb-linux-musleabihf",
"armeb-windows-gnu",
"arm-linux-gnueabi",
"arm-linux-gnueabihf",
"arm-linux-musleabi",
"arm-linux-musleabihf",
"thumb-linux-gnueabi",
"thumb-linux-gnueabihf",
"thumb-linux-musleabi",
"thumb-linux-musleabihf",
"arm-windows-gnu",
"csky-linux-gnueabi",
"csky-linux-gnueabihf",
"x86-linux-gnu",
"x86-linux-musl",
"x86-windows-gnu",
"m68k-linux-gnu",
"m68k-linux-musl",
"mips64el-linux-gnuabi64",
"mips64el-linux-gnuabin32",
"mips64el-linux-musl",
"mips64-linux-gnuabi64",
"mips64-linux-gnuabin32",
"mips64-linux-musl",
"mipsel-linux-gnueabi",
"mipsel-linux-gnueabihf",
"mipsel-linux-musl",
"mips-linux-gnueabi",
"mips-linux-gnueabihf",
"mips-linux-musl",
"powerpc64le-linux-gnu",
"powerpc64le-linux-musl",
"powerpc64-linux-gnu",
"powerpc64-linux-musl",
"powerpc-linux-gnueabi",
"powerpc-linux-gnueabihf",
"powerpc-linux-musl",
"riscv64-linux-gnu",
"riscv64-linux-musl",
"s390x-linux-gnu",
"s390x-linux-musl",
"sparc-linux-gnu",
"sparc64-linux-gnu",
"wasm32-freestanding-musl",
"wasm32-wasi-musl",
"x86_64-linux-gnu",
"x86_64-linux-gnux32",
"x86_64-linux-musl",
"x86_64-windows-gnu",
"x86_64-macos-none",
"x86_64-macos-none",
"x86_64-macos-none"
]
Uber donation for this solution(not programming use zig, just use zig cc
for link C++ code for linux-arm64/macOS) since 2022 January, and started using zig cc for all their C++ projects since H2 2022.
Check Bazel zig cc toolchain
All following code running on my laptop (Arch linux x86_64 Linux 6.7.0-arch3-1 with AMD Ryzen 7 7840HS)
In addition to the Crystal build toolchain, you need install zig use whatever package mananger you know, or build from source code, check https://ziglang.org/download.
For Arch, it just pacman -S zig
, check https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager
The current version of zig is 0.11.0
╰─ $ zig version
0.11.0
reproduces the example in the post zig cc: a Powerful Drop-In Replacement for GCC/Clang
, but use the latest version of zig.
generate a hello.exe
can running on Win10 on my linux machine.
╰─ $ \cat hello.c
#include <stdio.h>
int main(int argc, char **argv) {
fprintf(stderr, "Hello, World!\n");
return 0;
}
╰─ $ zig cc -o hello.exe hello.c -target x86_64-windows-gnu
╰─ $ 1 file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows, 7 sections
generate luagit aarch64-linux-musl static binary on my linux machine.
╰─ $ git clone https://github.com/LuaJIT/LuaJIT
Cloning into 'LuaJIT'...
╰─ $ cd LuaJIT/
╰─ $ make CC="zig cc -target aarch64-linux-musl" HOST_CC="zig cc" TARGET_STRIP="echo" LDFLAGS='-lunwind'
==== Building LuaJIT 2.1 ====
make -C src
make[1]: Entering directory '/home/common/Git/LuaJIT/src'
HOSTCC host/minilua.o
HOSTCC host/buildvm_asm.o
HOSTCC host/buildvm_peobj.o
HOSTCC host/buildvm_lib.o
HOSTCC host/buildvm_fold.o
CC lj_assert.o
CC lj_gc.o
CC lj_char.o
CC lj_buf.o
CC lj_obj.o
CC lj_str.o
CC lj_func.o
CC lj_tab.o
CC lj_udata.o
CC lj_meta.o
CC lj_debug.o
LLD Link... LLD Link... LLD Link... LLD Link... CC lj_prng.o
CC lj_vmevent.o
LLD Link... LLD Link... CC lj_vmmath.o
CC lj_strscan.o
LLD Link... CC lj_strfmt.o
CC lj_strfmt_num.o
LLD Link... CC lj_serialize.o
CC lj_api.o
LLD Link... CC lj_lex.o
LLD Link... CC lj_parse.o
LLD Link... LLD Link... CC lj_bcread.o
CC lj_bcwrite.o
CC lj_load.o
CC lj_ir.o
CC lj_opt_mem.o
LLD Link... CC lj_opt_narrow.o
CC lj_opt_dce.o
LLD Link... LLD Link... CC lj_opt_loop.o
CC lj_opt_split.o
CC lj_opt_sink.o
CC lj_mcode.o
LLD Link... LLD Link... LLD Link... CC lj_snap.o
CC lj_asm.o
LLD Link... LLD Link... LLD Link... CC lj_trace.o
LLD Link... CC lj_gdbjit.o
CC lj_ctype.o
CC lj_cdata.o
CC lj_cconv.o
LLD Link... CC lj_ccall.o
LLD Link... CC lj_ccallback.o
LLD Link... CC lj_carith.o
CC lj_clib.o
CC lj_cparse.o
LLD Link... LLD Link... CC lj_lib.o
CC lj_alloc.o
CC lib_aux.o
LLD Link... LLD Link... CC lib_package.o
CC lib_init.o
HOSTLINK host/minilua
VERSION luajit.h
DYNASM host/buildvm_arch.h
CC lj_profile.o
CC lj_state.o
CC luajit.o
HOSTCC host/buildvm.o
HOSTLINK host/buildvm
BUILDVM lj_bcdef.h
BUILDVM lj_ffdef.h
BUILDVM lj_libdef.h
BUILDVM lj_folddef.h
BUILDVM lj_vm.S
BUILDVM lj_recdef.h
BUILDVM jit/vmdef.lua
CC lj_err.o
CC lj_dispatch.o
CC lj_bc.o
CC lj_record.o
CC lj_crecord.o
CC lj_ffrecord.o
CC lib_base.o
CC lib_math.o
CC lib_string.o
CC lib_bit.o
CC lib_table.o
CC lib_io.o
CC lib_os.o
CC lib_debug.o
LLD Link... CC lib_jit.o
LLD Link... LLD Link... LLD Link... CC lib_ffi.o
LLD Link... CC lib_buffer.o
LLD Link... ASM lj_vm.o
CC lj_opt_fold.o
AR libluajit.a
DYNLINK libluajit.so
LLD Link... In file included from /usr/lib/zig/libc/musl/crt/rcrt1.c:3:
/usr/lib/zig/libc/musl/crt/../ldso/dlstart.c:146:20: warning: a function declaration without a prototype is deprecated in all versions of C and is treated as a zero-parameter prototype in C2x, conflicting with a subsequent definition [-Wdeprecated-non-prototype]
GETFUNCSYM(&dls2, __dls2, base+dyn[DT_PLTGOT]);
^
/usr/lib/zig/libc/musl/crt/rcrt1.c:11:13: note: conflicting prototype is here
hidden void __dls2(unsigned char *base, size_t *sp)
^
1 warning generated.
libluajit.so
LINK luajit
LLD Link... In file included from /usr/lib/zig/libc/musl/crt/rcrt1.c:3:
/usr/lib/zig/libc/musl/crt/../ldso/dlstart.c:146:20: warning: a function declaration without a prototype is deprecated in all versions of C and is treated as a zero-parameter prototype in C2x, conflicting with a subsequent definition [-Wdeprecated-non-prototype]
GETFUNCSYM(&dls2, __dls2, base+dyn[DT_PLTGOT]);
^
/usr/lib/zig/libc/musl/crt/rcrt1.c:11:13: note: conflicting prototype is here
hidden void __dls2(unsigned char *base, size_t *sp)
^
1 warning generated.
luajit
OK Successfully built LuaJIT
make[1]: Leaving directory '/home/common/Git/LuaJIT/src'
==== Successfully built LuaJIT 2.1 ====
╰─ $ file src/luajit
src/luajit: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), static-pie linked, with debug_info, not stripped
╰─ $ qemu-aarch64 src/luajit
LuaJIT 2.1.1703358377 -- Copyright (C) 2005-2023 Mike Pall. https://luajit.org/
JIT: ON fold cse dce fwd dse narrow loop abc sink fuse
>
We will build shards
binary for aarch64-linux-musl
target.
╰─ $ git clone https://github.com/crystal-lang/shards
Cloning into 'shards'...
╰─ $ git checkout v0.17.4
git checkout v0.17.4
HEAD is now at 29123fc Add CHANGELOG for 0.17.4 (#605)
╰─ $ shards build --production --static --release --no-debug --cross-compile --target=aarch64-linux-musl --link-flags=-s
Resolving dependencies
Fetching https://github.com/crystal-lang/crystal-molinillo.git
Installing molinillo (0.2.0)
Building: shards
cc /home/zw963/Crystal/crystal-lang/shards/bin/shards.o -o /home/zw963/Crystal/crystal-lang/shards/bin/shards -s -rdynamic -static -L/home/zw963/Crystal/bin/../lib/crystal -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent
So far we have seen that the compiler outputs the linker command using CC like this:
cc bin/shards.o -o bin/shards -s -rdynamic -static -L/home/zw963/Crystal/bin/../lib/crystal -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent
Usually we need linking it on the target platform, in our case, a ARM64 machine,
Now with zig cc
magic, we will link it directly on host platfrom instead, but we still need following static link libraries:
yaml-static
libevent-static
pcre2-dev
gc-dev
You can download both of them from https://dl-cdn.alpinelinux.org/alpine/v3.18/main/aarch64/, but thanks @@luislavena again, There is a ruby gem help us for this.
You need config ruby correctly to use this tool.
╰─ $ git clone https://github.com/luislavena/magic-haversack
╰─ $ cd magic-haversack
╰─ $ rake -T
rake clean # Remove any temporary products
rake clobber # Remove any generated files
rake clobber_package # Remove package products
rake fetch:aarch64-apple-darwin # Fetch all for 'aarch64-apple-darwin'
rake fetch:aarch64-linux-musl # Fetch all for 'aarch64-linux-musl'
rake fetch:all # Fetch all
rake fetch:x86_64-apple-darwin # Fetch all for 'x86_64-apple-darwin'
rake fetch:x86_64-linux-musl # Fetch all for 'x86_64-linux-musl'
rake package # Build all the packages
rake repackage # Force a rebuild of the package files
╰─ $ rake fetch:all
...
After done rake fetch:all
, all required (same version for different target) static library download into lib
folder and ready to use.
╰─ $ eza -T -L2 lib
lib
├── aarch64-apple-darwin
│ ├── libcrypto.a
│ ├── libevent.a
│ ├── libevent_pthreads.a
│ ├── libgc.a
│ ├── libgmp.a
│ ├── libpcre.a
│ ├── libpcre2-8.a
│ ├── libsodium.a
│ ├── libsqlite3.a
│ ├── libssl.a
│ ├── libyaml.a
│ ├── libz.a
│ └── pkgconfig
├── aarch64-linux-musl
│ ├── libcrypto.a
│ ├── libevent.a
│ ├── libevent_pthreads.a
│ ├── libgc.a
│ ├── libgmp.a
│ ├── libpcre.a
│ ├── libpcre2-8.a
│ ├── libsodium.a
│ ├── libsqlite3.a
│ ├── libssl.a
│ ├── libyaml.a
│ ├── libz.a
│ └── pkgconfig
├── x86_64-apple-darwin
│ ├── libcrypto.a
│ ├── libevent.a
│ ├── libevent_pthreads.a
│ ├── libgc.a
│ ├── libgmp.a
│ ├── libpcre.a
│ ├── libpcre2-8.a
│ ├── libsodium.a
│ ├── libsqlite3.a
│ ├── libssl.a
│ ├── libyaml.a
│ ├── libz.a
│ └── pkgconfig
└── x86_64-linux-musl
├── libcrypto.a
├── libevent.a
├── libevent_pthreads.a
├── libgc.a
├── libgmp.a
├── libpcre.a
├── libpcre2-8.a
├── libsodium.a
├── libsqlite3.a
├── libssl.a
├── libyaml.a
├── libz.a
└── pkgconfig
As of current version, only 4 targets and some of the most useful packages supported.
Check details on libs.yml
You can copy those file into whatever folder you wish, I copied it to ~/Crystal/static_libraries
personally.
╰─ $ ls ~/Crystal/static_libraries
aarch64-apple-darwin/ aarch64-linux-musl/ x86_64-apple-darwin/ x86_64-linux-musl/
We will continue the linking process in Step 1
If try running the outputed link command on local X86_64 machine, it not work obviously.
╰─ $ cd ~/Crystal/crystal-lang/shards
╰─ $ file bin/shards.o
bin/shards.o: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), not stripped
╰─ $ cc bin/shards.o -o bin/shards -s -rdynamic -static -L/home/zw963/Crystal/bin/../lib/crystal -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent
/usr/sbin/ld: bin/shards.o: Relocations in generic ELF (EM: 183)
/usr/sbin/ld: bin/shards.o: Relocations in generic ELF (EM: 183)
/usr/sbin/ld: bin/shards.o: Relocations in generic ELF (EM: 183)
/usr/sbin/ld: bin/shards.o: Relocations in generic ELF (EM: 183)
/usr/sbin/ld: bin/shards.o: Relocations in generic ELF (EM: 183)
/usr/sbin/ld: bin/shards.o: Relocations in generic ELF (EM: 183)
/usr/sbin/ld: bin/shards.o: error adding symbols: file in wrong format
collect2: error: ld returned 1 exit status
Now the magic show begins, if use zig cc
instead cc
, it works! with only trivial steps:
cc
replace withzig cc --target aarch64-linux-musl
-L/home/zw963/Crystal/bin/../lib/crystal
replace with/home/zw963/Crystal/static_libraries/aarch64-linux-musl
- append a
-lunwind
into link command is necessary, as mention by @luislavena in this video, which is something Crystal used to do backtrace.
So. the new linker command is:
╰─ $ zig cc --target=aarch64-linux-musl bin/shards.o -o bin/shards -s -rdynamic -static -L/home/zw963/Crystal/static_libraries/aarch64-linux-musl -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent -lunwind
LLD Link... In file included from /usr/lib/zig/libc/musl/crt/rcrt1.c:3:
/usr/lib/zig/libc/musl/crt/../ldso/dlstart.c:146:20: warning: a function declaration without a prototype is deprecated in all versions of C and is treated as a zero-parameter prototype in C2x, conflicting with a subsequent definition [-Wdeprecated-non-prototype]
GETFUNCSYM(&dls2, __dls2, base+dyn[DT_PLTGOT]);
^
/usr/lib/zig/libc/musl/crt/rcrt1.c:11:13: note: conflicting prototype is here
hidden void __dls2(unsigned char *base, size_t *sp)
^
1 warning generated.
╰─ $ file bin/shards
bin/shards: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), static-pie linked, stripped
╰─ $ qemu-aarch64 bin/shards --version
Shards 0.17.4 ()
It works!
built a binary for macOS is bascially same as above process, only two concerns
- you need compile-time tag
-Duse_libiconv
to allow developers to opt for libiconv variant instead of the built-in iconv support of their platform, check Allow explicit usage of libiconv for more details. - the zig cc supported target name not same as Crystal. you need change it manually.
We assume that all static library files are stored in ~/Crystal/static_libraries, like this:
╰─ $ eza -T -L 1 ~/Crystal/static_libraries
/home/zw963/Crystal/static_libraries
├── aarch64-apple-darwin
├── aarch64-linux-musl
├── x86_64-apple-darwin
└── x86_64-linux-musl
Following is the script, we name it as shards
, and rename orignal shards binary to shards.binary
.
You need at least BASH 4.0 to running this script, and some basic tools, grep, sed, tee, those should out of box on any linux distro, also install zig too.
#!/usr/bin/env bash
if [ "$1" == "build" ]; then
if echo "$*" |grep -F -qs -e '--target='; then
# use hash map bash 4.0 is requried.
declare -A zig_target_map=(
["x86_64-linux-musl"]="x86_64-linux-musl"
["aarch64-linux-musl"]="aarch64-linux-musl"
["x86_64-darwin"]="x86_64-macos-none"
["aarch64-darwin"]="aarch64-macos-none"
)
declare -A libname_map=(
["x86_64-linux-musl"]="x86_64-linux-musl"
["aarch64-linux-musl"]="aarch64-linux-musl"
["x86_64-darwin"]="x86_64-apple-darwin"
["aarch64-darwin"]="aarch64-apple-darwin"
)
tmp_file="$(mktemp -d)/$$"
cr_target=$(echo $* |sed 's#.*--target=\([a-z0-9_-]*\).*#\1#')
if [[ "$cr_target" =~ -darwin ]]; then
build_args="${@:2} -Duse_libiconv"
else
build_args="${@:2}"
fi
build_args=${build_args//--progress/}
shards.binary build ${build_args} |
grep '^cc ' |
sed "s#^cc#zig cc -target ${zig_target_map[$cr_target]}#" |
sed "s#-L[^ ]*#-L$HOME/Crystal/static_libraries/${libname_map[$cr_target]}#" |
sed "s#.*#& -lunwind#" |
tee $tmp_file
chmod +x $tmp_file && bash $tmp_file
else
shards.binary build "${@:2}"
fi
else
exec -a shards shards.binary "$@"
fi
╰─ $ cd ~/Crystal/crystal-lang/shards
╰─ $ git clean -fdx && shards build --production --static --no-debug --cross-compile --target=aarch64-linux-musl --link-flags=-s
Removing bin/shards
Removing bin/shards.o
Removing lib/
zig cc -target aarch64-linux-musl /home/zw963/Crystal/crystal-lang/shards/bin/shards.o -o /home/zw963/Crystal/crystal-lang/shards/bin/shards -s -rdynamic -static -L/home/zw963/Crystal/static_libraries/aarch64-linux-musl -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent -lunwind
╰─ $ file bin/shards
bin/shards: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), static-pie linked, stripped
╰─ $ git clean -fdx && shards build --production --static --no-debug --cross-compile --target=x86_64-linux-musl --link-flags=-s
Removing bin/shards
Removing bin/shards.o
Removing lib/
zig cc -target x86_64-linux-musl /home/zw963/Crystal/crystal-lang/shards/bin/shards.o -o /home/zw963/Crystal/crystal-lang/shards/bin/shards -s -rdynamic -static -L/home/zw963/Crystal/static_libraries/x86_64-linux-musl -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent -lunwind
╰─ $ file bin/shards
bin/shards: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), static-pie linked, stripped
╰─ $ git clean -fdx && shards build --production --static --no-debug --cross-compile --target=x86_64-darwin --link-flags=-s
Removing bin/shards
Removing bin/shards.o
Removing lib/
zig cc -target x86_64-macos-none /home/zw963/Crystal/crystal-lang/shards/bin/shards.o -o /home/zw963/Crystal/crystal-lang/shards/bin/shards -s -rdynamic -static -L/home/zw963/Crystal/static_libraries/x86_64-apple-darwin21.0 -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent -liconv -lunwind
╰─ $ file bin/shards
bin/shards: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE|HAS_TLV_DESCRIPTORS>
╰─ $ git clean -fdx && shards build --production --static --no-debug --cross-compile --target=aarch64-darwin --link-flags=-s
Removing bin/shards
Removing bin/shards.o
Removing lib/
zig cc -target aarch64-macos-none /home/zw963/Crystal/crystal-lang/shards/bin/shards.o -o /home/zw963/Crystal/crystal-lang/shards/bin/shards -s -rdynamic -static -L/home/zw963/Crystal/static_libraries/aarch64-apple-darwin21.0 -lyaml -lpcre2-8 -lgc -lpthread -ldl -levent -liconv -lunwind
╰─ $ file bin/shards
bin/shards: Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE|HAS_TLV_DESCRIPTORS>
As described in this forum:
OpenSSL is a issue, but it exists for any static-linking solution.
[quote="luislavena, post:5, topic:5588"]
Multi-threading is a bit flaky at this stage (-Dpreview_mt
), I don’t have concrete examples, but encountered random errors that are hard to reproduce.
[/quote]
This also needs more explanation from @luislavena.
There are some other concerns:
- The llvm version used by the Crystal compiler not matched with static libraries(e.g. libgc-dev)?
- build windows binary? it should be possible, as the
hello.exe
showcase.