diff --git a/jetc/build-shared.sh b/jetc/build-shared.sh index f53298a..7e62173 100755 --- a/jetc/build-shared.sh +++ b/jetc/build-shared.sh @@ -37,12 +37,20 @@ 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" ]; then + 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" @@ -62,21 +70,44 @@ done # 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" ]; then + 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 -ar rcs "$HERE/libCoreUtils_pic.a" $cu_pic_list +# 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..." -g++ -shared -fPIC -o "$OUTPUT" "$HERE/$STEM.compiled.pic.o" $list -std=c++23 \ +# 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"