#!/usr/bin/env bash
# wasmc++: the REAL C++ cross driver for wasm32-hwjs (musl arm) —
# PLAN-CXX-STATUS-CHANNEL Track R §6 item 5 (closes the litb#104 "the C++
# compiler is a symlink to the C driver" gap).
#
# What it is: clang++ pointed at the staged libc++/libc++abi headers +
# bitcode archives (the sysroot build's `make cxx-runtimes`) on top of wasmcc's
# proven C substrate. COMPILE emits BITCODE with the pinned clang++ (the
# HwjsEHPreLower + HwjsCoroutinize passes run ONCE over the merged module
# inside wasmld — per-TU invoke/landingpad IR is exactly their input).
# LINK delegates to wasmcc's coro link arm (crt/RT-provenance/pool/thunks
# machinery inherited verbatim) with the C++ pieces injected:
# - every C++/C source on the link line is pre-compiled HERE with the
# C++ flag set (wasmcc would C-compile it with -fno-exceptions);
# - the §5.2 matcher/terminus TU (cxa_hwjs_status_channel.cpp.o) rides
# as an explicit ROOT — wasmld's need-only pool pull runs BEFORE the
# plugin compile, but the references to __hwjs_cxa_type_match / the
# strong __hwjs_uncaught_unwinding are created BY the pre-lowering
# DURING that compile, so an archive-resident copy is never pulled
# (and the RT's weak __hwjs_uncaught_unwinding would silently win);
# - libc++.a + libc++abi.a are appended (default `-lc++ -lc++abi`
# semantics; explicit -lc++/-lc++abi/-lstdc++ are folded into that).
#
# EXCEPTIONS ARE ON, ALWAYS (plan N1): C++ exceptions propagate on the hwjs
# status channel (STATUS-CHANNEL-CONTRACT §5.2) after HwjsEHPreLowerPass
# lowers invoke/landingpad/resume to status checks — no libunwind, no
# _Unwind_* two-phase ABI, no LSDA (plan D5). -fno-exceptions is REFUSED
# loudly: a -fno-exceptions frame between a raise and its handler would
# skip its cleanups silently (the I3 silent-transit shape), and shipping
# STL-with-exceptions is the whole point.
#
# CHECK EMISSION (I3, plan §7): C++ code must be check-emitting, so this
# driver defaults HWJS_EH_PRELOWER=1 on the substrate of the day (wasmld
# loads the pre-lower plugin and its never-silent SUMMARY guard proves the
# pass ran; under HWJS_STATUS_CHANNEL=1 the same pass is loaded by that
# knob and the artifact additionally attests hwjs.abi check_emitting).
# An explicit HWJS_EH_PRELOWER=0 for a C++ link is refused (gate arms may
# acknowledge via WASMLD_HWJS_ALLOW_RED_KNOBS=1 — the house RED-knob
# pattern; the E1 mechanism-off twin is built exactly that way).
set -euo pipefail
HWJS_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=libexec/find-llvm.sh
source "$HWJS_HOME/libexec/find-llvm.sh"
# The sysroot tree, a directory of this checkout. The default was the
# checkout ROOT, one level above where the artifacts are, left over from when
# a separate repository built the libc trees and always exported the pointer.
TOOLS_DIR="${HWJS_TOOLS_DIR:-${BRINTIUM_SYSROOT:-$HWJS_HOME/sysroot}}"
HWJS_LLVM_BIN="$(hwjs_llvm_bin "$HWJS_HOME/libexec")" \
|| { echo "wasmc++: FATAL: pinned LLVM not found (set WASMLD_HWJS_LLVM_BIN, or build it per toolchain/llvm-from-source.md)." >&2; exit 1; }
CLANGXX="$HWJS_LLVM_BIN/clang++"
WASMCC="${WASMCXX_CC_DRIVER:-$HWJS_HOME/libexec/brintcc-musl}"
# C++ is hwjs-substrate-only on this platform (there is no Asyncify C++
# story): default the coro arm ON. An explicit WASMLD_HWJS=0 is honored
# for compile-only introspection but refused for links below.
export WASMLD_HWJS="${WASMLD_HWJS:-1}"
# ── libc sysroot (same env surface as wasmcc; the default probes this
# checkout's musl-src alias first, then the musl BUILD tree — a caller given
# only a staged build tree has no musl-src, only build/musl-1.2.5-build) ────
_musl_root="$TOOLS_DIR/musl-src"
[[ -f "$_musl_root/include/stdlib.h" ]] || _musl_root="$TOOLS_DIR/build/musl-1.2.5-build"
LIBC_INCLUDE="${WACKY_LIBC_INCLUDE:-$_musl_root/include}"
LIBC_ARCH_INCLUDE="${WACKY_LIBC_ARCH_INCLUDE:-$_musl_root/arch/wasm32}"
LIBC_GENERIC_INCLUDE="${WACKY_LIBC_GENERIC_INCLUDE:-${LIBC_ARCH_INCLUDE%/wasm32}/generic}"
LIBC_OBJ_INCLUDE="${WACKY_LIBC_OBJ_INCLUDE:-${LIBC_INCLUDE%/include}/obj/include}"
WACKY_LIBC_DIR="${WACKY_LIBC_DIR:-$TOOLS_DIR/build/musl-1.2.5-build/lib}"
# ── the staged C++ runtimes (the sysroot build's `make cxx-runtimes`) ────────────
LIBCXX_INC="${WACKY_LIBCXX_INC:-$TOOLS_DIR/build/musl-1.2.5-build/include/c++/v1}"
LIBCXX_LIBDIR="${WACKY_LIBCXX_LIBDIR:-$TOOLS_DIR/build/musl-1.2.5-build/lib}"
# Kernel UAPI headers (linux/*.h asm/*.h): libc++'s atomic machinery needs
# them (linux/futex.h); appended LAST so they can never shadow a libc or
# libc++ header.
#
# UNLIKE THE GLIBC ARM, this one is genuinely optional: a musl C++ line that
# never reaches the atomics wait machinery compiles without them, and the
# include below is added only when the directory exists. What is NOT optional
# is that the pointer be the real one. The value here used to be a path into a
# particular distribution's build tree, reached by walking out of the sysroot —
# it named nothing after that tree moved, and clang drops an -isystem that is
# not there without a word, so a C++ file that did include linux/futex.h
# failed as a missing header rather than as a missing input. There is one
# pointer at the kernel's headers in this toolchain and this is it.
WACKY_UAPI_INCLUDE="${WACKY_UAPI_INCLUDE:-${BRINTIUM_KERNEL_HEADERS:-}}"
# ── refusals (loud, value-keyed — never guess) ──────────────────────────────
for a in "$@"; do
case "$a" in
-fno-exceptions)
echo "wasmc++: FATAL: -fno-exceptions on a C++ line — exceptions are ON on wasm32-hwjs (they ride the status channel; PLAN-CXX-STATUS-CHANNEL N1). A -fno-exceptions frame between a raise and its handler would skip its cleanups SILENTLY (the I3 silent-transit shape). If a package only builds with exceptions off, that is a finding to report, not a flag to pass." >&2
exit 1 ;;
esac
done
case "${HWJS_EH_PRELOWER:-}" in
'') export HWJS_EH_PRELOWER=1 ;; # the check-emitting default (I3)
1) ;;
0)
if [[ "${WASMLD_HWJS_ALLOW_RED_KNOBS:-0}" != "1" ]]; then
echo "wasmc++: FATAL: HWJS_EH_PRELOWER=0 on a C++ line — without the EH pre-lowering the ported __cxa_throw raises on the status channel and RETURNS into code that never checks it (the demonstrated runtime-RED shape: noreturn-breach violation / I2 abort). C++ through this driver is check-emitting by default (I3). Set WASMLD_HWJS_ALLOW_RED_KNOBS=1 only for a deliberate mechanism-off RED arm." >&2
exit 1
fi
echo "wasmc++: NOTE (HWJS_EH_PRELOWER=0, RED arm ACKNOWLEDGED): building WITHOUT the EH pre-lowering — the mechanism-off twin. Non-production." >&2 ;;
*)
echo "wasmc++: FATAL: HWJS_EH_PRELOWER='${HWJS_EH_PRELOWER}' — this knob is VALUE-keyed (1 = on, 0 = RED arm, unset/empty = default 1); refusing to guess." >&2
exit 1 ;;
esac
# ── triples + flag sets ─────────────────────────────────────────────────────
COMPILE_TRIPLE="wasm32-hwjs-linux-musl"
COMPILE_TARGET="--target=$COMPILE_TRIPLE"
export WASMLD_HWJS_COMPILE_TARGET="${WASMLD_HWJS_COMPILE_TARGET:-$COMPILE_TRIPLE}"
# wasmcc COMMON_ARGS parity minus -fno-exceptions (see header), plus the
# C++ posture: -fexceptions -frtti explicit (wasm targets default
# exceptions OFF in clang — the flag is load-bearing).
COMMON_ARGS=(
-nostdlib
-nostdlibinc
-fno-builtin
-fexceptions -frtti
-fvisibility=default
-fno-pic -fno-PIE
-matomics -mbulk-memory -mmutable-globals -msign-ext
-mnontrapping-fptoint
-ffunction-sections -fdata-sections
)
CXX_ISYSTEM_ARGS=( -nostdinc++ )
[[ -d "$LIBCXX_INC" ]] || {
echo "wasmc++: FATAL: no staged libc++ headers at $LIBCXX_INC — run 'make -C sysroot cxx-runtimes', or set WACKY_LIBCXX_INC." >&2
exit 1
}
CXX_ISYSTEM_ARGS+=( -isystem "$LIBCXX_INC" )
for inc in "$LIBC_ARCH_INCLUDE" "$LIBC_GENERIC_INCLUDE" "$LIBC_OBJ_INCLUDE" "$LIBC_INCLUDE" "$WACKY_UAPI_INCLUDE"; do
[[ -d "$inc" ]] && CXX_ISYSTEM_ARGS+=( -isystem "$inc" )
done
# ── mode detection (mirror wasmcc) ──────────────────────────────────────────
is_link_mode=1
for a in "$@"; do
case "$a" in
-c|-E|-S|-M|-MM|-fsyntax-only|--version|-v|-dumpversion|-dumpmachine|-print-*)
is_link_mode=0 ;;
esac
done
if [[ "$is_link_mode" == "0" ]]; then
# ── COMPILE / preprocess / introspection ────────────────────────────────
# Bitcode out on the coro arm (wasmld partitions by magic); mirror
# wasmcc's default -o pinning (autotools suffix rules expect .o).
emit=()
compile=0
have_o=0
src=""
prev=""
for a in "$@"; do
case "$a" in
-c) compile=1; [[ "$WASMLD_HWJS" == "1" && "${WASMLD_HWJS_CONFTEST:-0}" == "0" ]] && emit=( -emit-llvm ) ;;
# BRINTOS-WORKAROUND: brintos/hwjs-cc#39 revisit
# The attached form must count too: missing it synthesises a second
# -o after the caller's args, so clang writes ./.o and the
# requested path never appears -- silently, exit 0. waf always emits
# -o.
-o|-o?*) have_o=1 ;;
esac
if [[ "$prev" != "-o" ]]; then
case "$a" in *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) src="$a" ;; esac
fi
prev="$a"
done
out=()
if [[ "$compile" == "1" && "$have_o" == "0" && -n "$src" ]]; then
base="${src##*/}"; base="${base%.*}"; out=( -o "${base}.o" )
fi
exec "$CLANGXX" \
"$COMPILE_TARGET" \
"${COMMON_ARGS[@]}" \
"${CXX_ISYSTEM_ARGS[@]}" \
"${emit[@]}" \
"$@" \
"${out[@]}"
fi
# ── LINK ────────────────────────────────────────────────────────────────────
if [[ "$WASMLD_HWJS" != "1" ]]; then
echo "wasmc++: FATAL: WASMLD_HWJS=0 on a C++ LINK — there is no non-coro C++ substrate on wasm32-hwjs (the status-channel lowering IS the exception mechanism). Unset WASMLD_HWJS or leave it 1." >&2
exit 1
fi
# Pre-compile every C-family source on the link line with the C++ flag set
# (wasmcc's own single-step path would C-compile it with -fno-exceptions).
# clang++ compiles a .c input as C++ here — the documented g++ behavior.
stage="$(mktemp -d "${TMPDIR:-/tmp}/wasmcxx-stage.XXXXXX")"
trap 'rm -rf "$stage"' EXIT
cxx_flags=()
expect=""
for a in "$@"; do
if [[ -n "$expect" ]]; then
[[ "$expect" == "cc" ]] && cxx_flags+=( "$a" )
expect=""
continue
fi
case "$a" in
-o|-L) expect="skip" ;;
-I|-D|-U|-include|-imacros|-isystem|-idirafter|-iquote|-Xclang)
cxx_flags+=( "$a" ); expect="cc" ;;
-o*|-L*|-l*|-Wl,*) : ;;
-*) cxx_flags+=( "$a" ) ;;
*) : ;;
esac
done
link_args=()
nostdlibxx=0
srcn=0
for a in "$@"; do
case "$a" in
-nostdlib|-nodefaultlibs|-nostdlib++) nostdlibxx=1; link_args+=( "$a" ) ;;
# this driver OWNS the C++ runtime archives (appended below with the
# matcher root); fold explicit spellings into that so the archive
# never rides the line twice
-lc++|-lc++abi|-lstdc++) : ;;
*.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++)
[[ -f "$a" ]] || { echo "wasmc++: FATAL: link-line source '$a' does not exist." >&2; exit 1; }
srcn=$((srcn + 1))
sb="$(basename "$a")"
so="$stage/linksrc-${srcn}-${sb%.*}.o"
"$CLANGXX" \
"$COMPILE_TARGET" \
"${COMMON_ARGS[@]}" \
"${CXX_ISYSTEM_ARGS[@]}" \
"${cxx_flags[@]}" \
-emit-llvm -c "$a" -o "$so" \
|| { echo "wasmc++: FATAL: single-step C++ compile of link-line source '$a' failed (it must join the merged-module pass pipeline; error above)." >&2; exit 1; }
link_args+=( "$so" )
;;
*) link_args+=( "$a" ) ;;
esac
done
CXX_LINK_EXTRAS=()
# -fexperimental-library (tzdb/PSTL/syncstream): the staged
# libc++experimental.a joins the link whenever it is staged — wasmld's pool
# pull is need-only, so non-experimental links are byte-identical. If the
# caller ASKED for the experimental library and the archive is not staged,
# refuse HERE with the staging recipe instead of leaking "undefined symbol
# std::chrono::get_tzdb" at the end of the link (the pre-2026-07-21 "config
# that lies" failure mode: __config_site advertised tzdb, nothing shipped it).
wants_experimental=0
for a in "$@"; do
[[ "$a" == "-fexperimental-library" ]] && wants_experimental=1
done
if [[ "$nostdlibxx" == "0" && "${WASMLD_HWJS_CONFTEST:-0}" == "0" ]]; then
MATCHER="${WACKY_LIBCXX_MATCHER:-$LIBCXX_LIBDIR/cxa_hwjs_status_channel.cpp.o}"
for f in "$MATCHER" "$LIBCXX_LIBDIR/libc++.a" "$LIBCXX_LIBDIR/libc++abi.a"; do
[[ -f "$f" ]] || {
echo "wasmc++: FATAL: missing staged C++ runtime piece '$f' — run 'make -C sysroot cxx-runtimes'." >&2
exit 1
}
done
LIBCXX_EXP="$LIBCXX_LIBDIR/libc++experimental.a"
if [[ "$wants_experimental" == "1" && ! -f "$LIBCXX_EXP" ]]; then
echo "wasmc++: FATAL: -fexperimental-library requested but no staged libc++experimental.a in $LIBCXX_LIBDIR — the headers would compile tzdb/PSTL/syncstream uses that cannot link. Rebuild the runtimes (LIBCXX_ENABLE_EXPERIMENTAL_LIBRARY=ON is the brintos cache default since 2026-07-21) and run 'make -C sysroot cxx-runtimes'." >&2
exit 1
fi
CXX_LINK_EXTRAS=( "$MATCHER" )
[[ -f "$LIBCXX_EXP" ]] && CXX_LINK_EXTRAS+=( "$LIBCXX_EXP" )
CXX_LINK_EXTRAS+=( "$LIBCXX_LIBDIR/libc++.a" "$LIBCXX_LIBDIR/libc++abi.a" )
# If a caller turns on the merge DCE (internalize+globaldce pre-pass;
# default OFF on the musl arm), the matcher TU's definitions look dead
# at DCE time — the references to them are created BY the pre-lowering
# during the LATER plugin compile — and would be deleted, leaking
# phantom env imports (witnessed on the glibc arm, where DCE defaults
# ON). Pin them as DCE roots whenever DCE is requested.
if [[ -n "${WASMLD_HWJS_DCE_MERGE:-}" && "${WASMLD_HWJS_DCE_MERGE}" != "0" ]]; then
export WASMLD_HWJS_DCE_EXTRA_ROOTS="${WASMLD_HWJS_DCE_EXTRA_ROOTS:+${WASMLD_HWJS_DCE_EXTRA_ROOTS},}__hwjs_cxa_type_match,__hwjs_uncaught_unwinding"
fi
elif [[ "$nostdlibxx" == "0" ]]; then
# conftest fast path: the archives would only be nm'd for the witness
# allowlist; hand them over so C++ probes answer honestly.
for f in "$LIBCXX_LIBDIR/libc++.a" "$LIBCXX_LIBDIR/libc++abi.a"; do
[[ -f "$f" ]] && CXX_LINK_EXTRAS+=( "$f" )
done
fi
# Delegate the link to wasmcc's coro arm: crt selection, RT provenance,
# musl pool, thunks, builtins — all inherited from the ONE proven C lane.
exec "$WASMCC" "${link_args[@]}" "${CXX_LINK_EXTRAS[@]}"