From 437090ff6c8a02da61aeedd8bc30487c9f9ac1fd Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Tue, 18 Aug 2026 10:45:41 +0300 Subject: [PATCH] cmake: wire up fastcache-cc / sccache / ccache compiler-cache selection Vendors cmake/CompileCache.cmake from LASTRADA-Software/fastcached (self-contained, no dependency on that project) and includes it before any fetched dependency, so both morph's own targets and the FetchContent-fetched glaze get compiler-cache coverage. Selection order: fastcache-cc when a fastcached daemon answers (probed end-to-end with a throwaway translation unit, not just presence on PATH), else sccache, else ccache, else no cache. Off entirely with -DUSE_COMPILER_CACHE=OFF; the daemon address defaults to 127.0.0.1:6674 and is overridable via FASTCACHE_ADDR. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 7 + cmake/CompileCache.cmake | 302 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 cmake/CompileCache.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d4f077c3..4396f004 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,6 +109,13 @@ endif() include(cmake/compiler_options.cmake) +# Compiler-cache launcher selection: fastcache-cc (when a fastcached daemon +# answers) -> sccache -> ccache -> none. Included before any target or fetched +# dependency (FetchContent glaze below) so those get cached too. See +# cmake/CompileCache.cmake for the full selection logic; it declares its own +# USE_COMPILER_CACHE/FASTCACHE_ADDR options. +include(cmake/CompileCache.cmake) + # ── clang-tidy integration ───────────────────────────────────────────────── if(MORPH_BUILD_CLANG_TIDY) find_program(CLANG_TIDY_BIN clang-tidy REQUIRED) diff --git a/cmake/CompileCache.cmake b/cmake/CompileCache.cmake new file mode 100644 index 00000000..4dd60236 --- /dev/null +++ b/cmake/CompileCache.cmake @@ -0,0 +1,302 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Compiler-cache launcher selection. +# +# Three launchers are supported, in preference order: +# +# 1. fastcache-cc — the launcher from the fastcached project. Its entries are +# portable across checkout paths, so a CI runner and a developer working +# 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; 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. +# +# Launchers are wired in as compiler launchers, so CPM-/FetchContent-fetched +# dependencies get cached too. If a launcher is already set (e.g. via the +# command line or a preset), it is left untouched. +# +# To disable entirely: -DUSE_COMPILER_CACHE=OFF. + +option(USE_COMPILER_CACHE + "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). +# Check both C and CXX: a toolchain may set only one of them, and we must not +# override either (nor silently set the other alongside it). +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 a fastcached daemon to be used") +find_program(SCCACHE sccache DOC "sccache tool path") +find_program(CCACHE ccache DOC "ccache tool path") + +# 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 +# _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 "