build-shared.sh: rebuild on stale objects, skip needless re-archiving, link atomically

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.
This commit is contained in:
Brad Arant committed 2026-09-01 00:04:25 +00:00
1 parent 7893746582
commit 54b6e522f9
1 file changed
+34 -3
+34 -3
View File
@@ -37,12 +37,20 @@ echo "Compiling $INPUT -> $STEM.compiled.cpp (shared mode)"
"$HERE/jetc" --shared "$INPUT" "$HERE/$STEM.compiled.cpp" "$HERE/jetc" --shared "$INPUT" "$HERE/$STEM.compiled.cpp"
echo "Building JetCore runtime objects as position-independent code..." 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="" list=""
for f in "$JC"/*.cpp; do for f in "$JC"/*.cpp; do
base=$(basename "$f") base=$(basename "$f")
if [ "$base" = "jet-2.0.cpp" ]; then continue; fi if [ "$base" = "jet-2.0.cpp" ]; then continue; fi
obj="$HERE/${base%.cpp}.pic.o" 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" & g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$f" -o "$obj" &
fi fi
list="$list $obj" list="$list $obj"
@@ -62,21 +70,44 @@ done
# at dlopen() time, even though nothing here actually uses it - a normal # 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. # archive link never pulls IMFMessage.o in at all, so it never hits this.
cu_pic_list="" cu_pic_list=""
cu_rebuilt=false
for f in "$CU"/*.cpp; do for f in "$CU"/*.cpp; do
base=$(basename "$f") base=$(basename "$f")
obj="$HERE/cu_${base%.cpp}.pic.o" 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" & g++ -g -fPIC -c -std=c++23 -I"$CU" "$f" -o "$obj" &
cu_rebuilt=true
fi fi
cu_pic_list="$cu_pic_list $obj" cu_pic_list="$cu_pic_list $obj"
done done
wait 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 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" g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$HERE/$STEM.compiled.cpp" -o "$HERE/$STEM.compiled.pic.o"
echo "Linking $OUTPUT..." 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 -L"$HERE" -lCoreUtils_pic -lmysqlclient -lcrypto
mv -f "$TMPOUT" "$OUTPUT"
echo "Built $OUTPUT" echo "Built $OUTPUT"