#!/bin/bash # Compiles a .jet file into a shared object (conventionally named .jtc - # "jet compiled" - to mark it as an APIServer-loadable content module, not # a generic .so) exporting a stable, dlsym()-able render entry point - # `jetc_render_(jet::Global &global, coreutils::MString &out)` # - for a long-running server to load and call directly instead of # spawning a jet-2.0 process per request. # # Usage: ./build-shared.sh # # The host server links/dlopen()s the .jtc and calls the exported symbol # (see the "entry symbol:" line jetc prints) with its own jet::Global - # this script does not decide that lifecycle, only builds the module. # coreutils::Exception propagates out of the call uncaught; the host # decides how to handle a rendering failure (log it, 500, etc.). set -e JC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CU="$JC/../CoreUtils" HERE="$JC/jetc" if [ $# -ne 2 ]; then echo "usage: $0 " exit 1 fi INPUT="$1" OUTPUT="$2" STEM="$(basename "$OUTPUT" .jtc)" if [ ! -x "$HERE/jetc" ]; then echo "Building jetc..." g++ -g -std=c++23 -o "$HERE/jetc" "$HERE/jetc.cpp" "$HERE/JetParser.cpp" "$HERE/JetCodegen.cpp" fi echo "Compiling $INPUT -> $STEM.compiled.cpp (shared mode)" "$HERE/jetc" --shared "$INPUT" "$HERE/$STEM.compiled.cpp" echo "Building JetCore runtime objects as position-independent code..." # Rebuild whenever the SOURCE is newer than the cached object, not just # when the object is missing - a pure existence check silently keeps # serving a stale compile forever once the .o exists once, even after the # .cpp changes. Confirmed this bit for real: Tag.pic.o (and 6 other # JetCore objects) sat uncompiled since 2026-08-25 while Tag.cpp itself # picked up same-day fixes (the tag dispatch among them) - every # .jtc built off the stale cache silently ran yesterday's JetCore code, # with falling through as unrecognized literal text at runtime. list="" for f in "$JC"/*.cpp; do base=$(basename "$f") if [ "$base" = "jet-2.0.cpp" ]; then continue; fi obj="$HERE/${base%.cpp}.pic.o" if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$f" -o "$obj" & fi list="$list $obj" done # libCoreUtils.a (built by CoreUtils/compile) is not position-independent - # ld refuses to link non-PIC static-library code into a shared object # ("relocation ... can not be used when making a shared object"). Rather # than touching CoreUtils's own build (its .a is fine and used as-is by # jet-2.0 and everything else), build a PIC copy of its objects here and # archive them - archived the same way libCoreUtils.a itself is, so the # linker only pulls in objects something actually references, same as a # normal -lCoreUtils link. This matters: force-linking every CoreUtils # .cpp directly (as JetCore's own objects are, below) surfaces an # unrelated, pre-existing incomplete class (IMFMessage - a known # partially-migrated class, see project memory) as an unresolved vtable # at dlopen() time, even though nothing here actually uses it - a normal # archive link never pulls IMFMessage.o in at all, so it never hits this. cu_pic_list="" cu_rebuilt=false for f in "$CU"/*.cpp; do base=$(basename "$f") obj="$HERE/cu_${base%.cpp}.pic.o" if [ ! -f "$obj" ] || [ "$f" -nt "$obj" ]; then g++ -g -fPIC -c -std=c++23 -I"$CU" "$f" -o "$obj" & cu_rebuilt=true fi cu_pic_list="$cu_pic_list $obj" done wait # Only re-archive when something actually changed - ar rcs on an unchanged # member set is harmless but pointless work every single build otherwise. if [ "$cu_rebuilt" = true ] || [ ! -f "$HERE/libCoreUtils_pic.a" ]; then ar rcs "$HERE/libCoreUtils_pic.a" $cu_pic_list fi g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$HERE/$STEM.compiled.cpp" -o "$HERE/$STEM.compiled.pic.o" echo "Linking $OUTPUT..." # Link to a temp file in the SAME directory as $OUTPUT, then rename() it # into place, rather than linking directly onto $OUTPUT - a live # APIServer instance may already have this exact path dlopen()ed (hot- # reload watches this directory). g++/ld opens $OUTPUT with O_TRUNC and # writes the new object into that same inode, which is also the one # backing the running server's mmap()ed view of the OLD module - # corrupting its code pages out from under it mid-flight. Confirmed as # the real cause of a live, reproducible dlclose() SIGSEGV (garbage PC # inside _dl_call_fini): the server's own unload-before-reload path # calls dlclose() on the now-corrupted mapping shortly after a build # like this one lands. rename() is atomic and, same filesystem, never # touches the old inode - any process with it still mapped keeps seeing # the unmodified original until it unmaps, and the watcher's own # dlclose()+dlopen() cycle (triggered by the CLOSE_WRITE this rename # still generates) picks up the new file cleanly on its next load. TMPOUT="$(dirname "$OUTPUT")/.$STEM.jtc.tmp.$$" g++ -shared -fPIC -o "$TMPOUT" "$HERE/$STEM.compiled.pic.o" $list -std=c++23 \ -L"$HERE" -lCoreUtils_pic -lmysqlclient -lcrypto mv -f "$TMPOUT" "$OUTPUT" echo "Built $OUTPUT"