Defect
Reconfiguring an existing build directory silently drops Threads::Threads (and, where TBB is in play, TBB::tbb) from the libraries' PUBLIC link and from the installed export.
detect_threading_systems() (cmake/Threading.cmake:6) early-returns when its completion flag is in the cache:
if(DEFINED CACHE{LIBSTATS_THREADING_DETECTION_COMPLETE})
...
return()
endif()
Cache variables persist across configure passes; imported targets do not — they must be recreated by find_package() on every pass. On any reconfigure of an already-configured build dir, the guard skips find_package(Threads), so Threads::Threads never exists during that pass, and the consuming guard goes quiet instead of failing:
# CMakeLists.txt:603
if(TARGET Threads::Threads)
target_link_libraries(libstats_platform_obj PRIVATE Threads::Threads)
target_link_libraries(libstats_static PUBLIC Threads::Threads)
target_link_libraries(libstats_shared PUBLIC Threads::Threads)
endif()
The PUBLIC links are what install(EXPORT) writes into libstats-targets.cmake — so the installed package's INTERFACE_LINK_LIBRARIES differs depending on whether the producing build dir was configured once or twice. Same install prefix, same commit, different package.
detect_tbb_unified() (cmake/Threading.cmake:140) has the identical cache guard, and CMakeLists.txt:618 the identical if(TARGET TBB::tbb) silent gate — the TBB export is lost the same way on configurations that link TBB (LIBSTATS_FORCE_TBB=ON, or platforms where TBB wins detection).
Evidence
Found 2026-07-26 during #83's install-byte-diff verification (PR #89). The baseline install tree — captured from a build dir that had been configured, then reconfigured to add -DCMAKE_EXPORT_COMPILE_COMMANDS=ON — was missing both Threads::Threads entries in libstats-targets.cmake. Fresh-configure builds of the same commit (and of the #83 branch) both produce them. The diff:
< INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:libstats::libstats_simd_interface>"
---
> INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:libstats::libstats_simd_interface>;Threads::Threads"
Reproduce: cmake --preset dev && cmake --preset dev -DLIBSTATS_VERBOSE_BUILD=ON (any second configure works), install both states to scratch prefixes, diff lib/cmake/libstats/libstats-targets.cmake.
Impact
- Linux: an installed package produced from a reconfigured build dir can underlink consumers —
-pthread (compile and link semantics) no longer propagates through the imported target. libstats_platform_obj also loses its PRIVATE Threads::Threads, so the library's own TUs can lose -pthread on the reconfigure pass.
- macOS: masked — pthreads live in libSystem — which is why this survived every local build to date. The defect is still real: install content is nondeterministic in configure-count.
- CI: not currently caught. The installed-package smoke-test leg configures exactly once, so it always sees the fresh-pass export.
Fix direction
The cheap, correct pattern: cache the detection results (as now), but make target existence the guard for the find_package() calls that create imported targets, e.g. re-run find_package(Threads QUIET) whenever NOT TARGET Threads::Threads — find_package on a cached result is fast, and imported-target recreation is exactly what it's for. Same treatment for the TBB path. The if(TARGET ...) consumers at CMakeLists.txt:603/618 can then stay as-is, or harden to message(FATAL_ERROR) when detection said the dependency exists but the target is absent — turning any future recurrence of this class from a silent export change into a configure failure.
Verification for whoever takes this: the #83 double-configure repro above must produce byte-identical libstats-targets.cmake files, and a Linux double-configured install must let consumer_example link.
Defect
Reconfiguring an existing build directory silently drops
Threads::Threads(and, where TBB is in play,TBB::tbb) from the libraries' PUBLIC link and from the installed export.detect_threading_systems()(cmake/Threading.cmake:6) early-returns when its completion flag is in the cache:Cache variables persist across configure passes; imported targets do not — they must be recreated by
find_package()on every pass. On any reconfigure of an already-configured build dir, the guard skipsfind_package(Threads), soThreads::Threadsnever exists during that pass, and the consuming guard goes quiet instead of failing:The PUBLIC links are what
install(EXPORT)writes intolibstats-targets.cmake— so the installed package'sINTERFACE_LINK_LIBRARIESdiffers depending on whether the producing build dir was configured once or twice. Same install prefix, same commit, different package.detect_tbb_unified()(cmake/Threading.cmake:140) has the identical cache guard, andCMakeLists.txt:618the identicalif(TARGET TBB::tbb)silent gate — the TBB export is lost the same way on configurations that link TBB (LIBSTATS_FORCE_TBB=ON, or platforms where TBB wins detection).Evidence
Found 2026-07-26 during #83's install-byte-diff verification (PR #89). The baseline install tree — captured from a build dir that had been configured, then reconfigured to add
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON— was missing bothThreads::Threadsentries inlibstats-targets.cmake. Fresh-configure builds of the same commit (and of the #83 branch) both produce them. The diff:Reproduce:
cmake --preset dev && cmake --preset dev -DLIBSTATS_VERBOSE_BUILD=ON(any second configure works), install both states to scratch prefixes, difflib/cmake/libstats/libstats-targets.cmake.Impact
-pthread(compile and link semantics) no longer propagates through the imported target.libstats_platform_objalso loses its PRIVATEThreads::Threads, so the library's own TUs can lose-pthreadon the reconfigure pass.Fix direction
The cheap, correct pattern: cache the detection results (as now), but make target existence the guard for the
find_package()calls that create imported targets, e.g. re-runfind_package(Threads QUIET)wheneverNOT TARGET Threads::Threads—find_packageon a cached result is fast, and imported-target recreation is exactly what it's for. Same treatment for the TBB path. Theif(TARGET ...)consumers atCMakeLists.txt:603/618can then stay as-is, or harden tomessage(FATAL_ERROR)when detection said the dependency exists but the target is absent — turning any future recurrence of this class from a silent export change into a configure failure.Verification for whoever takes this: the #83 double-configure repro above must produce byte-identical
libstats-targets.cmakefiles, and a Linux double-configured install must letconsumer_examplelink.