diff --git a/AGENT.md b/AGENT.md index 1762652f..ee3fc315 100644 --- a/AGENT.md +++ b/AGENT.md @@ -496,9 +496,13 @@ cmake --build --preset clangcl-debug `PEDANTIC_COMPILER_WERROR=ON` is the default for Windows presets — warnings break the build, fix them at the source. `USE_COMPILER_CACHE` (default ON, `cmake/CompileCache.cmake`) fronts the compiler -with our own `fastcache-cc` when it is on `PATH` and `FASTCACHE_ADDR=host:port` -names a daemon — `FASTCACHE_SRCROOT`/`FASTCACHE_BUILDTREE` are injected from the -source and build trees — and otherwise falls back to `sccache`. A cache hit +with our own `fastcache-cc` when it is on `PATH` and a daemon answers — at +`127.0.0.1:6674` by default, or wherever `FASTCACHE_ADDR=host:port` points; +`FASTCACHE_SRCROOT`/`FASTCACHE_BUILDTREE` are injected from the source and build +trees. Configure proves the cache works by compiling one tiny file through the +launcher (~0.1 s) and requiring a `HIT`/`MISS`, because a launcher that cannot +reach its daemon still compiles fine and would otherwise cost every TU a failed +connect in silence. When nothing answers it falls back to `sccache`. A cache hit reproduces only the object file, so with either launcher active the module scan and precompiled headers are turned off and MSVC debug info is forced to `/Z7` (a modmap flag makes the launcher's preprocess step fail, and a PCH or shared diff --git a/CMakeLists.txt b/CMakeLists.txt index 302040bf..31e3c9d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,9 +156,10 @@ option(TRACY_ON_DEMAND "Tracy: only collect data while a profiler is connected" # ---------------------------------------------------------------------------- # Compiler caching. Loaded before any compiler invocation so that CPM-fetched # dependencies also get cached. CompileCache.cmake prefers our own fastcache-cc -# launcher (when installed and FASTCACHE_ADDR names a daemon), falls back to -# sccache and then ccache, and silently no-ops when none is usable or -# USE_COMPILER_CACHE=OFF. It also declares the USE_COMPILER_CACHE option. +# launcher (when installed and a daemon answers, at 127.0.0.1:6674 unless +# FASTCACHE_ADDR says otherwise), falls back to sccache and then ccache, and +# silently no-ops when none is usable or USE_COMPILER_CACHE=OFF. It also declares +# the USE_COMPILER_CACHE option. include(CompileCache) diff --git a/cmake/CompileCache.cmake b/cmake/CompileCache.cmake index e07e5120..4dd60236 100644 --- a/cmake/CompileCache.cmake +++ b/cmake/CompileCache.cmake @@ -9,9 +9,10 @@ # from different directories share cache hits. It must already be installed # and on PATH. It is configured purely through the environment and caches # nothing unless FASTCACHE_ADDR / FASTCACHE_SRCROOT / FASTCACHE_BUILDTREE -# are all set, so it is only selected once a daemon address is known; the -# two roots are injected here via `cmake -E env` because CMake already -# knows them. +# are all set; the address defaults to fastcached's own port, +# 127.0.0.1:6674, and the two roots are injected here via `cmake -E env`, +# because CMake already knows them. Selecting it is conditional on a daemon +# actually answering there — see the probe below. # 2. sccache — the usual third-party launcher, used when fastcache-cc is # unavailable or unconfigured. Supports shared (Redis/S3/...) caches. # 3. ccache — the classic local cache, used when neither of the above applies. @@ -23,7 +24,7 @@ # To disable entirely: -DUSE_COMPILER_CACHE=OFF. option(USE_COMPILER_CACHE - "Use a compiler-cache launcher when one is available (fastcache-cc when configured, else sccache, else ccache) [default: ON]" + "Use a compiler-cache launcher when one is available (fastcache-cc when a daemon answers, else sccache, else ccache) [default: ON]" ON) # Respect a launcher provided externally (command line, preset, toolchain). @@ -32,54 +33,208 @@ option(USE_COMPILER_CACHE if(DEFINED CMAKE_CXX_COMPILER_LAUNCHER OR DEFINED CMAKE_C_COMPILER_LAUNCHER) message(STATUS "[cache] Compiler launcher already set externally " "(C='${CMAKE_C_COMPILER_LAUNCHER}', CXX='${CMAKE_CXX_COMPILER_LAUNCHER}'); leaving it untouched.") + # A build tree configured before this module existed carries the launcher of + # the day in its cache, and would keep it forever without a word about why + # the selection below never runs. + if(DEFINED CACHE{CMAKE_CXX_COMPILER_LAUNCHER} OR DEFINED CACHE{CMAKE_C_COMPILER_LAUNCHER}) + message(STATUS "[cache] That value comes from the CMake cache (a -D, a preset, or an older configure); " + "reconfigure with --fresh to let this module choose instead.") + endif() return() endif() -find_program(FASTCACHE_CC fastcache-cc DOC "fastcache-cc tool path; needs FASTCACHE_ADDR to be used") +find_program(FASTCACHE_CC fastcache-cc DOC "fastcache-cc tool path; needs a fastcached daemon to be used") find_program(SCCACHE sccache DOC "sccache tool path") find_program(CCACHE ccache DOC "ccache tool path") -set(FASTCACHE_ADDR "$ENV{FASTCACHE_ADDR}" CACHE STRING - "host:port of the fastcached compile-cache daemon (enables the fastcache-cc launcher)") +# Where the daemon is: FASTCACHE_ADDR from the environment, else fastcached's +# own port, which a stock daemon (and the service the installers register) +# listens on. An empty -DFASTCACHE_ADDR= opts out of fastcache-cc entirely. +set(_fc_addr_env "$ENV{FASTCACHE_ADDR}") +if(_fc_addr_env STREQUAL "") + set(_fc_addr_wanted "127.0.0.1:6674") +else() + set(_fc_addr_wanted "${_fc_addr_env}") +endif() + +# Ordinary cache semantics would freeze the address at whatever the first +# configure saw, so exporting FASTCACHE_ADDR to reach a remote daemon would do +# nothing until the build tree was wiped. Track the environment across +# configures instead and let a *change* to it retarget the cache entry — while +# leaving a -D from this very run alone, which is the one instruction more +# deliberate than the environment. The two are told apart by whether the cache +# still holds what this module last put there, which is also why the retarget +# needs a previous configure to compare against: on a first configure there is +# no bookkeeping yet, both tests hold vacuously, and a -DFASTCACHE_ADDR= meant +# to opt out would be overwritten by an address merely left in the environment. +if(NOT DEFINED CACHE{FASTCACHE_ADDR}) + set(FASTCACHE_ADDR "${_fc_addr_wanted}" CACHE STRING + "host:port of the fastcached compile-cache daemon, 127.0.0.1:6674 by default (empty disables the fastcache-cc launcher)") +elseif(DEFINED CACHE{_FASTCACHE_ADDR_APPLIED} + AND NOT _fc_addr_env STREQUAL "${_FASTCACHE_ADDR_ENV_SEEN}" + AND FASTCACHE_ADDR STREQUAL "${_FASTCACHE_ADDR_APPLIED}") + message(STATUS "[cache] FASTCACHE_ADDR changed in the environment; retargeting to ${_fc_addr_wanted}") + set(FASTCACHE_ADDR "${_fc_addr_wanted}" CACHE STRING + "host:port of the fastcached compile-cache daemon, 127.0.0.1:6674 by default (empty disables the fastcache-cc launcher)" + FORCE) +endif() +set(_FASTCACHE_ADDR_ENV_SEEN "${_fc_addr_env}" CACHE INTERNAL + "FASTCACHE_ADDR as the environment last presented it, to notice a change on reconfigure") +set(_FASTCACHE_ADDR_APPLIED "${FASTCACHE_ADDR}" CACHE INTERNAL + "the address this module last applied, to tell its own value from one set externally") + +# How fastcache-cc is configured, in one place: the probe below must test the +# very environment the build will use, or it would vouch for a configuration +# nothing else runs. +set(_fc_fastcache_env + "FASTCACHE_ADDR=${FASTCACHE_ADDR}" + "FASTCACHE_SRCROOT=${CMAKE_SOURCE_DIR}" + "FASTCACHE_BUILDTREE=${CMAKE_BINARY_DIR}") + +# Ask fastcache-cc itself whether the cache works, by compiling one tiny +# translation unit through it with FASTCACHE_VERBOSE=1 and requiring a reported +# cache outcome. A launcher that cannot reach its daemon still compiles fine — +# it just runs the real compiler — so nothing but an end-to-end exchange tells +# "the cache works" apart from "every TU will silently pay a failed connect, +# with precompiled headers disabled for nothing and ccache passed over". +# +# The match is positive (HIT/MISS only): should the launcher's diagnostics ever +# be reworded, this reports unusable and the build falls back to the next +# launcher, which is the harmless direction to be wrong in. +# +# @param outVar Set to TRUE when the cache served the probe, FALSE otherwise. +# @param reasonVar Set to a short diagnostic when outVar is FALSE. +function(_fc_probe_fastcache_cc outVar reasonVar) + set(${outVar} FALSE PARENT_SCOPE) + + set(_dir "${CMAKE_BINARY_DIR}/CMakeFiles/fastcache-probe") + set(_src "${_dir}/probe.cpp") + # Both the file and its content are fixed, so the probe itself is a cache + # hit from the second configure onwards — which exercises FETCH rather than + # just STORE, and costs less than the first run. + file(WRITE "${_src}" "int fastcacheProbe() { return 0; }\n") + + if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + set(_args /nologo /c "${_src}" "/Fo${_dir}/probe.obj") + else() + set(_args -c "${_src}" -o "${_dir}/probe.o") + endif() + + # A probe that answers takes ~0.1s locally and little more over a LAN, so ten + # seconds is generous for a working daemon and a bounded wait for a broken + # one. The cap has to live here: FASTCACHE_TIMEOUT_MS bounds the launcher's + # send/recv but not its connect(), so an address that drops packets rather + # than refusing them — a firewall, a downed VPN, a host that is simply gone — + # stalls on the TCP connect timeout instead (measured: 2m30s), and every + # configure would pay it. + set(_timeoutSeconds 10) + + # NO_STATS keeps the probe out of `fastcache-cc --show-stats`, where it would + # read as a build that never hits. TIMEOUT_MS bounds a daemon that accepts + # the connection and then stalls; builds keep the launcher's own default. + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env + ${_fc_fastcache_env} + "FASTCACHE_VERBOSE=1" + "FASTCACHE_NO_STATS=1" + "FASTCACHE_TIMEOUT_MS=2000" + "${FASTCACHE_CC}" "${CMAKE_CXX_COMPILER}" ${_args} + WORKING_DIRECTORY "${_dir}" + TIMEOUT ${_timeoutSeconds} + RESULT_VARIABLE _rc + OUTPUT_QUIET + ERROR_VARIABLE _err) + + if(_rc MATCHES "[Tt]imeout") + set(${reasonVar} "no answer within ${_timeoutSeconds}s" PARENT_SCOPE) + elseif(NOT _rc EQUAL 0) + set(${reasonVar} "probe compile failed (${_rc})" PARENT_SCOPE) + elseif(_err MATCHES "fastcache-cc: (HIT|MISS) key=") + set(${outVar} TRUE PARENT_SCOPE) + set(${reasonVar} "" PARENT_SCOPE) + elseif(_err MATCHES "fastcache-cc: cache unavailable \\(([^)]*)\\)") + set(${reasonVar} "${CMAKE_MATCH_1}" PARENT_SCOPE) + else() + set(${reasonVar} "no cache outcome reported" PARENT_SCOPE) + endif() +endfunction() # Candidate table, most-preferred first. Each row is described by: # _fc_cache__label human-readable name for the status message # _fc_cache__program the found program (empty when not installed) # _fc_cache__requires extra condition; the row is skipped when falsy # _fc_cache__env NAME=VALUE pairs to inject around the invocation -# Supporting a fourth launcher is adding an id here plus its four variables. +# _fc_cache__check function deciding usability at configure time +# (empty when being installed is enough); called as +# ( ) and only for a row that +# already passed program and requires +# _fc_cache__detail extra words for the status message (empty for none) +# Supporting a fourth launcher is adding an id here plus its six variables. set(_fc_cache_candidates fastcache_cc sccache ccache) +# Render "