Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
196 changes: 176 additions & 20 deletions cmake/CompileCache.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).
Expand All @@ -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 <id> is described by:
# _fc_cache_<id>_label human-readable name for the status message
# _fc_cache_<id>_program the found program (empty when not installed)
# _fc_cache_<id>_requires extra condition; the row is skipped when falsy
# _fc_cache_<id>_env NAME=VALUE pairs to inject around the invocation
# Supporting a fourth launcher is adding an id here plus its four variables.
# _fc_cache_<id>_check function deciding usability at configure time
# (empty when being installed is enough); called as
# <fn>(<outVar> <reasonVar>) and only for a row that
# already passed program and requires
# _fc_cache_<id>_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 "<label>[ <detail>]" for a row, so a launcher and where it points are
# named the same way whether it won or was passed over. Diagnosing a daemon that
# did not answer starts with knowing which address was tried.
# @param id Row id from _fc_cache_candidates.
# @param outVar Receives the rendered text.
function(_fc_cache_describe id outVar)
set(_text "${_fc_cache_${id}_label}")
if(_fc_cache_${id}_detail)
string(APPEND _text " ${_fc_cache_${id}_detail}")
endif()
set(${outVar} "${_text}" PARENT_SCOPE)
endfunction()

set(_fc_cache_fastcache_cc_label "fastcache-cc")
set(_fc_cache_fastcache_cc_program "${FASTCACHE_CC}")
set(_fc_cache_fastcache_cc_requires "${FASTCACHE_ADDR}")
set(_fc_cache_fastcache_cc_env
"FASTCACHE_ADDR=${FASTCACHE_ADDR}"
"FASTCACHE_SRCROOT=${CMAKE_SOURCE_DIR}"
"FASTCACHE_BUILDTREE=${CMAKE_BINARY_DIR}")
set(_fc_cache_fastcache_cc_env ${_fc_fastcache_env})
set(_fc_cache_fastcache_cc_check _fc_probe_fastcache_cc)
set(_fc_cache_fastcache_cc_detail "at ${FASTCACHE_ADDR}")

set(_fc_cache_sccache_label "sccache")
set(_fc_cache_sccache_program "${SCCACHE}")
set(_fc_cache_sccache_requires ON)
set(_fc_cache_sccache_env "")
set(_fc_cache_sccache_check "")
set(_fc_cache_sccache_detail "")

set(_fc_cache_ccache_label "ccache")
set(_fc_cache_ccache_program "${CCACHE}")
set(_fc_cache_ccache_requires ON)
set(_fc_cache_ccache_env "")
set(_fc_cache_ccache_check "")
set(_fc_cache_ccache_detail "")

set(_fc_cache_chosen "")
set(_fc_cache_rejected "")
if(USE_COMPILER_CACHE)
foreach(_id IN LISTS _fc_cache_candidates)
if(_fc_cache_${_id}_program AND _fc_cache_${_id}_requires)
set(_fc_cache_chosen "${_id}")
break()
if(NOT _fc_cache_${_id}_program OR NOT _fc_cache_${_id}_requires)
continue()
endif()
if(_fc_cache_${_id}_check)
cmake_language(CALL ${_fc_cache_${_id}_check} _fc_cache_usable _fc_cache_why_not)
if(NOT _fc_cache_usable)
# Remember why, so a fall-through to a slower launcher explains
# itself rather than looking like the faster one was never there.
_fc_cache_describe("${_id}" _fc_cache_desc)
list(APPEND _fc_cache_rejected "${_fc_cache_desc}: ${_fc_cache_why_not}")
continue()
endif()
endif()
set(_fc_cache_chosen "${_id}")
break()
endforeach()
endif()

# Say why a preferred launcher was passed over, whatever the outcome: falling
# through in silence looks exactly like it never being installed.
foreach(_rejection IN LISTS _fc_cache_rejected)
message(STATUS "[cache] Not using ${_rejection}")
endforeach()

if(_fc_cache_chosen)
set(_fc_cache_label "${_fc_cache_${_fc_cache_chosen}_label}")
set(_fc_cache_program "${_fc_cache_${_fc_cache_chosen}_program}")

