Three correctness fixes to the --shared build path: - Only skipped rebuilding a .pic.o when it existed at all, never checking whether the source .cpp was newer - confirmed live: Tag.pic.o (and 6 other JetCore objects) sat uncompiled since 2026-08-25 while Tag.cpp itself picked up same-day fixes, so every .jtc silently ran yesterday's JetCore code. - Only re-archive libCoreUtils_pic.a when a member object actually changed, instead of unconditionally on every build. - Link to a temp file in the same directory, then rename() it into place, instead of linking directly onto the live .jtc path. A running APIServer may already have that exact path dlopen()ed (hot-reload watches this directory) - overwriting it in place corrupts the live mapping's code pages out from under the process, causing a dlclose() SIGSEGV the next time it reloads. rename() never touches the old inode, so anything with it still mapped keeps seeing the unmodified original.
114 lines
5.0 KiB
Bash
Executable File
114 lines
5.0 KiB
Bash
Executable File
#!/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_<basename>(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 <input.jet> <output.jtc>
|
|
#
|
|
# 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 <input.jet> <output.jtc>"
|
|
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 <try> tag dispatch among them) - every
|
|
# .jtc built off the stale cache silently ran yesterday's JetCore code,
|
|
# with <try> 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"
|