#!/usr/bin/env bash # wasmcc: clang wrapper that compiles C to wasm32 against our (static) # musl libc port and our syscall ABI. Used for ALL userspace binaries: # bash, coreutils, /init, and the U0/U1 smoke fixtures in tests/. # # This wrapper is NEVER used for the kernel build. The kernel is built # by upstream Kbuild calling the toolchains-LLVM clang directly with # ARCH=wasm32. # # No WASI, no Emscripten runtime — just clang + wasm-ld + musl. # # CORO ARM (WASMLD_HWJS=1 — HWJS-3, docs/HWJS-3-USERLAND-DESIGN.md §7): # the whole-program brintium substrate. Compile mode emits BITCODE with the # pinned LLVM-22 clang (the HwjsCoroutinize pass runs ONCE over the merged # module inside wasmld, so per-TU objects must be IR the pin can read); # link mode swaps the startfiles to the coro crt (crt1_musl_hwjs.bc) + # hwjs_user_rt.bc + hwjs_user_thunks.o and drops -lc (wasmld substitutes # the musl bitcode pool). Assembly inputs (.s/.S) have no IR form and are # compiled native with the toolchains-LLVM clang on both arms (they join # the link's native stage — wasm_thread.S carries no suspend path by # construction). set -euo pipefail HWJS_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=libexec/find-llvm.sh source "$HWJS_HOME/libexec/find-llvm.sh" # shellcheck source=libexec/link-direct.sh source "$HWJS_HOME/libexec/link-direct.sh" # Base for the libc sysroot + startfiles this driver links: the sysroot tree, # which is a directory of this checkout. The default used to be the checkout # ROOT, from when the libc trees were built by a separate repository that # always exported HWJS_TOOLS_DIR — so nothing exercised it and it stayed one # level up from where the artifacts are. bin/brintcc resolves and exports the # pointer for every real invocation; this default is what a direct call gets, # and it now names the same tree. No machine-specific absolute path. TOOLS_DIR="${HWJS_TOOLS_DIR:-${BRINTIUM_SYSROOT:-$HWJS_HOME/sysroot}}" HWJS="${WASMLD_HWJS:-0}" HWJS_LLVM_BIN="$(hwjs_llvm_bin "$HWJS_HOME/libexec")" \ || { echo "wasmcc: FATAL: pinned LLVM not found (set WASMLD_HWJS_LLVM_BIN, or build it per toolchain/llvm-from-source.md)." >&2; exit 1; } LIBC_INCLUDE="${WACKY_LIBC_INCLUDE:-$TOOLS_DIR/musl-src/include}" LIBC_ARCH_INCLUDE="${WACKY_LIBC_ARCH_INCLUDE:-$TOOLS_DIR/musl-src/arch/wasm32}" # D1-BASH.a: the full musl header search path, baked into the wrapper so # wasmcc is a self-sufficient cross cc (autotools' $CPP carries only # CPPFLAGS, not CFLAGS, so the generated-header dir must NOT depend on the # caller passing -I). musl's own build order is: # arch/$ARCH → arch/generic → obj/include (GENERATED bits/*) → include # obj/include holds build-time-generated headers (bits/alltypes.h, # bits/syscall.h); arch/generic holds the generic bits/* fallbacks. LIBC_GENERIC_INCLUDE="${WACKY_LIBC_GENERIC_INCLUDE:-${LIBC_ARCH_INCLUDE%/wasm32}/generic}" LIBC_OBJ_INCLUDE="${WACKY_LIBC_OBJ_INCLUDE:-${LIBC_INCLUDE%/include}/obj/include}" # emsdk is RETIRED: the native-stage / driver clang now comes from the # from-source toolchains LLVM build (same pin convention as the coro # compile path's $HWJS_LLVM_BIN). Override via WASMLD_HWJS_LLVM_BIN. CLANG="$HWJS_LLVM_BIN/clang" WASMLD="${HWJS_WASMLD:-$HWJS_HOME/libexec/brintld}" # ── BrintOS wasm target triples (hwjs vendor: the prescriptive coroutinized # platform). The build SPLITS compile from link: # COMPILE (C -> bitcode) uses a *-linux-* triple so musl's headers see # __linux__; clang's DRIVER can only LINK a wasm32-*-unknown triple (it has no # *-linux wasm link target), so LINK — and any combined compile+link conftest — # uses the -unknown form. Mirrors wasmcc-glibc's COMPILE_TARGET/LINK_TARGET. COMPILE_TRIPLE="wasm32-hwjs-linux-musl" LINK_TRIPLE="wasm32-hwjs-unknown" COMPILE_TARGET="--target=$COMPILE_TRIPLE" LINK_TARGET="--target=$LINK_TRIPLE" # Tell wasmld (reached via -fuse-ld below) which COMPILE triple the program # bitcode carries, so the helper bitcode it synthesizes (abi_keepalive/main_bridge) # is stamped with the SAME triple and llvm-link sees one triple — no "Linking two # modules of different target triples" warning. export WASMLD_HWJS_COMPILE_TARGET="${WASMLD_HWJS_COMPILE_TARGET:-$COMPILE_TRIPLE}" ISYSTEM_ARGS=() for inc in "$LIBC_ARCH_INCLUDE" "$LIBC_GENERIC_INCLUDE" "$LIBC_OBJ_INCLUDE" "$LIBC_INCLUDE"; do if [[ -d "$inc" ]]; then ISYSTEM_ARGS+=( -isystem "$inc" ) fi done COMMON_ARGS=( -nostdlib -nostdlibinc -fno-builtin -fno-exceptions -fvisibility=default # STATIC, NON-PIC MODEL. The COMPILE triple carries a -linux- OS (so musl's # headers see __linux__), and for a *-linux target clang DEFAULTS to PIC/PIE # (__PIC__/__PIE__ defined). That is wrong for our whole-program, statically # linked wasm image: it perturbs codegen vs the pre-rename wasm32-unknown # compile triple, and — critically — musl's ./configure keys on __PIC__ and, # when set, emits `AOBJS = $(LOBJS)`, building the static libc.a from the PIC # `-DSHARED` .lo objects. That leaves ZERO .o for the musl-bc pool copy loop # (find -name '*.o') and breaks the coro libc pool. Force non-PIC here so the # triple rename stays codegen-neutral and configure sees pic_default=no. -fno-pic -fno-PIE -matomics -mbulk-memory -mmutable-globals -msign-ext -mnontrapping-fptoint -ffunction-sections -fdata-sections ) # ============================================================ # D1-BASH.a: full cross C-compiler-DRIVER mode. # # Autotools (bash/coreutils ./configure + make) drive CC both to # compile (-c) AND to LINK whole executables: `CC conftest.c -o # conftest`, `CC a.o b.o -lfoo -o prog`. The very first configure # gate is "can the C compiler create executables", so a bare clang # (-nostdlib, no startfiles, no libc) would fail immediately. # # When NO compile-only / preprocess-only flag is present we are in # LINK mode: inject the K7 musl startfiles (crt1_musl + wasm_thread) # before the caller's inputs, append `-L/lib -lc`, and route # the link through the wasmld wrapper (memory imports, exports, and # the Asyncify pass). Compile-only (`-c`/`-E`/`-S`/`-M*`/syntax-only) # is unchanged from K5/K7 — existing demo builds (always `-c`, then a # separate wasmld) are unaffected. # # Startfiles/libc resolve from the sysroot's build/ by default; the # wackywasm/ bash build overrides via env to the musl-build tree. # ============================================================ # ============================================================================ # THE PLAIN ARM: -fno-hwjs-coro / BRINTIUM_NO_CORO=1 # # Compile and link an ordinary wasm32 module with NO coroutinize pass. The # result is straight WebAssembly: no frame arena, no safepoints, no drive/park # protocol, no lane table -- and correspondingly no fork(), no preemption, and # no in-wasm suspension of any kind. # # WHY THE TOOLCHAIN OFFERS THIS AT ALL. The coro arm exists to make a blocking # syscall, fork() and preemption possible inside wasm, and it charges for that # on every call: a coroutinized function allocates a frame, returns a handle, # and is re-entered through a ramp. A consumer that does not need any of it -- # a library the host drives one call at a time, whose blocking is done for it # by the host on the OTHER side of the syscall import -- pays the whole tax for # none of the benefit. Measured on brintos-fs's bfs_util TU set: 208 KB # coroutinized against 45 KB plain, for byte-identical behaviour. # # WHAT YOU GIVE UP, STATED PLAINLY. Without the pass, the weak # __hwjs_user_park leaf resolves to an `unreachable` stub -- lld synthesizes it # silently, with no warning and a clean exit. So a plain image TRAPS the first # time anything parks, and the ONE protocol that parks is a host answering a # musl cancellation point with -ERESTARTRESCHED (-517). A host driving a plain # image MUST therefore answer every syscall synchronously, blocking on its own # side (Atomics.wait) rather than asking the guest to suspend. That is a real # obligation and it is not checkable from here, which is why it is written # down here: choosing this arm is choosing that host contract. # # THREADS. --export-table is passed because a host that implements SYS_clone # needs it: musl's wasm32 __clone only ISSUES the syscall, handing the host a # {fn, arg} pair (musl/src/thread/wasm32/clone.c), and fn is a function-table # index. On the coro arm a kernel reads that pair out of a per-thread arg # region and the new Worker's __wasm32_user_entry dispatches it; a plain image # has no kernel, so its host calls table.get(fn)(arg) itself after setting the # new thread's __stack_pointer and __tls_base through the exported accessors. # Exporting the table is what makes that possible, and it costs nothing when # nobody spawns a thread. # # WHAT IS UNCHANGED. Compilation still emits bitcode, and the link is still # whole-program LTO over the libc pool -- "plain" names the absent PASS, not # absent optimization. The image still imports its memory as env.user_memory # and still gets every OS service through linux.syscall; only the two coro # imports (hwjs_suspend.park and the RT's frame ops) disappear. # ============================================================================ NOCORO="${BRINTIUM_NO_CORO:-0}" _nc_args=() for _a in "$@"; do case "$_a" in -fno-hwjs-coro) NOCORO=1 ;; -fhwjs-coro) NOCORO=0 ;; *) _nc_args+=( "$_a" ) ;; esac done if [[ "$NOCORO" == "1" ]]; then set -- "${_nc_args[@]}" # The two arms are mutually exclusive, and silently preferring one would # hand the caller an artifact whose shape does not match what they asked # for -- the failure would surface as a trap at first park, far from here. if [[ "${WASMLD_HWJS:-0}" == "1" && -n "${BRINTIUM_NO_CORO:-}${_NC_FLAG:-}" ]]; then : # WASMLD_HWJS=1 is the ambient default in most rigs; the explicit # per-invocation flag wins over it deliberately and quietly. fi HWJS=0 _nc_is_link=1 _nc_has_asm=0 for _a in "$@"; do case "$_a" in -c|-E|-S|-M|-MM|-fsyntax-only|--version|-v|-dumpversion|-dumpmachine|-print-*) _nc_is_link=0 ;; *.s|*.S) _nc_has_asm=1 ;; esac done if [[ "$_nc_is_link" == "0" ]]; then # Compile: bitcode, exactly as the coro arm emits, so the link below is # one LTO unit with the libc pool. `.S` has no IR form. _nc_emit=() [[ "$_nc_has_asm" == "0" ]] && for _a in "$@"; do [[ "$_a" == "-c" ]] && _nc_emit=( -emit-llvm ) done exec "$HWJS_LLVM_BIN/clang" "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" "${ISYSTEM_ARGS[@]}" "${_nc_emit[@]}" "$@" fi # ---- the plain link ---------------------------------------------------- _nc_musl="$TOOLS_DIR/musl-src" _nc_libc="$TOOLS_DIR/build/musl-1.2.5-build/lib/libc.a" _nc_wthr="${WACKY_HWJS_WTHR:-$TOOLS_DIR/build/wasm_thread.k7hwjs.o}" for _f in "$_nc_libc" "$_nc_wthr"; do [[ -f "$_f" ]] || { echo "wasmcc: FATAL (-fno-hwjs-coro): missing link input '$_f' — build the sysroot (make -C sysroot)." >&2; exit 1; } done _nc_work="$(mktemp -d "${TMPDIR:-/tmp}/brintcc-plain.XXXXXX")" trap 'rm -rf "$_nc_work"' EXIT # MAIN-ARITY BRIDGE. musl's crt1 calls __libc_start_main(main, ...), whose # `main` parameter is int(*)(int,char**,char**) -- three arguments. clang # emits a `main` matching what the SOURCE declared, so `int main(void)` and # `int main(int,char**)` produce a two-argument `main` plus a __main_void / # __main_argc_argv body. wasm's calling convention is strictly typed, so # calling either through crt1's three-argument pointer is not a coercion, # it is a trap -- "null function or function signature mismatch", raised # inside libc_start_main_stage2 with nothing in the backtrace naming the # cause. The coro arm never meets this because the pass rewrites every call # into its own drive thunk. Here we bridge it, the same way brintld already # bridges the BusyBox two-argument shape. _nc_objs=() for _a in "$@"; do case "$_a" in *.o|*.bc|*.a) [[ -f "$_a" ]] && _nc_objs+=( "$_a" ) ;; esac done _nc_syms="" [[ ${#_nc_objs[@]} -gt 0 ]] && _nc_syms="$("$HWJS_LLVM_BIN/llvm-nm" --defined-only "${_nc_objs[@]}" 2>/dev/null | awk '{print $NF}' | sort -u)" _nc_bridge=() if grep -qx '__main_void' <<<"$_nc_syms"; then cat > "$_nc_work/mainbridge.c" <<'BRIDGE_EOF' /* `int main(void)` -- clang kept the body as __main_void and emitted a * two-argument `main`. crt1 needs a three-argument one. */ extern int __main_void(void); int __brintium_main3(int argc, char **argv, char **envp) { (void)argc; (void)argv; (void)envp; return __main_void(); } BRIDGE_EOF elif grep -qx '__main_argc_argv' <<<"$_nc_syms"; then cat > "$_nc_work/mainbridge.c" <<'BRIDGE_EOF' /* `int main(int, char **)` -- clang kept the body as __main_argc_argv. */ extern int __main_argc_argv(int argc, char **argv); int __brintium_main3(int argc, char **argv, char **envp) { (void)envp; return __main_argc_argv(argc, argv); } BRIDGE_EOF fi # NOTE ON THE .o EXTENSIONS BELOW: these files hold BITCODE. wasm-ld # partitions its inputs by magic rather than by name, so bitcode named .o # joins the LTO unit correctly -- while a .bc on a clang link line would be # pre-compiled to a native object first, splitting it out of that unit. The # visible symptom of getting this wrong is a `_fini` signature mismatch that # traps during exit(), long after main() has already succeeded. if [[ -f "$_nc_work/mainbridge.c" ]]; then if ! "$HWJS_LLVM_BIN/clang" "$COMPILE_TARGET" "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" -O2 -emit-llvm -c "$_nc_work/mainbridge.c" \ -o "$_nc_work/mainbridge.o" 2>"$_nc_work/mb.err"; then cat "$_nc_work/mb.err" >&2 echo "wasmcc: FATAL (-fno-hwjs-coro): could not compile the main-arity bridge." >&2 exit 1 fi _nc_bridge=( "$_nc_work/mainbridge.o" ) # crt1 is made to CALL the bridge, rather than the bridge being aliased # over `main`: clang has already emitted its own two-argument `main` # alongside __main_void, so a --defsym would collide with a real # definition. Renaming crt1's reference leaves clang's `main` unused and # --gc-sections drops it. _nc_crt_def=( -Dmain=__brintium_main3 ) echo "wasmcc: NOTE (-fno-hwjs-coro): bridging main to crt1's three-argument entry." >&2 fi # crt1, compiled here rather than staged: the sysroot builds only the coro # crt variants (crt1_musl_hwjs*.bc), and crt1_musl.c's own #ifndef # WASM32_HWJS_CORO arm is exactly this one. One small TU per link. if ! "$HWJS_LLVM_BIN/clang" "$COMPILE_TARGET" "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" -I"$_nc_musl/src/include" -O2 \ ${_nc_crt_def[@]+"${_nc_crt_def[@]}"} -emit-llvm -c \ "$_nc_musl/arch/wasm32/crt1_musl.c" -o "$_nc_work/crt1.o" 2>"$_nc_work/crt1.err"; then cat "$_nc_work/crt1.err" >&2 echo "wasmcc: FATAL (-fno-hwjs-coro): could not compile the plain crt1 from '$_nc_musl/arch/wasm32/crt1_musl.c'." >&2 exit 1 fi # BUILTINS_ARG is resolved further down this file, after the point the # plain arm exits from, so the compiler-rt archive is named here instead of # read from a variable that is still unset. _nc_builtins=() _nc_bi="${WACKY_BUILTINS:-$TOOLS_DIR/build/libclang_rt.builtins-wasm32.a}" [[ -f "$_nc_bi" ]] && _nc_builtins=( "$_nc_bi" ) echo "wasmcc: NOTE (-fno-hwjs-coro): PLAIN wasm32 link — no coroutinize pass, no RT, no park. The host must answer every syscall synchronously (a -517 would trap)." >&2 hwjs_link "$CLANG" "$HWJS_LLVM_BIN/wasm-ld" \ "$LINK_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ -Wl,--no-entry \ -Wl,--export=_start \ -Wl,--export-if-defined=__wasm_call_ctors \ -Wl,--export=__heap_base -Wl,--export=__data_end \ -Wl,--export-if-defined=__wasm32_user_entry \ -Wl,--export-if-defined=__wasm_init_tls \ -Wl,--export-if-defined=__wasm32_get_tls_size \ -Wl,--export-if-defined=__wasm32_get_tls_align \ -Wl,--export-table \ -Wl,--export-if-defined=__wasm32_set_tls_base \ -Wl,--export-if-defined=__wasm32_set_stack_pointer \ -Xlinker --import-memory=env,user_memory \ -Wl,--shared-memory \ -Wl,--initial-memory=$(( ${BRINTIUM_INIT_PAGES:-130} * 65536 )) \ -Wl,--max-memory=$(( ${BRINTIUM_MAX_PAGES:-16384} * 65536 )) \ -Xlinker -z -Xlinker stack-size=$(( ${BRINTIUM_STACK_SIZE:-1048576} )) \ -Wl,--gc-sections \ -Wl,--import-undefined \ -Wl,--error-limit=0 \ "$_nc_work/crt1.o" \ ${_nc_bridge[@]+"${_nc_bridge[@]}"} \ "$@" \ "$_nc_wthr" \ "$_nc_libc" \ ${_nc_builtins[@]+"${_nc_builtins[@]}"} exit $? fi WACKY_CRT1="${WACKY_CRT1:-$TOOLS_DIR/build/crt1_musl.o}" WACKY_WTHR="${WACKY_WTHR:-$TOOLS_DIR/build/wasm_thread.k7.o}" WACKY_LIBC_DIR="${WACKY_LIBC_DIR:-$TOOLS_DIR/build/musl-1.2.5-build/lib}" # Coro-arm startfiles (HWJS-3): bitcode crt + runtime TU (merged by wasmld) # + the native-stage resume/done/destroy thunks (post-merge by design — # the calls must stay opaque to CoroElide; see hwjs_user_thunks.ll). # # FLAT-DRIVE crt selection (HWJS-3 close: FLAT is the CANONICAL default). The # FLAT crt (crt1_musl_hwjs_flat.bc) whose _start runs the in-wasm flat driver is # injected BY DEFAULT (matching wasmld's default flat codegen); opt back to the # nested crt (crt1_musl_hwjs.bc, which leaves the parked set for the host's # drive_abi=2 dispatcher) with HWJS_CORO_DRIVE_NESTED=1 (or HWJS_CORO_DRIVE_FLAT=0). # This decision is shared VERBATIM with wasmld — keep them in sync (a flat crt # under nested codegen, or vice versa, is a broken image wasmld's DRIVE-CONTRACT # guard refuses). An explicit WACKY_CRT1_HWJS override still wins. (There is no # clean C-level runtime flat signal — a C ref to the plugin's __hwjs_drive_abi # sentinel suppresses its emission — so the crt is chosen at build time here.) # VALUE-keyed drive knobs (issue linux-in-the-browser#41): 1 (on), 0 (off), or unset/empty # (default) — anything else is a loud refusal (kept in sync with wasmld and the pass's # envKnobBool; a garbage value must never silently pick a crt). for _dk in HWJS_CORO_DRIVE_FLAT HWJS_CORO_DRIVE_NESTED; do case "${!_dk:-}" in ''|0|1) ;; *) echo "wasmcc: FATAL: $_dk='${!_dk}' — this knob is VALUE-keyed (1 = on, 0 = off, unset/empty = default); refusing to guess (linux-in-the-browser#41)." >&2 exit 1 ;; esac done if [[ "${HWJS_CORO_DRIVE_NESTED:-0}" != "0" ]]; then HWJS_DRIVE_FLAT=0 else HWJS_DRIVE_FLAT="${HWJS_CORO_DRIVE_FLAT:-1}" # default 1 = flat fi # STATUS-CHANNEL crt selection (drive_abi=4; STATUS-CHANNEL-CONTRACT §10 crt # bullet): the SAME value-keyed knob that selects abi-4 codegen in the pass # selects the §4 terminus crt here — knob and crt can never skew. The knob # contradictions (status + nested, status + flat=0) refuse in the pass and in # wasmld; refusing HERE too keeps the earliest vantage loud (a nested crt # under abi-4 codegen is a broken image whose termini never run). case "${HWJS_STATUS_CHANNEL:-}" in ''|0|1) ;; *) echo "wasmcc: FATAL: HWJS_STATUS_CHANNEL='${HWJS_STATUS_CHANNEL}' — this knob is VALUE-keyed (1 = on, 0 = off, unset/empty = default); refusing to guess (linux-in-the-browser#41)." >&2 exit 1 ;; esac if [[ "${HWJS_STATUS_CHANNEL:-0}" == "1" && "$HWJS_DRIVE_FLAT" == "0" ]]; then echo "wasmcc: FATAL: HWJS_STATUS_CHANNEL=1 with the nested drive selected (HWJS_CORO_DRIVE_NESTED=1 / HWJS_CORO_DRIVE_FLAT=0) — the status channel is a flat-substrate contract (STATUS-CHANNEL-CONTRACT §10 refuses this pair in the pass; refusing at the crt-selection vantage too so a broken crt/codegen pair can never link)." >&2 exit 1 fi # LANE-AWARE CHANNEL ADDRESSING (PLAN-THREADS-SINGLE-MEMORY §4 step 2): the # HWJS_RT_LANES knob (lane COUNT; unset/0/1 = single-lane, byte-identical) # flows through wasmld into the merged pass run. Refusals at the crt/RT # selection vantage: (a) garbage values / lanes>1 without the status channel # (mirrors the pass + wasmld); (b) lanes>1 while injecting an INSTALLED # startfiles runtime that is NOT actually laned — the kernel packaging ships # build/hwjs_user_rt.bc laned by default now (scripts/build-hwjs-rt.sh # HWJS_RT_LANES_BUILD, §4 step 4), but a single-lane rebuild would pair # pass-emitted lane-global checks with an RT whose channel array has ONE # element — every lane>=1 address aliases other RT data silently, the exact # transit class the manifest bit exists to refuse; the posture is verified # against the artifact's own symbol surface at the link vantage below. The # crt itself needs no laned twin (it is BITCODE compiled in the merged pass # run and inherits lane addressing there). An explicit WACKY_HWJS_RT # override (a caller-built WIP RT, the gate world) is the sanctioned escape # — noted loudly by the RT-PROVENANCE override path below. case "${HWJS_RT_LANES:-}" in ''|0|1) ;; *) if ! [[ "${HWJS_RT_LANES}" =~ ^[0-9]+$ && "${HWJS_RT_LANES}" -ge 2 && "${HWJS_RT_LANES}" -le 32 ]]; then echo "wasmcc: FATAL: HWJS_RT_LANES='${HWJS_RT_LANES}' — this knob is the lane COUNT: an integer 0..32 (unset/0/1 = single-lane; 2..32 = lane-aware channel addressing). Refusing to guess (PLAN-THREADS-SINGLE-MEMORY §4 step 2)." >&2 exit 1 fi if [[ "${HWJS_STATUS_CHANNEL:-0}" != "1" ]]; then echo "wasmcc: FATAL: HWJS_RT_LANES>1 without HWJS_STATUS_CHANNEL=1 — lane-aware addressing is a property of the status channel (drive_abi=4). Set both or neither (PLAN-THREADS-SINGLE-MEMORY §4 step 2)." >&2 exit 1 fi # Refusal (b) is LINK-vantage-only (a -c compile injects no RT): # recorded here, enforced below once is_link_mode is known. HWJS_LANES_LINK_GUARD=1 ;; esac if [[ -n "${WACKY_CRT1_HWJS:-}" ]]; then echo "wasmcc: NOTE (CRT-PROVENANCE): explicit WACKY_CRT1_HWJS override ($WACKY_CRT1_HWJS) — WIP crt, provenance UNVERIFIED" >&2 elif [[ "${HWJS_STATUS_CHANNEL:-0}" == "1" ]]; then WACKY_CRT1_HWJS="$TOOLS_DIR/build/crt1_musl_hwjs_status.bc" elif [[ "$HWJS_DRIVE_FLAT" != "0" ]]; then WACKY_CRT1_HWJS="$TOOLS_DIR/build/crt1_musl_hwjs_flat.bc" else WACKY_CRT1_HWJS="$TOOLS_DIR/build/crt1_musl_hwjs.bc" fi # CRT-PROVENANCE (2026-07-21, the stale-terminus gap): the RT had an # attestation gate; the crt did not — a crt1_musl_hwjs*.bc left behind by # an older musl tree linked SILENTLY, and a stale §4 terminus misterminates # every program built with it (the post-link DRIVE-CONTRACT check catches a # stale plugin, never a stale crt). The producer (the sysroot build's crt # rules) writes a sidecar attesting the crt SOURCE's sha256; verify it # against the checked-out musl tree whenever that tree is present. Explicit # WACKY_CRT1_HWJS overrides skip this (noted loudly above), mirroring the # WACKY_HWJS_RT contract. Called at the LINK vantage only — the crt is # injected only there, and the crt's OWN compile (`wasmcc -c crt1_musl.c`) # must not be blocked by the absence of the stamp it is about to produce. # # THE SOURCE IS RESOLVED FROM THIS CHECKOUT, not from a path relative to the # sysroot. It used to be spelled as a walk up out of the sysroot into a # super-repository's package directory, which stopped naming anything when the # sysroot became a directory of this repository — and because a source that is # not there means "cannot check", the whole gate went quiet: every link # injected an unverified crt and said nothing. musl is a submodule here, so # the check is live again; BRINTIUM_MUSL_SRC moves it for a checkout that # builds its libc somewhere else. crt_provenance_verify() { [[ "$WACKY_CRT1_HWJS" == "$TOOLS_DIR/build/"crt1_musl_hwjs*.bc ]] || return 0 local musl_src="${BRINTIUM_MUSL_SRC:-$HWJS_HOME/musl}" local crt_src="$musl_src/arch/wasm32/crt1_musl.c" if [[ ! -f "$crt_src" ]]; then # Not silent, and not fatal either: a checkout used only as a compiler, # against a sysroot someone else built, legitimately has no libc # sources. Say which check did not run and what would make it run, # rather than letting an unverified crt look like a verified one. echo "wasmcc: NOTE (CRT-PROVENANCE): NOT CHECKED — no musl crt source at $crt_src, so the installed crt cannot be tied to any source. Point BRINTIUM_MUSL_SRC at the musl checkout, or run 'git submodule update --init musl'." >&2 return 0 fi local crt_stamp="$WACKY_CRT1_HWJS.stamp" if [[ ! -f "$crt_stamp" ]]; then echo "wasmcc: FATAL (CRT-PROVENANCE): no attestation sidecar at $crt_stamp — the installed crt cannot be tied to the checked-out musl crt source. Rebuild: 'make -C sysroot hwjs-startfiles'." >&2 exit 1 fi local crt_key crt_line crt_key="$(sha256sum "$crt_src" | cut -d' ' -f1)" crt_line="$(cat "$crt_stamp")" if [[ "$crt_line" != "CRT-PROVENANCE src=$crt_key" ]]; then echo "wasmcc: FATAL (CRT-PROVENANCE): the INSTALLED crt is STALE: $crt_stamp attests '$crt_line' but the checked-out crt source ($crt_src) keys to src=$crt_key — rebuild via 'make -C sysroot hwjs-startfiles'. Refusing at the injection vantage." >&2 exit 1 fi } # PLAN-HWJS-RT-PSABI R3: the runtime TU is KERNEL-owned and # provenance-attested (RT-PROVENANCE sidecar written by the kernel's # build-hwjs-rt.sh, installed beside the .bc by the sysroot build). The # DEFAULT-path artifact must attest the checked-out linux submodule's # RT-INPUT srckey with dirty=0 — NARROWED KEY (2026-07-21): the key is # computed by the ONE shared definition, linux scripts/hwjs-rt-srckey.sh # (object ids of arch/wasm32/vdso + the build/splice/key scripts, dirty # bit pathspec-limited to the same), so a docs-only linux commit no # longer refuses here while any real RT-input change still does. A # stale/dirty runtime is refused at the injection vantage # (never-silent). An EXPLICIT WACKY_HWJS_RT override is the WIP-runtime # dev knob (the WASMLD_HWJS_SRC_DIR pattern): honored verbatim, with a # loud NOTE that provenance is unverified. hwjs_rt_verify() { local rt="$1" stamp="$1.stamp" # WHERE THE KERNEL CHECKOUT IS: TAKEN, NEVER COMPUTED. Re-keying the # attestation means running the kernel's own key script over the kernel's # own sources, so this needs a kernel checkout — which this toolchain does # not contain and cannot produce. This line used to fall back to a walk up # out of the sysroot into a package directory of the repository this # toolchain was a submodule of. Standalone, that walk names nothing, and # every refusal below then quoted a path nobody had chosen and sent the # reader looking for a kernel checkout at it. The pointer is the whole # answer now; unset is its own refusal, one situation per message. local linux_src="${BRINTIUM_KERNEL_SRC:-}" [[ -n "$linux_src" ]] || { echo "wasmcc: FATAL (RT-PROVENANCE): BRINTIUM_KERNEL_SRC is not set, and this link injects the kernel-owned runtime $rt, whose attestation can only be re-keyed by running the kernel's own scripts/hwjs-rt-srckey.sh over the kernel's own sources. This toolchain has no kernel of its own and there is nothing here to guess from. Set BRINTIUM_KERNEL_SRC to the kernel checkout that produced this runtime — the repository that produces the runtime also publishes the pointer, so a build driven from there should be exporting it — or set WACKY_HWJS_RT to take the artifact verbatim, which is honoured and says on every link that its provenance is unverified." >&2 exit 1 } [[ -f "$stamp" ]] || { echo "wasmcc: FATAL (RT-PROVENANCE): no attestation sidecar at $stamp — the kernel owns hwjs_user_rt and attests it at the kernel link; rebuild it there (or override WACKY_HWJS_RT explicitly for a work-in-progress runtime)" >&2 exit 1 } local line; line="$(cat "$stamp")" local keyscript="$linux_src/scripts/hwjs-rt-srckey.sh" [[ -d "$linux_src" ]] || { echo "wasmcc: FATAL (RT-PROVENANCE): BRINTIUM_KERNEL_SRC='$linux_src' is not a directory, so the attestation beside $rt cannot be re-keyed against the kernel it claims to come from. This toolchain has no kernel of its own; deciding whether that sidecar still describes the current one is a question only a kernel checkout can answer. Point BRINTIUM_KERNEL_SRC at the kernel checkout that produced this runtime, or set WACKY_HWJS_RT to take the artifact verbatim — which is honoured, and says on every link that its provenance is unverified." >&2 exit 1 } [[ -x "$keyscript" ]] || { echo "wasmcc: FATAL (RT-PROVENANCE): BRINTIUM_KERNEL_SRC='$linux_src' ships no executable scripts/hwjs-rt-srckey.sh, which is the one definition of the key this sidecar is written against. Either it is not a kernel checkout, or it sits on a commit that predates the script; move it to one that carries it." >&2 exit 1 } local out; out="$("$keyscript" "$linux_src")" || { echo "wasmcc: FATAL (RT-PROVENANCE): hwjs-rt-srckey.sh failed for $linux_src — cannot verify $stamp" >&2 exit 1 } local key="${out%% *}" dirt="${out##* }" [[ "$dirt" == "0" ]] || { echo "wasmcc: FATAL (RT-PROVENANCE): the LINUX CHECKOUT carries uncommitted edits to the RT inputs (git -C $linux_src status -- arch/wasm32/vdso scripts/build-hwjs-rt.sh scripts/wasm-add-custom-section.py scripts/hwjs-rt-srckey.sh) — no attestation can cover uncommitted inputs; commit or revert, then 'make kernel FORCE=1'. (Pathspec-limited: edits elsewhere in the linux tree do NOT trip this.)" >&2 exit 1 } if [[ ! "$line" =~ ^RT-PROVENANCE\ commit=[0-9a-f]{40}\ dirty=0\ src=[0-9a-f]{64}\ srckey=$key$ ]]; then if [[ "$line" =~ \ srckey=[0-9a-f]{64}$ ]]; then echo "wasmcc: FATAL (RT-PROVENANCE): the INSTALLED RUNTIME ARTIFACT is STALE (or attests dirty RT inputs): $stamp attests '$line' but the checked-out linux RT inputs key to srckey=$key — re-attest via 'make kernel FORCE=1' then 'make -C sysroot hwjs-startfiles'. Refusing at the injection vantage." >&2 else echo "wasmcc: FATAL (RT-PROVENANCE): the INSTALLED RUNTIME ARTIFACT predates the narrowed RT-provenance key (no srckey= field): $stamp attests '$line' — the PRODUCER side is stale; rebuild via 'make kernel FORCE=1' then 'make -C sysroot hwjs-startfiles'. Refusing at the injection vantage." >&2 fi exit 1 fi } if [[ -n "${WACKY_HWJS_RT:-}" ]]; then echo "wasmcc: NOTE (RT-PROVENANCE): explicit WACKY_HWJS_RT override ($WACKY_HWJS_RT) — WIP runtime, provenance UNVERIFIED" >&2 else WACKY_HWJS_RT="$TOOLS_DIR/build/hwjs_user_rt.bc" WACKY_HWJS_RT_VERIFY=1 fi WACKY_HWJS_THUNKS="${WACKY_HWJS_THUNKS:-$TOOLS_DIR/build/hwjs_user_thunks.o}" # F88 §8b seam (HWJS-3): __wasm32_user_entry lives in BITCODE on the coro # arm (hwjs_user_entry.bc, joins the merge) and wasm_thread is assembled # WITHOUT the native entry (wasm_thread.k7hwjs.o, -DHWJS_USER_ENTRY_IN_BITCODE) # — a native `call _start`/call_indirect against the post-F88 coroutinized # _start is the §8b hole wasmld refuses. The Asyncify WACKY_WTHR # (wasm_thread.k7.o) carries that native entry and MUST NOT be used here. WACKY_HWJS_ENTRY="${WACKY_HWJS_ENTRY:-$TOOLS_DIR/build/hwjs_user_entry.bc}" WACKY_HWJS_WTHR="${WACKY_HWJS_WTHR:-$TOOLS_DIR/build/wasm_thread.k7hwjs.o}" # D1-BASH.b: compiler-rt soft-float/int builtins (__addtf3 .. __floatunsitf, # __multi3). wasm32 long double is IEEE binary128 (LDBL_MANT_DIG=113), so any # long-double FP path (bash's printf) pulls these; musl libc.a leaves them # UNDEFINED (compiler-rt's job). Built from the toolchains-LLVM compiler-rt # source (make -C sysroot builtins). Linked AFTER -lc so it satisfies the # undefined refs musl objects bring in. Self-contained (no libc deps), so no # --start-group needed. Absent => harmless for binaries that never touch # long double (the K7 demos); required for bash. WACKY_BUILTINS="${WACKY_BUILTINS:-$TOOLS_DIR/build/libclang_rt.builtins-wasm32.a}" BUILTINS_ARG=() [[ -f "$WACKY_BUILTINS" ]] && BUILTINS_ARG=( "$WACKY_BUILTINS" ) is_link_mode=1 has_asm_input=0 is_reloc=0 for a in "$@"; do case "$a" in -c|-E|-S|-M|-MM|-fsyntax-only|--version|-v|-dumpversion|-dumpmachine|-print-*) is_link_mode=0 ;; -r|--relocatable) is_reloc=1 ;; *.s|*.S) has_asm_input=1 ;; esac done # ── RELOCATABLE LINK (-r) — ticket #7. A partial link combines objects for a # LATER link line; it must carry NO startfiles, NO libc, and none of the # final-image link flags (wasm-ld rejects -r alongside --gc-sections/ # --shared-memory, and an injected crt would duplicate _start at the real # link). Route the object list straight to wasmld, whose -r path merges # bitcode inputs with llvm-link (whole-TU IR still reaches the coroutinize # pass at the FINAL link) or native inputs with a bare `wasm-ld -r`. # Sources on a -r line are refused loudly: compile first, then combine. # Lanes>1 link posture check (PLAN-THREADS-SINGLE-MEMORY §4 step 4): the # kernel packaging now ships the DEFAULT hwjs_user_rt.bc laned # (build-hwjs-rt.sh HWJS_RT_LANES_BUILD, default 8), so a lane-stamped link # over the installed RT is the SUPPORTED path — but only if the installed # artifact really is laned. A single-lane rebuild (HWJS_RT_LANES_BUILD=1) # under a lanes>1 link would pair lane-global checks with a one-element # channel array: every lane>=1 address silently aliases other RT data # (I3-2). Verify the posture by the artifact's own symbol surface (the # laned RT DEFINES __hwjs_status_channel_lanes; the single-lane RT has no # such symbol). The previous spelling of this guard keyed on an unset # WACKY_HWJS_RT and had become vacuous (the installed default is assigned # above before this line) — this check fires on the artifact itself. # An explicit WACKY_HWJS_RT override is exempt (the WIP-runtime escape, # noted loudly by the RT-PROVENANCE path above). # # SIGPIPE DISCIPLINE — this guard is NOT a pipe (the rule wasmld already # learned at its merged-symbol gates, libexec/brintld:1781 and the F163 REFUSE # gate at :2309). `nm | grep -q` under `set -o pipefail` is a FALSE-FAILURE # generator: `grep -q` exits the instant it matches and closes the pipe, # llvm-nm — whose defined-symbol table for a real runtime is far larger than # one pipe buffer — then dies of SIGPIPE (141), pipefail promotes 141 to the # pipeline's status, and the leading `!` turns a SUCCESSFUL match into this # FATAL. It is intermittent BY CONSTRUCTION (it fires only when nm has not # already finished writing when grep exits) and it has produced at least one # bogus SINGLE-LANE verdict. Capture the symbol table into a string first, # then match the string. # # Second defect fixed here: the old form sent llvm-nm's own diagnostics to # /dev/null and ignored its exit status, so an ABSENT or UNREADABLE runtime # produced an empty symbol list and was reported as a SINGLE-LANE artifact — # a misdiagnosis with a misleading remedy attached. Those are now distinct # verdicts. if [[ "$is_link_mode" == "1" && "${HWJS_LANES_LINK_GUARD:-0}" == "1" && "${WACKY_HWJS_RT_VERIFY:-0}" == "1" ]]; then _rt_nm_rc=0 # stderr folded in only so a failure can be REPORTED; on success llvm-nm # writes nothing there, and the match pattern is anchored to a symbol line. _rt_syms="$("$HWJS_LLVM_BIN/llvm-nm" --defined-only "$WACKY_HWJS_RT" 2>&1)" || _rt_nm_rc=$? if [[ "$_rt_nm_rc" != "0" ]]; then echo "wasmcc: FATAL: HWJS_RT_LANES=${HWJS_RT_LANES} lane-posture check could NOT READ the installed runtime ($WACKY_HWJS_RT) — llvm-nm exited $_rt_nm_rc: ${_rt_syms//$'\n'/ | }. This is not a lane verdict: the artifact is missing or unreadable. Rebuild the kernel packaging ('make kernel FORCE=1', then 'make -C sysroot hwjs-startfiles')." >&2 exit 1 fi if ! grep -q ' __hwjs_status_channel_lanes$' <<<"$_rt_syms"; then echo "wasmcc: FATAL: HWJS_RT_LANES=${HWJS_RT_LANES} on a LINK injecting the INSTALLED runtime ($WACKY_HWJS_RT), but that artifact is SINGLE-LANE (it defines no __hwjs_status_channel_lanes array) — lane-global checks over a one-element channel array alias other RT data silently for every lane>=1 (I3-2). Rebuild the kernel packaging laned ('make kernel FORCE=1' with the default HWJS_RT_LANES_BUILD, then 'make -C sysroot hwjs-startfiles'), provide a caller-built laned runtime via WACKY_HWJS_RT, or drop the knob." >&2 exit 1 fi unset _rt_syms _rt_nm_rc fi if [[ "$is_link_mode" == "1" && "$is_reloc" == "1" ]]; then for a in "$@"; do case "$a" in *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) echo "wasmcc: FATAL: source file '$a' on a relocatable (-r) link line — compile it first (-c), then combine the objects. (A gcc-style compile+partial-link in one step is not supported.)" >&2 exit 1 ;; esac done exec "$WASMLD" "$@" fi if [[ "$is_link_mode" == "1" ]]; then crt_provenance_verify if [[ "${WASMLD_HWJS_CONFTEST:-0}" != "0" ]]; then # ── FAST CONFIGURE PATH (WASMLD_HWJS_CONFTEST=1). autoconf link probes # only check that a link SUCCEEDS (the wasm is never run — cross-compile: # AC_RUN_IFELSE cannot execute and autoconf itself forces the site-cache # seed path, so exit-status-only IS the whole contract), so a plain # wasm-ld link skips the whole-program coroutinize merge + pass entirely # (~7s per conftest -> instant). NEVER set for a real build. # # HONEST FUNCTION PROBES (ticket #4). This path links with # --allow-undefined and NO libc, so every undefined symbol — real # musl function or pure fiction — became an env.* import and the # link "succeeded": AC_CHECK_FUNCS answered yes for EVERYTHING and # packages skipped their fallbacks (the bash termcap hole). The fix # keeps the fast link but attests the probe image afterward with the # PHANTOM-IMPORT witness against an allowlist of every symbol the # REAL link would resolve (the musl bitcode pool + coro startfiles + # native thunks/wthr + compiler-rt builtins), cached next to the # pool and rebuilt whenever the pool is newer. A probe calling a # real libc function still passes (the real link resolves it); a # probe calling a NONEXISTENT function now fails the link — so # configure's yes/no matches what a real build can actually provide. # # ARCHIVE LINK-PROBES (linux-in-the-browser#63). AC_CHECK_LIB / # AC_LINK_IFELSE against a large STATIC archive (curl's dozens of # openssl probes: `conftest.c -lssl -lcrypto -lz` against a multi-MB # BITCODE libcrypto.a) used to ride the archives straight into this # fast link — plain wasm-ld then ran full LTO codegen over every # pulled member, ~30s PER PROBE (and once configure grows # LIBS=-lssl -lcrypto, EVERY later probe pays it): configure still # took O(hour). The archives contribute NOTHING to the probe's # yes/no that their symbol table doesn't: take each resolvable # archive OFF the fast link line entirely and extend the witness # allowlist with the archive's EXTERNALLY-DEFINED symbols instead # (llvm-nm, cached per archive keyed on mtime+size — dozens of # probes pay nm once). Honesty is preserved in both directions: # - a probe referencing a symbol the archive really defines links # (import allowlisted — the real link resolves it from the # archive); # - a probe referencing a symbol NOTHING provides still fails the # witness (honest "no"); # - `-lNAME` that resolves to NO archive on the -L path stays on # the line and fails the link (honest "no" for AC_CHECK_LIB of # a nonexistent library); # - archive-internal locals ('t'/'d') are NOT allowlisted — a # probe naming a static-only symbol fails, exactly like the # real link. # The coroutinize pass now NEVER runs for a conftest link; it only # ever runs for real links (where WASMLD_HWJS_CONFTEST must be 0). # # RED (gate-only): WASMLD_HWJS_CONFTEST_ARCHIVE_RIDE=1 reproduces the # pre-#63 behavior (archives ride into the fast link, paying the # per-probe archive LTO); it is a fix-disabling knob, so it is refused # unless WASMLD_HWJS_ALLOW_RED_KNOBS=1 acknowledges a deliberate # non-vacuity arm (test/conftest-archive-fastpath.sh). if [[ "${WASMLD_HWJS_CONFTEST_ARCHIVE_RIDE:-0}" != "0" && "${WASMLD_HWJS_ALLOW_RED_KNOBS:-0}" != "1" ]]; then echo "wasmcc: FATAL: fix-disabling RED knob WASMLD_HWJS_CONFTEST_ARCHIVE_RIDE=${WASMLD_HWJS_CONFTEST_ARCHIVE_RIDE} is set for a conftest link — archives would ride into the fast link and pay the per-probe archive-LTO cost this knob exists to demonstrate (linux-in-the-browser#63). Unset it, or set WASMLD_HWJS_ALLOW_RED_KNOBS=1 for a deliberate non-vacuity arm." >&2 exit 1 fi conftest_out="" prev="" for a in "$@"; do if [[ "$prev" == "-o" ]]; then conftest_out="$a"; fi case "$a" in -o?*) conftest_out="${a#-o}" ;; esac prev="$a" done # -L search path (same resolution order the coro link arm uses, # minus WACKY_LIBC_DIR: -lc never resolves to an archive here — the # pool allowlist already speaks for libc). cft_Ldirs=() prev="" for a in "$@"; do case "$a" in -L?*) cft_Ldirs+=( "${a#-L}" ) ;; esac if [[ "$prev" == "-L" ]]; then cft_Ldirs+=( "$a" ); fi prev="$a" done # Split every resolvable archive off the link line (positional *.a # and resolvable -lNAME); keep everything else verbatim — an # UNRESOLVABLE -lNAME or missing *.a stays and fails the link # honestly. Deduplicate (configure lines repeat `-lssl -lcrypto # -lssl -lcrypto`) so each archive is nm'd once. cft_args=() cft_archives=() cft_seen_archives="" cft_note_archive() { local abs abs="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" case "$cft_seen_archives" in *"|$abs|"*) return ;; esac cft_seen_archives="$cft_seen_archives|$abs|" cft_archives+=( "$abs" ) } if [[ "${WASMLD_HWJS_CONFTEST_ARCHIVE_RIDE:-0}" != "0" ]]; then echo "wasmcc: NOTE (WASMLD_HWJS_CONFTEST_ARCHIVE_RIDE, RED arm ACKNOWLEDGED): archives ride into the conftest fast link — the pre-#63 per-probe archive-LTO behavior. Non-production." >&2 cft_args=( "$@" ) else for a in "$@"; do case "$a" in *.a) if [[ -f "$a" ]]; then cft_note_archive "$a"; else cft_args+=( "$a" ); fi ;; -lc) cft_args+=( "$a" ) ;; -l?*) name="${a#-l}" found="" for d in "${cft_Ldirs[@]}"; do if [[ -f "$d/lib${name}.a" ]]; then found="$d/lib${name}.a"; break; fi done if [[ -n "$found" ]]; then cft_note_archive "$found"; else cft_args+=( "$a" ); fi ;; *) cft_args+=( "$a" ) ;; esac done fi # ── THE ALLOWLIST IS BUILT BEFORE THE LINK, because the link now USES it. # # Ticket #4 made FUNCTION probes honest: the fast link imports every undefined function # and the PHANTOM-IMPORT witness afterwards refuses any name no real link could resolve, # so AC_CHECK_FUNCS stopped answering yes for pure fiction. DATA probes were left behind, # and for a structural reason: --allow-undefined binds an undefined DATUM to address 0 and # emits NO import, so the witness — which reads the finished module's import section — is # blind to it. Every autoconf link-probe for a VARIABLE therefore answered YES: # AC_STRUCT_TIMEZONE's tzname, sys_siglist, program_invocation_name, daylight, any # `extern int foo;` a package tests for. The package then compiled a reference to address # 0 into its shipped binary and skipped the fallback it has for exactly this case. That is # the bash/termcap hole again, wearing a variable instead of a function. # # Splitting --allow-undefined into its two halves (lld Driver.cpp:865-870: # --import-undefined + --unresolved-symbols=ignore) fixes it with the allowlist that is # ALREADY here and already derived: # --import-undefined every undefined FUNCTION still imports — the ticket-#4 # flow is bit-for-bit unchanged, witness and all. # --allow-undefined-file= an undefined DATUM links iff the union of the musl pool, # the startfiles, the native objects, the builtins and the # archives on this line actually DEFINES it — i.e. iff the # real link would resolve it. Honest yes. # (anything else) `undefined symbol: ` — the link fails, configure # records no, the package takes its fallback. Honest no. # Not one symbol name is written down: the list is llvm-nm over the providers, rebuilt # whenever a provider is newer, exactly as the function witness has always used it. # # lld honors only the LAST --allow-undefined-file= (Driver.cpp getLastArg), so the pool # list and the per-archive lists are concatenated into one file rather than passed # severally — a second flag would silently shadow the first. HWJS_LIBC_BC="${WASMLD_HWJS_LIBC_BC:-$TOOLS_DIR/build/musl-bc}" allowlist="$HWJS_LIBC_BC/.conftest-resolvable-symbols.txt" # Rebuild the cached allowlist when it's missing or older than any # provider (pool member, startfile, native-stage object, builtins). providers=( "$HWJS_LIBC_BC"/*.bc "$WACKY_CRT1_HWJS" "$WACKY_HWJS_RT" "$WACKY_HWJS_ENTRY" "$WACKY_HWJS_THUNKS" "$WACKY_HWJS_WTHR" ) [[ -f "$WACKY_BUILTINS" ]] && providers+=( "$WACKY_BUILTINS" ) rebuild=0 if [[ ! -f "$allowlist" ]]; then rebuild=1 else for p in "${providers[@]}"; do [[ -f "$p" && "$p" -nt "$allowlist" ]] && { rebuild=1; break; } done fi if [[ "$rebuild" == "1" ]]; then if ! "$HWJS_LLVM_BIN/llvm-nm" --defined-only "${providers[@]}" 2>/dev/null \ | awk '{print $NF}' | sort -u > "$allowlist.tmp.$$"; then rm -f "$allowlist.tmp.$$" echo "wasmcc: FATAL (conftest): llvm-nm over the libc pool failed — cannot build the resolvable-symbol allowlist, so the probe's import surface cannot be attested. Refusing rather than answering configure dishonestly." >&2 exit 1 fi mv -f "$allowlist.tmp.$$" "$allowlist" fi # Per-archive symbol allowlists (linux-in-the-browser#63): each archive # taken off the link line above speaks through its EXTERNALLY-DEFINED # symbol list instead. Cached per archive, keyed on basename + a path # hash + mtime + size (a rebuilt archive gets a fresh key; stale cache # files are inert), so the dozens of probes in one configure run pay # llvm-nm exactly once per archive. Only external definitions # (uppercase nm type) are allowlisted — an archive-local 't' symbol # cannot resolve a probe's reference in a real link, so it must not # here either. An nm failure is a loud refusal, never a silent # empty allowlist (which would flip real symbols to dishonest "no"). cft_witness_allow=( --allow-undefined-from="$allowlist" ) cft_allow_files=( "$allowlist" ) if [[ "${#cft_archives[@]}" -gt 0 ]]; then cft_symcache="${WASMLD_HWJS_CONFTEST_SYMCACHE:-${TMPDIR:-/tmp}/hwjs-conftest-symcache-$(id -u)}" mkdir -p "$cft_symcache" for ar_path in "${cft_archives[@]}"; do ar_stat="$(stat -c '%Y.%s' "$ar_path" 2>/dev/null)" || { echo "wasmcc: FATAL (conftest): cannot stat archive '$ar_path' — its symbol surface cannot be attested. Refusing rather than answering configure dishonestly." >&2 exit 1 } ar_key="$(printf '%s' "$ar_path" | cksum | awk '{print $1}')" ar_syms="$cft_symcache/$(basename "$ar_path").$ar_key.$ar_stat.syms" if [[ ! -f "$ar_syms" ]]; then if ! "$HWJS_LLVM_BIN/llvm-nm" --defined-only "$ar_path" 2>/dev/null \ | awk 'NF >= 2 && $(NF-1) ~ /^[A-Z]$/ { print $NF }' | sort -u > "$ar_syms.tmp.$$"; then rm -f "$ar_syms.tmp.$$" echo "wasmcc: FATAL (conftest): llvm-nm over archive '$ar_path' failed — its symbol surface cannot be attested, so the probe's yes/no would be a guess. Refusing rather than answering configure dishonestly." >&2 exit 1 fi mv -f "$ar_syms.tmp.$$" "$ar_syms" fi cft_witness_allow+=( --allow-undefined-from="$ar_syms" ) cft_allow_files+=( "$ar_syms" ) done fi # The ONE file lld will read (see the note above on getLastArg). cft_undef_allow="$(mktemp "${TMPDIR:-/tmp}/hwjs-conftest-undef.XXXXXX")" trap 'rm -f "$cft_undef_allow"' EXIT cat "${cft_allow_files[@]}" > "$cft_undef_allow" || { echo "wasmcc: FATAL (conftest): cannot assemble the resolvable-symbol list for the link — without it an undefined DATUM would bind to address 0 unnoticed and the probe would answer configure 'yes' for a variable nothing defines. Refusing." >&2 exit 1 } # --export-all keeps the whole probe LIVE: with --no-entry and no # exports wasm-ld garbage-collects everything (main included), so # the probe's undefined references would never surface as imports # and the witness below would see an empty image. Exporting all # defined symbols pins the probe body, so every symbol a real link # would have to resolve survives as an env.* import for the witness # to judge. (The image is never run; only the yes/no matters.) # --error-limit=0: lld stops naming undefined symbols after 20, and a # probe that reports two of its five missing names is a diagnostic that # runs out halfway. hwjs_link "$CLANG" "$HWJS_LLVM_BIN/wasm-ld" \ "$LINK_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ -Wl,--no-entry -Wl,--import-undefined \ -Wl,--allow-undefined-file="$cft_undef_allow" \ -Wl,--error-limit=0 -Wl,--export-all \ "${cft_args[@]}" || exit $? # OUT-less probes (e.g. `-shared` feature checks) have no image to # attest; the link answer above is all configure asked for. [[ -n "$conftest_out" && -f "$conftest_out" ]] || exit 0 # An installed toolchain's own witness; see libexec/brintld. if [[ -x "$HWJS_HOME/libexec/brint-tool" ]]; then IMPORT_WITNESS=( "$HWJS_HOME/libexec/brint-tool" import-witness ) else IMPORT_WITNESS=( node "$HWJS_HOME/libexec/wasm-import-witness.mjs" ) if [[ ! -f "${IMPORT_WITNESS[1]}" ]] || ! command -v node >/dev/null 2>&1; then echo "wasmcc: FATAL (conftest): the PHANTOM-IMPORT witness (libexec/wasm-import-witness.mjs + node) is unavailable — the probe's import surface cannot be attested. Refusing rather than answering configure dishonestly." >&2 exit 1 fi fi phantom="$("${IMPORT_WITNESS[@]}" "$conftest_out" --mode=static "${cft_witness_allow[@]}" 2>&1)" && exit 0 echo "$phantom" | sed 's/^/wasmcc: conftest probe references unresolvable symbol(s): /' >&2 echo "wasmcc: conftest link FAILED honestly (WASMLD_HWJS_CONFTEST fast path, ticket #4): the probe references symbol(s) no real link could resolve — answering configure 'no' instead of shipping a phantom env import." >&2 rm -f "$conftest_out" exit 1 fi if [[ "$HWJS" == "1" ]]; then # CORO LINK MODE: bitcode startfiles + runtime TU into the merge # (wasmld partitions by file magic), thunks at the native stage, # NO -lc (wasmld owns the musl bitcode pool). The builtins archive # stays: native leaf math (soft-float/__int128), never on a # suspend or sjlj path — correct at the final wasm-ld stage. for f in "$WACKY_CRT1_HWJS" "$WACKY_HWJS_RT" "$WACKY_HWJS_ENTRY" "$WACKY_HWJS_THUNKS" "$WACKY_HWJS_WTHR"; do if [[ ! -f "$f" ]]; then echo "wasmcc: HWJS link mode needs startfile '$f' (run 'make -C sysroot hwjs-startfiles musl-bc' or set WACKY_CRT1_HWJS/WACKY_HWJS_RT/WACKY_HWJS_ENTRY/WACKY_HWJS_THUNKS/WACKY_HWJS_WTHR)" >&2 exit 1 fi done # R3: the default-path runtime is verified at the injection vantage. [[ "${WACKY_HWJS_RT_VERIFY:-0}" == "1" ]] && hwjs_rt_verify "$WACKY_HWJS_RT" # §8b STARTFILE DELIVERY (D1-BASH): the bitcode startfiles # (crt1_musl_hwjs, hwjs_user_rt, hwjs_user_entry) MUST reach wasmld # AS BITCODE so they join the merge (wasmld:48 — crt1's _start→main # edge has to be in the bitcode closure for the ramp to bracket # main's coroutine; otherwise a NATIVE crt1 calls the coroutinized # main and consumes its frame handle as the exit code — the §8b hole # wasmld refuses). But the clang driver decides compile-vs-link # phase by EXTENSION: a `.bc` link input is codegen'd to a native # temp `.o` (`-x ir`) BEFORE -fuse-ld ever sees it, while a # bitcode-in-`.o` (how every program TU already arrives) passes # straight through. The repro links call wasmld directly and dodge # this; bash is the first autotools program through this driver path. # Stage the three bitcode startfiles under `.o` names so clang hands # them to wasmld untouched (wasmld classifies by file magic, not # extension). Native thunks/wthr stay as-is. hwjs_stage="$(mktemp -d "${TMPDIR:-/tmp}/wasmcc-hwjs-stage.XXXXXX")" trap 'rm -rf "$hwjs_stage"' EXIT stage_bc() { # -> echoes /.o (bitcode preserved) local src="$1" base base="$(basename "${src%.bc}")" cp -f "$src" "$hwjs_stage/$base.o" printf '%s\n' "$hwjs_stage/$base.o" } WACKY_CRT1_HWJS_O="$(stage_bc "$WACKY_CRT1_HWJS")" # PLAN-HWJS-RT-PSABI R4 (WASMLD_RT_IMPORT=1, dynamic mains only): # the runtime TU stays OUT of the merge — the loader provides the # kernel-built libhwjs-rt module #0 and the main IMPORTS the ops # (witness sanctions exactly that surface). Empty staging var is # filtered off the link line below. if [[ "${WASMLD_RT_IMPORT:-0}" == "1" ]]; then if [[ "${WASMLD_DYNAMIC_MAIN:-0}" == "0" ]]; then echo "wasmcc: FATAL (WASMLD_RT_IMPORT): runtime-import linking requires WASMLD_DYNAMIC_MAIN=1 (a static image must carry the archive packaging)" >&2 exit 1 fi echo "wasmcc: NOTE (WASMLD_RT_IMPORT): runtime TU excluded from the merge — the kernel-provided libhwjs-rt (module #0) is the provider at load time (PLAN-HWJS-RT-PSABI R4)." >&2 WACKY_HWJS_RT_O="" # PUBLICATION SEED (PLAN-HWJS-RT-PSABI follow-up, dynamic-libc prereq — # linux-in-the-browser task #7): excluding the runtime TU also # excludes its hwjs.suspends.v1 PUBLICATION of the one userland # park leaf __hwjs_user_park (published on its DEFINITION in # arch/wasm32/vdso/hwjs_user_rt.c; kernel ABI spec §4.1 — the # rest of the rt callable surface is native by construction). # Without it, musl's plain `extern void __hwjs_user_park(unsigned)` # decls (dlopen/_Fork/execve/__syscall_cp) stop seeding, their # callers stay NATIVE, and rt's leaf coroutine parks under a # native caller — the F151 orphan-at-clear trap, witnessed # in-guest on the M4 tclsh stub (dlopen's park leaked, the next # bracket entry trapped). So every rt-import link injects a tiny # seed TU carrying the annotated PROTOTYPE (+ a used, # never-called static: an unreferenced prototype produces no IR, # and with it no annotation entry): @llvm.global.annotations is # appending-linkage, the pass canonicalizes it to the # hwjs-suspends fn-attr at run() start, and the F83 cross-TU # edge contract routes every caller of the published DECLARATION # as a CALLEE-SUSPEND descend into the imported coroutine — # exactly how the merged-TU world routed it. # RED (gate-only): HWJS_RT_IMPORT_NO_SEED=1 skips the injection # and reproduces the park-under-native failure class; it is a # fix-disabling knob, so a production link refuses it unless # WASMLD_HWJS_ALLOW_RED_KNOBS=1 acknowledges a deliberate # non-vacuity arm. if [[ "${HWJS_RT_IMPORT_NO_SEED:-0}" != "0" ]]; then if [[ "${WASMLD_HWJS_ALLOW_RED_KNOBS:-0}" != "1" ]]; then echo "wasmcc: FATAL: fix-disabling RED knob HWJS_RT_IMPORT_NO_SEED=${HWJS_RT_IMPORT_NO_SEED} is set for a PRODUCTION rt-import link — without the publication-seed TU the runtime's park leaf __hwjs_user_park loses its hwjs.suspends.v1 seed, musl's dlopen/_Fork/execve/__syscall_cp callers stay native, and the leaf parks under a native caller (the in-guest orphan-at-clear trap). Unset it, or set WASMLD_HWJS_ALLOW_RED_KNOBS=1 for a deliberate non-vacuity arm." >&2 exit 1 fi echo "wasmcc: NOTE (HWJS_RT_IMPORT_NO_SEED, RED arm ACKNOWLEDGED): SKIPPING the rt-import publication-seed TU — the pre-fix park-under-native shape. Non-production." >&2 WACKY_HWJS_RT_SEED_O="" else WACKY_HWJS_RT_SEED_O="$hwjs_stage/hwjs-rt-import-pubseed.o" cat > "$hwjs_stage/hwjs-rt-import-pubseed.c" <<'PUBSEED' /* hwjs-rt-import publication seed — GENERATED by wasmcc (WASMLD_RT_IMPORT=1). * Annotated prototypes for the rt callable surface's SUSPENDING leaves, * re-published module-side because the runtime TU (their publishing home) * is excluded from this merge. See the injection comment in libexec/brintcc-musl. */ __attribute__((annotate("hwjs.suspends.v1"))) extern void __hwjs_user_park(unsigned int reason); __attribute__((used)) static void __hwjs_rt_import_pubseed(void) { __hwjs_user_park(0u); /* reason 0 = NONE/breath; never executed */ } PUBSEED "$HWJS_LLVM_BIN/clang" \ "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ -emit-llvm -c "$hwjs_stage/hwjs-rt-import-pubseed.c" \ -o "$WACKY_HWJS_RT_SEED_O" \ || { echo "wasmcc: FATAL (WASMLD_RT_IMPORT): the publication-seed TU failed to compile to bitcode (error above) — an unseeded rt-import link would park under native callers, so refusing rather than linking the failure class in." >&2; exit 1; } echo "wasmcc: NOTE (WASMLD_RT_IMPORT): INJECTED the publication-seed TU — hwjs.suspends.v1 prototypes for the rt surface's suspending leaves (__hwjs_user_park), restoring the excluded runtime TU's cross-TU suspend seed (RED: HWJS_RT_IMPORT_NO_SEED=1)." >&2 fi else WACKY_HWJS_RT_O="$(stage_bc "$WACKY_HWJS_RT")" WACKY_HWJS_RT_SEED_O="" fi WACKY_HWJS_ENTRY_O="$(stage_bc "$WACKY_HWJS_ENTRY")" # ── SINGLE-STEP SOURCES (HWJS-DEFERRALS §2 follow-up: the ticket-#6 # single-step gap). A C-family source on the LINK line ("wasmcc foo.c # -o foo.wasm") used to be compiled by clang's LINK driver to a NATIVE # temp object (no -emit-llvm, and under the -unknown LINK triple, not # the -linux-musl COMPILE triple) BEFORE -fuse-ld handed the link to # wasmld — so the user TU never joined the merged-module coroutinize # pass at all: `main` stayed native (the libc_start_main_stage2.resume # call_indirect trap for any non-3-arg main — synthesizeMainDriveThunks # cannot fire on a main it never sees), and every libc reference the # merge never saw leaked as an unresolvable env import (a single-step # hello "linked" but failed to instantiate on env.printf). Pre-compile # each link-line source HERE exactly like compile mode does (pinned # clang, COMPILE triple, -emit-llvm, same COMMON/ISYSTEM args) and hand # the staged bitcode to the link instead, so single-step and two-step # are the SAME merged-module-then-pass path from this point on. # RED (gate-only): HWJS_CORO_NO_LINK_SRC_BC=1 skips the pre-compile and # reproduces the pre-fix native-TU behavior; it is a fix-disabling knob, # so a production link refuses it unless WASMLD_HWJS_ALLOW_RED_KNOBS=1 # acknowledges a deliberate non-vacuity arm (main_drive_thunk.mjs). if [[ "${HWJS_CORO_NO_LINK_SRC_BC:-0}" != "0" && "${WASMLD_HWJS_ALLOW_RED_KNOBS:-0}" != "1" ]]; then echo "wasmcc: FATAL: fix-disabling RED knob HWJS_CORO_NO_LINK_SRC_BC=${HWJS_CORO_NO_LINK_SRC_BC} is set for a PRODUCTION coro link — a link-line source would bypass the merged-module coroutinize pass (native main -> the stage2 call_indirect trap; unmerged libc refs -> unresolvable env imports). Unset it, or set WASMLD_HWJS_ALLOW_RED_KNOBS=1 for a deliberate non-vacuity arm." >&2 exit 1 fi # Compile-relevant flags forwarded from the link line to the per-source # bitcode compile (the flags clang's link driver would have applied to # its own compile phase). Link-only args (-o/-L/-l/-Wl,*) are excluded; # positional inputs never join a compile. hwjs_cc_flags=() hwjs_cc_expect="" for a in "$@"; do if [[ -n "$hwjs_cc_expect" ]]; then [[ "$hwjs_cc_expect" == "cc" ]] && hwjs_cc_flags+=( "$a" ) hwjs_cc_expect="" continue fi case "$a" in -o|-L) hwjs_cc_expect="skip" ;; -I|-D|-U|-include|-imacros|-isystem|-idirafter|-iquote|-Xclang) hwjs_cc_flags+=( "$a" ); hwjs_cc_expect="cc" ;; -o*|-L*|-l*|-Wl,*) : ;; -*) hwjs_cc_flags+=( "$a" ) ;; *) : ;; esac done # D1-BASH: the coro arm (wasmld) refuses `-lNAME` — it can't decide a # library's bitcode-vs-native kind from a name alone, so each archive # must arrive as a concrete file path. Autotools links its own static # sublibs that way (bash: `-L./builtins ... -lbuiltins -lglob -lsh`). # Resolve every `-lNAME` against the accumulated `-L` search path # (and WACKY_LIBC_DIR) to /libNAME.a here. `-lc` is the lone # exception: wasmld swaps it for the musl bitcode pool, so it passes # through verbatim. Unresolvable `-lNAME` is left as-is (wasmld then # reports it against the real, missing archive). hwjs_link_args=() # BRINTOS-884: libraries a `-lNAME` resolved to a SHARED object. They are # not link inputs — they are needed-graph entries, handed to wasmld through # the environment knob it already reads, so one recorder derives every # digest and soname in this build. hwjs_needed_sos=() # The same arm gate wasmld applies, for the same reason: ON for the # dynamic-main consumer arm AND for a -shared side module. On the -shared # arm it RESOLVES and DROPS without recording (see the export below, still # LIBC_IMPORT-gated): a side module carries no needed list of its own. A # static link cannot consume a .so, and a side module's own imports bind # through the loader's graph rather than through this arm. Everywhere else # `-lNAME` keeps resolving to lib.a, byte for byte as before. # A FINISHED side module, by content -- never by name. readline's build names # its object files readline.so, vi_mode.so, funmap.so ...; those are bitcode, # not shared libraries, and dropping them leaves the -shared link with no # inputs at all ("no bitcode roots on the link line"). Test the wasm magic and # the dylink.0 section instead, which is the same question wasmld's own # classifier asks. _hwjs_is_side_module() { [[ -f "$1" ]] || return 1 [[ "$(head -c4 "$1" 2>/dev/null | od -An -tx1 | tr -d ' \n')" == "0061736d" ]] || return 1 head -c 4096 "$1" 2>/dev/null | grep -qa 'dylink.0' } hwjs_dash_l_shared=0 hwjs_is_shared=0 case " $* " in *" -shared "*|*" --shared "*) hwjs_is_shared=1 ;; esac if [[ ( "${WASMLD_LIBC_IMPORT:-0}" == "1" || $hwjs_is_shared == 1 ) && "${WASMLD_DASH_L_STATIC:-0}" != "1" ]]; then hwjs_dash_l_shared=1 fi hwjs_Ldirs=( "$WACKY_LIBC_DIR" ) hwjs_prev="" for a in "$@"; do case "$a" in -L?*) hwjs_Ldirs+=( "${a#-L}" ) ;; -L) : ;; # next token is the dir; captured below esac if [[ "$hwjs_prev" == "-L" ]]; then hwjs_Ldirs+=( "$a" ); fi hwjs_prev="$a" done hwjs_prev="" hwjs_srcn=0 for a in "$@"; do # OUTPUT_OPERAND_VERBATIM: `-o`'s operand is a filename to CREATE, not an # input to classify. The loop below sorts every argument by shape and, until # this arm, tracked `prev` for `-L` alone -- so the output path fell through # to the shape arms like any other token. A `-shared` link's output ends in # `.so` by definition, so it was tested by the `*.so` arm, and the answer # depended on whether the file happened to exist yet: # # do_compile, FIRST link -- no file, test false, path kept, link succeeds. # do_install, RE-link -- the file is there from do_compile and IS a # valid side module, so SIDE_MODULE_SO_DROP ate # the output path. # # `-o` was then left to take the NEXT token as its operand, which in waf's # link rule is STLIB_MARKER (`-Wl,-Bstatic`) -- and wasmld bound its output # name to that, which node then rejects as one of its own options. That # asymmetry is the whole of "do_compile succeeds, the install re-link does # not". # # The SEPARATED form above is the live defect. The attached `-o.so` # form reaches the same `*.so` arm, but survives today by ACCIDENT: # _hwjs_is_side_module opens its argument as a path, and `-o` is not # one, so the `-f` test fails and the token is kept. It is exempted here # too, so the output operand is safe by construction rather than by that # coincidence -- and stays safe if the existence test is ever relaxed. if [[ "$hwjs_prev" == "-o" ]]; then hwjs_link_args+=( "$a" ); hwjs_prev=""; continue; fi if [[ "$hwjs_prev" == "-L" ]]; then hwjs_prev=""; continue; fi case "$a" in -o) hwjs_link_args+=( "$a" ); hwjs_prev="-o"; continue ;; -o?*) hwjs_link_args+=( "$a" ); continue ;; -L) hwjs_prev="-L"; continue ;; # drop `-L dir` (resolved to paths) -L?*) continue ;; # drop `-Ldir` *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) # Single-step source: pre-compile to bitcode (above) so it # joins the merged-module pass like a two-step object. if [[ "${HWJS_CORO_NO_LINK_SRC_BC:-0}" != "0" ]]; then echo "wasmcc: NOTE (HWJS_CORO_NO_LINK_SRC_BC, RED arm ACKNOWLEDGED): leaving link-line source '$a' to the native clang link driver — the pre-fix single-step behavior. Non-production." >&2 hwjs_link_args+=( "$a" ) else [[ -f "$a" ]] || { echo "wasmcc: FATAL: link-line source '$a' does not exist." >&2; exit 1; } hwjs_srcn=$((hwjs_srcn + 1)) hwjs_src_base="$(basename "$a")" hwjs_src_bc="$hwjs_stage/linksrc-${hwjs_srcn}-${hwjs_src_base%.*}.o" "$HWJS_LLVM_BIN/clang" \ "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ "${hwjs_cc_flags[@]}" \ -emit-llvm -c "$a" -o "$hwjs_src_bc" \ || { echo "wasmcc: FATAL: single-step bitcode compile of link-line source '$a' failed (it must join the merged-module coroutinize pass; error above)." >&2; exit 1; } hwjs_link_args+=( "$hwjs_src_bc" ) fi ;; -lc) hwjs_link_args+=( "$a" ) ;; # wasmld owns the musl pool -l?*) name="${a#-l}" found="" found_so="" # Per DIRECTORY, .so before .a — GNU ld's order, so a -L # earlier on the line shadows a later one whichever kind it # holds, which is what the caller's -L order expresses. # DASH_L_SHARED_WASM_ONLY: the .so must BE a wasm module. ncurses # stages libtermcap.so as an ld script (`INPUT(libncurses.so)`), # and a non-wasm file recorded as a needed entry fatals the link # in needed_so_record. Rejecting here falls through to the # archive branch, then to the loud not-found. A -shared link # records nothing -- it drops the hit -- so the test is scoped # off there rather than turning that drop into a refusal. for d in "${hwjs_Ldirs[@]}"; do if [[ $hwjs_dash_l_shared == 1 && -f "$d/lib${name}.so" ]] \ && { [[ $hwjs_is_shared == 1 ]] || _hwjs_is_side_module "$d/lib${name}.so"; }; then found_so="$d/lib${name}.so"; break fi if [[ -f "$d/lib${name}.a" ]]; then found="$d/lib${name}.a"; break; fi done if [[ -n "$found_so" ]]; then # NOT a link input. Handing the .so to the linker asks it # to absorb a dynamic object statically, which it refuses # by name; the dynamic arm wants the symbol left undefined # so --unresolved-symbols=import-dynamic imports it and the # loader binds it from the module it places. sodir="$(cd "$(dirname "$found_so")" && pwd)" hwjs_needed_sos+=( "$sodir/$(basename "$found_so")" ) elif [[ -n "$found" ]]; then # wasmld extracts archives in a private temp CWD, so a # relative path (`./builtins/libbuiltins.a`) breaks; hand # it an absolute path. fdir="$(cd "$(dirname "$found")" && pwd)" hwjs_link_args+=( "$fdir/$(basename "$found")" ) else hwjs_link_args+=( "$a" ) fi ;; # PATH_SO_NEEDED: a shared object named by PATH rather than by # A shared object named by PATH rather than by -lNAME -- the # shape libtool emits. Same needed-graph destination as the -l # hits above; see the note beside the append below, which # already anticipated a caller naming a library by path. *.so|*.so.*) if [[ $hwjs_dash_l_shared == 1 && -f "$a" ]] && _hwjs_is_side_module "$a"; then sodir="$(cd "$(dirname "$a")" && pwd)" hwjs_needed_sos+=( "$sodir/$(basename "$a")" ) elif [[ $hwjs_is_shared == 1 ]] && _hwjs_is_side_module "$a"; then # SIDE_MODULE_SO_DROP -- see the note in wasmcc-glibc. # Depending on a library is not absorbing one; the # symbols stay undefined and the loader binds them. : else hwjs_link_args+=( "$a" ) fi ;; *) hwjs_link_args+=( "$a" ) ;; esac hwjs_prev="" done # Hand the shared hits to wasmld through the knob it already reads, so the # snapshot, the digest, the soname and the witness surface are derived in # ONE place for both producers. Appended to any value the caller already # set rather than replacing it: a recipe naming a library by path and a # link line naming another by -l are both legitimate, and the caller's # entries keep their position, which is the order the loader walks. # LIBC_IMPORT-gated even though the search above is not: wasmld refuses # WASMLD_NEEDED_LIBS on a link that is not the consumer arm, by name. The # -shared arm therefore resolves its hits and drops them here. if [[ ${#hwjs_needed_sos[@]} -gt 0 && "${WASMLD_LIBC_IMPORT:-0}" == "1" ]]; then hwjs_needed_joined="$(IFS=':'; printf '%s' "${hwjs_needed_sos[*]}")" if [[ -n "${WASMLD_NEEDED_LIBS:-}" ]]; then export WASMLD_NEEDED_LIBS="${WASMLD_NEEDED_LIBS}:${hwjs_needed_joined}" else export WASMLD_NEEDED_LIBS="${hwjs_needed_joined}" fi echo "wasmcc: NOTE (BRINTOS-884): -l resolved ${#hwjs_needed_sos[@]} shared librar(y/ies) to needed-graph entries: ${hwjs_needed_joined}" >&2 fi # Child run (not exec) so the EXIT trap reaps the staging dir. hwjs_link "$CLANG" "$WASMLD" \ "$LINK_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ "$WACKY_CRT1_HWJS_O" \ ${WACKY_HWJS_RT_O:+"$WACKY_HWJS_RT_O"} \ ${WACKY_HWJS_RT_SEED_O:+"$WACKY_HWJS_RT_SEED_O"} \ "$WACKY_HWJS_ENTRY_O" \ "${hwjs_link_args[@]}" \ "$WACKY_HWJS_THUNKS" \ "$WACKY_HWJS_WTHR" \ "${BUILTINS_ARG[@]}" exit $? fi for f in "$WACKY_CRT1" "$WACKY_WTHR"; do if [[ ! -f "$f" ]]; then echo "wasmcc: link mode needs startfile '$f' (run 'make -C wackywasm-tools libc hello-k7' or set WACKY_CRT1/WACKY_WTHR)" >&2 exit 1 fi done hwjs_link "$CLANG" "$WASMLD" \ "$LINK_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ "$WACKY_CRT1" \ "$@" \ "$WACKY_WTHR" \ -L"$WACKY_LIBC_DIR" -lc \ "${BUILTINS_ARG[@]}" exit $? fi # CORO COMPILE MODE: pinned LLVM-22 clang, IR out (-emit-llvm rides -c; # -E/-M*/--version etc. are unaffected by it only when compiling, so gate # on an actual compile). Assembly has no IR form: native toolchains-LLVM compile. # # D1-BASH: `clang -emit-llvm -c foo.c` (no -o) defaults its output to # foo.BC, but every autotools Makefile suffix rule (`.c.o: $(CC) -c $<`) # expects foo.O and then `ar`s foo.o. So when -c is present and the caller # gave no -o, we pin -o to the conventional .o ourselves — the file # still holds bitcode (wasmld partitions by magic, not extension), it just # carries the name the Makefile/ar will look for. An explicit -o is honored # verbatim (clang writes bitcode into whatever name was requested). if [[ "$HWJS" == "1" && "$has_asm_input" == "0" && "${WASMLD_HWJS_CONFTEST:-0}" == "0" ]]; then hwjs_emit=() hwjs_compile=0 have_o=0 src="" prev="" for a in "$@"; do case "$a" in -c) hwjs_emit=( -emit-llvm ); hwjs_compile=1 ;; # 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 hwjs_out=() if [[ "$hwjs_compile" == "1" && "$have_o" == "0" && -n "$src" ]]; then base="${src##*/}" # strip dir base="${base%.*}" # strip source extension hwjs_out=( -o "${base}.o" ) fi exec "$HWJS_LLVM_BIN/clang" \ "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ "${hwjs_emit[@]}" \ "$@" \ "${hwjs_out[@]}" fi exec "$CLANG" \ "$COMPILE_TARGET" \ "${COMMON_ARGS[@]}" \ "${ISYSTEM_ARGS[@]}" \ "$@"