#!/usr/bin/env bash # # crossBuild.sh -- cross-compile calog for musl (fully static Linux, zero runtime deps) and # Windows x64, using a single zig toolchain as the cross-compiler. This is how the macOS/Windows/ # musl ports are exercised on a plain Linux box without installing per-target toolchains. # # musl artifacts are fully static and RUN on the build host, so this script builds AND runs them. # Windows artifacts (.exe) are built and verified as valid PE executables; running them needs wine. # # Requirements: zig (https://ziglang.org/download/). Point ZIG at the binary, or put it on PATH: # ZIG=/path/to/zig ./tools/crossBuild.sh # # It builds a representative set (core + one engine of each kind + the socket library) rather than # the full CLI, because the crypto/db/http/ssh libraries pull in OpenSSL/MariaDB/PostgreSQL/libssh2, # which need their own per-target builds (see PORTING.md). set -u cd "$(dirname "$0")/.." ROOT=$(pwd) ZIG=${ZIG:-zig} OUT=${OUT:-build/cross} mkdir -p "$OUT" if ! command -v "$ZIG" >/dev/null 2>&1; then echo "error: zig not found. Set ZIG=/path/to/zig (see https://ziglang.org/download/)." >&2 exit 1 fi echo "zig: $($ZIG version)" CORE="src/broker.c src/value.c src/context.c" LUASRC=$(ls vendor/lua/src/*.c | grep -vE '/(lua|luac)\.c$' | tr '\n' ' ') ENET_UNIX=$(ls vendor/enet/*.c | grep -vE '/win32\.c$' | tr '\n' ' ') ENET_WIN=$(ls vendor/enet/*.c | grep -vE '/unix\.c$' | tr '\n' ' ') SQSRC=$(ls vendor/squirrel-src/squirrel/*.cpp | tr '\n' ' ') BERRYSRC=$(ls vendor/berry/src/*.c vendor/berry/default/be_port.c vendor/berry/default/be_modtab.c | tr '\n' ' ') JANETDEF="-DJANET_NO_NET -DJANET_NO_PROCESSES -DJANET_NO_DYNAMIC_MODULES" MBSRC=$(ls vendor/ourbasic/*.c | tr '\n' ' ') ENETDEF="-D_GNU_SOURCE -DHAS_FCNTL=1 -DHAS_POLL=1 -DHAS_GETADDRINFO=1 -DHAS_GETNAMEINFO=1 -DHAS_GETHOSTBYNAME_R=1 -DHAS_GETHOSTBYADDR_R=1 -DHAS_INET_PTON=1 -DHAS_INET_NTOP=1 -DHAS_MSGHDR_FLAGS=1 -DHAS_SOCKLEN_T=1" pass=0; fail=0; skip=0 # Per-target OpenSSL, built by `tools/crossDeps.sh openssl`. calogNet has needed it since # the tcp transport gained TLS, which is what this script's testNet case links against. Echoes the # include + archive flags for , or nothing when that target's OpenSSL has not been built -- # the caller then SKIPS testNet rather than reporting a failure the script cannot fix itself. # mac wants the repacked BSD-format archives; zig's Mach-O linker cannot read OpenSSL's GNU-format .a. ossl_flags() { # local dir="$ROOT/build/cross/$1" local libs="$dir/openssl-src" case "$1" in mac-x64|mac-arm64) libs="$dir/openssl-repack" ;; esac [ -f "$libs/libssl.a" ] && [ -f "$libs/libcrypto.a" ] || return 1 echo "-I$dir/openssl-src/include $libs/libssl.a $libs/libcrypto.a" } skip_no_ossl() { # echo " [$2 SKIP] $1 -- no OpenSSL for this target; run: ./tools/crossDeps.sh $2 openssl" skip=$((skip+1)) } # Build a fully static musl binary and RUN it here. A nonzero exit is a FAILURE: every test in this # matrix returns 0 only when all its checks passed. This used to pass them regardless ("RAN, nonzero" # counted as ok, on the theory that some exit nonzero by design -- none do), and it hid a real one: # testEngineMyBasic reported success on musl while 13 of its 20 checks were failing. The last line of # output is echoed so the check counts are visible, and timeout bounds a hang. mrun() { # name, then compiler args -- build static musl, then RUN it; nonzero == FAIL local name=$1; shift if "$@" -o "$OUT/$name-musl" 2>"$OUT/$name-musl.err"; then if timeout 120 "$OUT/$name-musl" >"$OUT/$name-musl.out" 2>&1; then echo " [musl RUN ok] $name -- $(tail -n 1 "$OUT/$name-musl.out")"; pass=$((pass+1)) else echo " [musl RUN FAILED] $name (see $OUT/$name-musl.out)"; fail=$((fail+1)) fi else echo " [musl FAIL] $name (see $OUT/$name-musl.err)"; fail=$((fail+1)) fi } wbuild() { # name, then compiler args -- build a Windows .exe, verify it is a PE local name=$1; shift if "$@" -o "$OUT/$name.exe" 2>"$OUT/$name.exe.err"; then if file "$OUT/$name.exe" | grep -q 'PE32+ executable'; then echo " [win BUILT] $name.exe"; pass=$((pass+1)) else echo " [win ??] $name.exe (not a PE?)"; fail=$((fail+1)) fi else echo " [win FAIL] $name.exe (see $OUT/$name.exe.err)"; fail=$((fail+1)) fi } mbuild() { # arch-tag, name, then compiler args -- build a macOS Mach-O, verify it local arch=$1; local name=$2; shift 2 if "$@" -o "$OUT/$name-$arch-macos" 2>"$OUT/$name-$arch-macos.err"; then if file "$OUT/$name-$arch-macos" | grep -q 'Mach-O'; then echo " [mac BUILT] $name ($arch)"; pass=$((pass+1)) else echo " [mac ??] $name ($arch, not Mach-O?)"; fail=$((fail+1)) fi else echo " [mac FAIL] $name ($arch, see $OUT/$name-$arch-macos.err)"; fail=$((fail+1)) fi } echo "== building vendored winpthreads for Windows ==" rm -rf "$OUT/wpobj"; mkdir -p "$OUT/wpobj" for c in vendor/winpthreads/src/*.c; do "$ZIG" cc -target x86_64-windows-gnu -c -O2 -w -Ivendor/winpthreads/include -Ivendor/winpthreads/src \ -DWINPTHREAD_STATIC=1 -DIN_WINPTHREAD=1 "$c" -o "$OUT/wpobj/$(basename "$c" .c).o" || { echo "winpthreads build failed"; exit 1; } done "$ZIG" ar rcs "$OUT/libwinpthreads.a" "$OUT"/wpobj/*.o WPA="$OUT/libwinpthreads.a" WPINC="-Ivendor/winpthreads/include -DWINPTHREAD_STATIC=1" # s7 on Windows: s7.c takes its POSIX path (it only sets MS_Windows from _MSC_VER, which clang/mingw # never defines) and includes , which mingw-w64 does not ship. Generate the same # minimal shim the full-CLI build uses, rather than depend on that one -- it lives under build/, # which is untracked, so it cannot be assumed present. mkdir -p "$OUT/shim/sys" cat > "$OUT/shim/sys/utsname.h" <<'UTSNAME' // utsname.h -- cross-build compatibility shim for vendored s7 on mingw-w64 (generated by // tools/crossBuild.sh). s7.c includes under `#if !MS_Windows`, and it only defines // MS_Windows from _MSC_VER, so the clang/mingw cross build wrongly takes the POSIX path. This // satisfies the include without patching the pristine vendored source; it is on s7's include path // only, so nothing else in calog sees it. #ifndef CALOG_SHIM_SYS_UTSNAME_H #define CALOG_SHIM_SYS_UTSNAME_H #include struct utsname { char sysname[65]; char nodename[65]; char release[65]; char version[65]; char machine[65]; }; static inline int uname(struct utsname *u) { if (u == NULL) { return -1; } memset(u, 0, sizeof(*u)); strcpy(u->sysname, "Windows"); strcpy(u->nodename, "localhost"); strcpy(u->release, "10"); strcpy(u->version, "10"); strcpy(u->machine, "x86_64"); return 0; } #endif UTSNAME S7SHIM="-I$OUT/shim" echo "== musl (fully static Linux, built AND run) ==" mrun testEngineLua "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src -DLUA_USE_POSIX $CORE src/lua/luaEngine.c src/lua/luaAdapter.c tests/testEngineLua.c $LUASRC -lm mrun testEngineJs "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/js -Ilibs -Ivendor/quickjs -D_GNU_SOURCE $CORE src/js/jsEngine.c src/js/jsAdapter.c tests/testEngineJs.c vendor/quickjs/quickjs.c vendor/quickjs/libregexp.c vendor/quickjs/libunicode.c vendor/quickjs/dtoa.c -lm mrun testEngineMyBasic "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/mybasic -Ilibs -Ivendor/ourbasic -DMB_DOUBLE_FLOAT $CORE src/mybasic/mybasicEngine.c src/mybasic/mybasicAdapter.c tests/testEngineMyBasic.c $MBSRC -lm if OSSL=$(ossl_flags musl); then mrun testNet "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src -Ivendor/enet/include -DLUA_USE_POSIX $ENETDEF $CORE src/lua/luaEngine.c src/lua/luaAdapter.c libs/calogNet.c libs/calogHandle.c tests/testNet.c $LUASRC $ENET_UNIX $OSSL -lm # Real TLS at runtime, so the cross-built OpenSSL is EXERCISED rather than merely link-verified: # testHttps generates an RSA key + self-signed cert in-process and drives a loopback TLS server. # No external server and no egress. musl only -- testHttps uses POSIX sockets/pthreads/mkstemp, # which x86_64-windows-gnu cannot compile, and a mac build would add nothing over testNet's link. mrun testHttps "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src -DLUA_USE_POSIX $CORE src/lua/luaEngine.c src/lua/luaAdapter.c libs/calogHttp.c tests/testHttps.c $LUASRC $OSSL -lm else skip_no_ossl testNet musl skip_no_ossl testHttps musl fi mrun testEngineBerry "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/berry -Ilibs -Ivendor/berry/src -Ivendor/berry $CORE src/berry/berryEngine.c src/berry/berryAdapter.c tests/testEngineBerry.c $BERRYSRC -lm mrun testEngineS7 "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/s7 -Ilibs -Ivendor/s7 -D_GNU_SOURCE $CORE src/s7/s7Engine.c src/s7/s7Adapter.c tests/testEngineS7.c vendor/s7/s7.c -lm mrun testEngineWren "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/wren -Ilibs -Ivendor/wren $CORE src/wren/wrenEngine.c src/wren/wrenAdapter.c tests/testEngineWren.c vendor/wren/wren.c -lm mrun testEngineJanet "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/janet -Ilibs -Ivendor/janet $JANETDEF $CORE src/janet/janetEngine.c src/janet/janetAdapter.c tests/testEngineJanet.c vendor/janet/janet.c -lm # mruby and Tcl are the two engines that need a per-target prebuilt archive rather than vendored C: # tools/crossDeps.sh mruby / tcl produces them. Skip cleanly when they are not there. if [ -f "$ROOT/build/cross/musl/mruby/libmruby.a" ]; then mrun testEngineMruby "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/mruby -Ilibs -Ivendor/mruby/include -Ivendor/mruby/build/cross-musl/include -DMRB_USE_DEBUG_HOOK $CORE src/mruby/mrubyEngine.c src/mruby/mrubyAdapter.c tests/testEngineMruby.c "$ROOT/build/cross/musl/mruby/libmruby.a" -lm else echo " [musl SKIP] testEngineMruby -- run: ./tools/crossDeps.sh musl mruby"; skip=$((skip+1)) fi if [ -f "$ROOT/build/cross/musl/tcl/libtcl9.0.a" ]; then mrun testEngineTcl "$ZIG" cc -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/tcl -Ilibs -Ivendor/tcl/generic -Ivendor/tcl/unix $CORE src/tcl/tclEngine.c src/tcl/tclAdapter.c tests/testEngineTcl.c "$ROOT/build/cross/musl/tcl/libtcl9.0.a" "$ROOT/build/cross/musl/lib/libz.a" -lm else echo " [musl SKIP] testEngineTcl -- run: ./tools/crossDeps.sh musl tcl"; skip=$((skip+1)) fi # Squirrel is C++ (vendored VM); the calog .c files stay C (-x c), the VM is C++ (-x c++). It goes # through the same runner as everything else -- it used to run its binary and DISCARD the exit code, # reporting "RUN ok" unconditionally, which is the same defect as the old lenient mrun but total. mrun testEngineSquirrel "$ZIG" c++ -target x86_64-linux-musl -static -O2 -pthread -w -Isrc -Isrc/squirrel -Ilibs -Ivendor/squirrel-src/include -Ivendor/squirrel-src/squirrel -D_SQ64 -DSQUSEDOUBLE \ -x c $CORE src/squirrel/squirrelEngine.c src/squirrel/squirrelAdapter.c tests/testEngineSquirrel.c -x c++ $SQSRC echo "== Windows x64 (.exe built against vendored winpthreads; run needs wine) ==" wbuild testEngineLua "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src $WPINC $CORE src/lua/luaEngine.c src/lua/luaAdapter.c tests/testEngineLua.c $LUASRC "$WPA" wbuild testEngineBerry "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/berry -Ilibs -Ivendor/berry/src -Ivendor/berry $WPINC $CORE src/berry/berryEngine.c src/berry/berryAdapter.c tests/testEngineBerry.c $BERRYSRC "$WPA" wbuild testEngineS7 "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/s7 -Ilibs -Ivendor/s7 $S7SHIM -D_GNU_SOURCE $WPINC $CORE src/s7/s7Engine.c src/s7/s7Adapter.c tests/testEngineS7.c vendor/s7/s7.c "$WPA" wbuild testEngineWren "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/wren -Ilibs -Ivendor/wren $WPINC $CORE src/wren/wrenEngine.c src/wren/wrenAdapter.c tests/testEngineWren.c vendor/wren/wren.c "$WPA" wbuild testEngineJanet "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/janet -Ilibs -Ivendor/janet $JANETDEF $WPINC $CORE src/janet/janetEngine.c src/janet/janetAdapter.c tests/testEngineJanet.c vendor/janet/janet.c "$WPA" if [ -f "$ROOT/build/cross/win/tcl/libtcl90.a" ]; then wbuild testEngineTcl "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/tcl -Ilibs -Ivendor/tcl/generic -Ivendor/tcl/win -DSTATIC_BUILD $WPINC $CORE src/tcl/tclEngine.c src/tcl/tclAdapter.c tests/testEngineTcl.c "$ROOT/build/cross/win/tcl/libtcl90.a" "$WPA" -lws2_32 -lnetapi32 -luserenv -lole32 -loleaut32 -luuid else echo " [win SKIP] testEngineTcl -- run: ./tools/crossDeps.sh win tcl"; skip=$((skip+1)) fi if [ -f "$ROOT/build/cross/win/mruby/libmruby.a" ]; then wbuild testEngineMruby "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/mruby -Ilibs -Ivendor/mruby/include -Ivendor/mruby/build/cross-mingw/include -DMRB_USE_DEBUG_HOOK $WPINC $CORE src/mruby/mrubyEngine.c src/mruby/mrubyAdapter.c tests/testEngineMruby.c "$ROOT/build/cross/win/mruby/libmruby.a" "$WPA" else echo " [win SKIP] testEngineMruby -- run: ./tools/crossDeps.sh win mruby"; skip=$((skip+1)) fi if OSSL=$(ossl_flags win); then wbuild testNet "$ZIG" cc -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src -Ivendor/enet/include $WPINC $CORE src/lua/luaEngine.c src/lua/luaAdapter.c libs/calogNet.c libs/calogHandle.c tests/testNet.c $LUASRC $ENET_WIN "$WPA" $OSSL -lws2_32 -lwinmm -lcrypt32 -lbcrypt -ladvapi32 -luser32 else skip_no_ossl testNet win fi # Squirrel: C++ VM, so the C sources are forced with -x c and the VM with -x c++, as on musl. if "$ZIG" c++ -target x86_64-windows-gnu -O2 -w -Isrc -Isrc/squirrel -Ilibs -Ivendor/squirrel-src/include -Ivendor/squirrel-src/squirrel -D_SQ64 -DSQUSEDOUBLE $WPINC \ -x c $CORE src/squirrel/squirrelEngine.c src/squirrel/squirrelAdapter.c tests/testEngineSquirrel.c -x c++ $SQSRC -x none "$WPA" -o "$OUT/testEngineSquirrel.exe" 2>"$OUT/sq-win.err"; then if file "$OUT/testEngineSquirrel.exe" | grep -q 'PE32+ executable'; then echo " [win BUILT] testEngineSquirrel.exe"; pass=$((pass+1)) else echo " [win ??] testEngineSquirrel.exe (not a PE?)"; fail=$((fail+1)); fi else echo " [win FAIL] testEngineSquirrel.exe (see $OUT/sq-win.err)"; fail=$((fail+1)); fi echo "== macOS (Mach-O built via zig's libSystem stub -- Intel + Apple Silicon; run needs a Mac) ==" for arch in x86_64 aarch64; do mbuild "$arch" testEngineLua "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src -DLUA_USE_MACOSX $CORE src/lua/luaEngine.c src/lua/luaAdapter.c tests/testEngineLua.c $LUASRC mbuild "$arch" testEngineBerry "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/berry -Ilibs -Ivendor/berry/src -Ivendor/berry $CORE src/berry/berryEngine.c src/berry/berryAdapter.c tests/testEngineBerry.c $BERRYSRC mbuild "$arch" testEngineS7 "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/s7 -Ilibs -Ivendor/s7 -D_GNU_SOURCE $CORE src/s7/s7Engine.c src/s7/s7Adapter.c tests/testEngineS7.c vendor/s7/s7.c mbuild "$arch" testEngineWren "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/wren -Ilibs -Ivendor/wren $CORE src/wren/wrenEngine.c src/wren/wrenAdapter.c tests/testEngineWren.c vendor/wren/wren.c mbuild "$arch" testEngineJanet "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/janet -Ilibs -Ivendor/janet $JANETDEF $CORE src/janet/janetEngine.c src/janet/janetAdapter.c tests/testEngineJanet.c vendor/janet/janet.c MACT=$(echo "$arch" | sed 's/^x86_64$/mac-x64/; s/^aarch64$/mac-arm64/') if [ -f "$ROOT/build/cross/$MACT/mruby/libmruby.a" ]; then mbuild "$arch" testEngineMruby "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/mruby -Ilibs -Ivendor/mruby/include -Ivendor/mruby/build/cross-$MACT/include -DMRB_USE_DEBUG_HOOK $CORE src/mruby/mrubyEngine.c src/mruby/mrubyAdapter.c tests/testEngineMruby.c "$ROOT/build/cross/$MACT/mruby/libmruby.a" else echo " [mac SKIP] testEngineMruby ($arch) -- run: ./tools/crossDeps.sh $MACT mruby"; skip=$((skip+1)) fi # Tcl links zlib; there is no system libz for a cross target, so use the one build_codecs made. if [ -f "$ROOT/build/cross/$MACT/tcl/libtcl9.0.a" ] && [ -f "$ROOT/build/cross/$MACT/lib/libz.a" ]; then mbuild "$arch" testEngineTcl "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/tcl -Ilibs -Ivendor/tcl/generic -Ivendor/tcl/unix $CORE src/tcl/tclEngine.c src/tcl/tclAdapter.c tests/testEngineTcl.c "$ROOT/build/cross/$MACT/tcl/libtcl9.0.a" "$ROOT/build/cross/$MACT/lib/libz.a" else echo " [mac SKIP] testEngineTcl ($arch) -- run: ./tools/crossDeps.sh $MACT tcl codecs"; skip=$((skip+1)) fi if "$ZIG" c++ -target $arch-macos -O2 -w -Isrc -Isrc/squirrel -Ilibs -Ivendor/squirrel-src/include -Ivendor/squirrel-src/squirrel -D_SQ64 -DSQUSEDOUBLE \ -x c $CORE src/squirrel/squirrelEngine.c src/squirrel/squirrelAdapter.c tests/testEngineSquirrel.c -x c++ $SQSRC -o "$OUT/testEngineSquirrel-$arch-macos" 2>"$OUT/sq-$arch-macos.err"; then if file "$OUT/testEngineSquirrel-$arch-macos" | grep -q 'Mach-O'; then echo " [mac BUILT] testEngineSquirrel ($arch)"; pass=$((pass+1)) else echo " [mac ??] testEngineSquirrel ($arch, not Mach-O?)"; fail=$((fail+1)); fi else echo " [mac FAIL] testEngineSquirrel ($arch, see $OUT/sq-$arch-macos.err)"; fail=$((fail+1)); fi if OSSL=$(ossl_flags "$MACT"); then mbuild "$arch" testNet "$ZIG" cc -target $arch-macos -O2 -w -Isrc -Isrc/lua -Ilibs -Ivendor/lua/src -Ivendor/enet/include -DLUA_USE_MACOSX $ENETDEF $CORE src/lua/luaEngine.c src/lua/luaAdapter.c libs/calogNet.c libs/calogHandle.c tests/testNet.c $LUASRC $ENET_UNIX $OSSL else skip_no_ossl testNet "$arch" fi done echo if [ "$skip" -gt 0 ]; then echo "== cross-build: $pass ok, $fail failed, $skip skipped (artifacts in $OUT/) ==" else echo "== cross-build: $pass ok, $fail failed (artifacts in $OUT/) ==" fi [ "$fail" -eq 0 ]