# `cmake -E env NAME=VALUE ... <program>` is the only way to attach
Expand All @@ -94,7 +249,8 @@ if(_fc_cache_chosen)
set(_fc_cache_launcher "${_fc_cache_program}")
endif()

message(STATUS "[cache] Enabling ${_fc_cache_label} (${_fc_cache_program}) for C/C++ compilation")
_fc_cache_describe("${_fc_cache_chosen}" _fc_cache_desc)
message(STATUS "[cache] Enabling ${_fc_cache_desc} (${_fc_cache_program}) for C/C++ compilation")
set(CMAKE_C_COMPILER_LAUNCHER ${_fc_cache_launcher})
set(CMAKE_CXX_COMPILER_LAUNCHER ${_fc_cache_launcher})

Expand Down Expand Up @@ -137,9 +293,9 @@ else()

if(NOT USE_COMPILER_CACHE)
message(STATUS "[cache] Compiler caching disabled by USE_COMPILER_CACHE=OFF")
elseif(FASTCACHE_CC AND NOT FASTCACHE_ADDR)
message(STATUS "[cache] fastcache-cc found but FASTCACHE_ADDR is unset, and neither sccache nor "
"ccache was found; caching disabled (set FASTCACHE_ADDR=host:port to use fastcache-cc)")
elseif(_fc_cache_rejected)
message(STATUS "[cache] No other compiler-cache launcher found (sccache, ccache); caching disabled "
"(start a daemon with `fastcached` to cache through fastcache-cc)")
else()
message(STATUS "[cache] No compiler-cache launcher found (fastcache-cc, sccache, ccache); caching disabled")
endif()
Expand Down
29 changes: 25 additions & 4 deletions docs/tools/fastcache-cc.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,32 @@ cmake -S . -B build -G Ninja \

The fastcached build does this for itself: `cmake/CompileCache.cmake` picks
`fastcache-cc` up automatically whenever the binary is on `PATH` and a daemon
address is known — exported as `FASTCACHE_ADDR` or passed as
`-DFASTCACHE_ADDR=host:port` — and injects `FASTCACHE_SRCROOT` /
answers at `127.0.0.1:6674` — at any other daemon, local or remote, when
`FASTCACHE_ADDR` is exported, at `-DFASTCACHE_ADDR=host:port` ahead of even that,
nowhere if it is set empty — and injects `FASTCACHE_SRCROOT` /
`FASTCACHE_BUILDTREE` from the source and binary directories, so those two need
not be exported. Without an address it falls back to `sccache`;
`-DUSE_COMPILER_CACHE=OFF` disables both.
not be exported.

Exporting `FASTCACHE_ADDR` retargets an existing build tree on its next
configure, rather than being frozen at whatever the first configure saw, which is
what ordinary cache semantics would do to it. A `-DFASTCACHE_ADDR=` passed on the
current run still wins over the environment — including the empty value that opts
out — since it is the more deliberate of the two.

"Answers" is checked, not assumed: configure compiles one tiny translation unit
through the launcher with `FASTCACHE_VERBOSE=1` and accepts only a reported
`HIT`/`MISS`, since a launcher whose daemon is down still compiles fine and would
otherwise leave every TU paying a failed connect with nothing to show for it.
That costs about 0.1 s against a daemon that answers, runs on every configure so
that starting the daemon and reconfiguring is enough, and any other outcome —
`connect failed`, a version mismatch, no daemon at all — falls back to `sccache`
naming the address it tried; `-DUSE_COMPILER_CACHE=OFF` disables both.

The probe carries its own ten-second cap, which is what bounds a remote address
that drops packets rather than refusing them: `FASTCACHE_TIMEOUT_MS` bounds each
send and receive, not the TCP `connect()`, so a firewalled or vanished host
otherwise takes the kernel's connect timeout to fail (measured at 2m30s on
macOS) — once per configure here, but once per translation unit in a build.

## Environment

Expand Down
Loading