build: serialize module@X/lib@X per directory to fix a -j race

main@ depends on both `modules` and `libs`, so `module@X` (`make -C X module`)
and `lib@X` (`make -C X lib`) could run in the same directory concurrently under
-j and race on X's shared objects -- each %.o recipe does `rm $*.o; cc -c`, so
one make would delete/rebuild an object while the other was linking it, e.g.
`ld: cannot find touchtypes.o` while linking libutils.o.  It was rare (timing
dependent) but real; it surfaced on a --without-tcl --disable-readline -j12 build.

Make `lib@%` depend on its own `module@%`: module@X builds the objects, lib@X
then only archives them, so the two never touch X's *.o at the same time.
Cross-directory parallelism is unchanged (lib@X waits only on module@X, not the
whole modules phase).

Verified: 4/4 clean --without-tcl -j12 builds pass (previously intermittently
failed); Tcl -j12 still builds 375 files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darryl L. Miles 2026-07-23 16:18:43 +00:00 committed by R. Timothy Edwards
parent c05ac60851
commit 5d6c9899d4
1 changed files with 7 additions and 1 deletions

View File

@ -106,7 +106,13 @@ LIB_TARGETS := $(addprefix lib@,${MODULES})
.PHONY: $(MODULE_TARGETS) $(LIB_TARGETS)
$(MODULE_TARGETS): module@%: ${DATABASE_H}
@$(call submake,$*,module)
$(LIB_TARGETS): lib@%: ${DATABASE_H}
# lib@X depends on module@X (its OWN module): main@ pulls both `modules` and
# `libs`, so without this `make -C X module` and `make -C X lib` can run in the
# same directory at once under -j and race on X's shared *.o (each recipe does
# `rm $*.o; cc -c $*.o`). Serializing them per-dir removes the race; module@X
# builds the objects and lib@X then just archives them. Cross-dir parallelism is
# unaffected (lib@X only waits on module@X, not the whole modules phase).
$(LIB_TARGETS): lib@%: ${DATABASE_H} module@%
@$(call submake,$*,lib)
# Cross-module archive edge (-j correctness): libdatabase.a physically archives