calog/tools/crossBuild.sh

255 lines
18 KiB
Bash
Executable file

#!/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 <target> 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 <target>, 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() { # <target: musl|win|mac-x64|mac-arm64>
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() { # <name> <target>
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 <sys/utsname.h>, 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 <sys/utsname.h> 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 <string.h>
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 <target> 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 ]