Martin Chang · 2026-03-06
Who has debugged a CMake error that gave you no indication of where it came from?
...right.
CMake Error at CMakeLists.txt:276 (find_package):
By not providing "FindThreads.cmake" in CMAKE_MODULE_PATH this project has
asked CMake to find a package configuration file provided by "Threads",
but CMake did not find one.
Could not find a package configuration file provided by "Threads"...
Which line triggered this? Which file? What's the call stack?
You already know the answer.
...and we've accepted this too
function(foo)
message(STATUS "x = ${x}") # sees caller's x
endfunction()
set(x 42)
foo() # prints 42 - not a closure, not lexical
# the call stack IS the scope chain
No lexical scope. No closures. Variables bleed across call frames freely.
# VAR_NOT_EXIST is not set
if(NOT ${VAR_NOT_EXIST}) # despite CMake elises into if(NOT) and not syntax erroring
message("this should not run")
endif()
# expands to: if(NOT ) which CMake treats as if(NOT)
# which somehow is not a syntax error
Unset variables expand to empty string at parse time. Conditions are string-matched, not type-checked.
You meant if(NOT VAR_NOT_EXIST). These
are not the same.
set(varname "MY_VAR")
set(${varname} "hello") # sets MY_VAR at runtime
# variable names are arbitrary strings resolved at runtime
# no static analysis, no interning, no slot optimisation
set("a variable with spaces" "valid") # this works
Variable names are runtime strings. The interpreter cannot know what variables exist at parse time.
This is why a fast CMake interpreter is hard. Every lookup is a hashtable hit.
Compatibility means compatibility.
Your CMakeLists.txt relies on these
semantics whether you know it or not.
The goal is a better engine under the same contract - not a new language.
The bugs we fix are the ones CMake should have fixed itself.
It beat C++ on experience
cargo build
just works
A new C++ developer hitting CMake in 2026 is a recruitment problem for the entire ecosystem
It eats your existing CMakeLists.txt.
No changes required.
It is the build system. Not a generator.
That second sentence matters. We'll come back to it.
Configure → Generator → Build system
Three separate processes. Information is lost at each boundary.
Kiln: one process, one graph.
That's the constraint everything else follows from.
Error reporting · Bug detection · Speed
[WARN] This macro can only be used with libraries
[STATUS] Trantor using SSL library: None
[STATUS] c-ares found!
warning: Could not find "Threads": no FindThreads.cmake under CMAKE_MODULE_PATH and no
ThreadsConfig.cmake found. Set Threads_DIR or CMAKE_PREFIX_PATH accordingly.
--> /home/marty/Documents/trantor/CMakeLists.txt:276:1
|
276 | find_package(Threads)
| ^^^^^^^^^^^^^^^^^^^^^
File. Line. Column. First time, every time.
Yes I stole Rust's message format
[WARN] This macro can only be used with libraries
[STATUS] Trantor using SSL library: None
[STATUS] c-ares found!
[STATUS] Found module: /usr/share/cmake/Modules/FindThreads.cmake
error: FindThreads only works if either C or CXX language is enabled
--> /usr/share/cmake/Modules/FindThreads.cmake:129:3
|
129 | message(FATAL_ERROR "FindThreads only works if either C or CXX ...")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Call Stack (most recent call first):
/usr/share/cmake/Modules/FindThreads.cmake:122 (if)
/home/marty/Documents/trantor/CMakeLists.txt:276 (find_package)
Error: Interpretation error
-- Checking Performing Test HAVE_VISIBILITY_HIDDEN - Success
warning: Unbalanced generator expression (missing closing '>')
--> /home/marty/.../mariadb-server/cmake/install_macros.cmake:291:7
|
291 | $<$<BOOL:${VCPKG_INSTALLED_DIR}>:${VCPKG_INSTALLED_DIR}/...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: Accepting as-is - CMake treats generator expressions
as strings at parse time.
This is in MariaDB. It has been there. CMake never told anyone.
This is a real bug category, not a toy example.
# CMake's own documentation warns you:
It is tempting to use this command to avoid writing the list of
source files for a library or executable target. While this seems
to work, there is no way for CMake to generate a build system that
knows when a new source file has been added.
aux_source_directory(SOURCES src/)
add_executable(myapp ${SOURCES})
Because CMake is a generator, glob results are baked
in at configure time.
New file added? Manually
rerun CMake. Or forget and debug a missing symbol.
We cache and re-evaluate globs before every build.
New file in src/? It's in the next
build. No manual reconfigure.
This is the same reason ExternalProject graph
merging works.
We never handed control to
another process.
You can disregard CMake's own advice here.
NAS hardware (Intel 245K, not a workstation)
CMake: 15.2s · Kiln: 1.5s
10x. Reproduced with hyperfine.
Not including 7.0s CMake writes the actual build system
Benchmark 1: cmake -P benchmark/8queens.cmake
Time (mean ± σ): 2.491 s ± 0.016 s
Benchmark 2: kiln -P benchmark/8queens.cmake
Time (mean ± σ): 153.4 ms ± 3.6 ms
Summary: kiln ran 16.24x faster than cmake
Pure interpreter speed. Matters for large projects with heavy CMake scripting and agressively cached.
"So it's faster and has better errors."
"That's not why I built it."
CMake's ExternalProject_Add:
This is a generator limitation. CMake hands off to Ninja/Make and loses control.
[ 1/258] Configuring jsoncpp
[ 2/280] Ready jsoncpp <- injected into main graph
[ 3/280] Building src/main.cpp
[ 4/280] Building jsoncpp/src/lib_json/json_reader.cpp
...
ExternalProject's build graph is merged into the main graph
Shared parallelism. Verified dependencies. Errors at merge time, not link time.
This is structurally impossible in a generator-based system.
compile_commands.json by default,
always correct
(kiln) /home/marty/.../c-ares/CMakeLists.txt:3 CMAKE_MINIMUM_REQUIRED(VERSION 3.5.0)
(kiln) > break find_package
(kiln) > continue
(kiln) > backtrace
(kiln) > print CMAKE_THREAD_LIBS_INIT
break, step, next, watch, backtrace - full gdb-style debugger for CMake scripts
Differential fuzzing · Compatibility matrix · Real projects
The interpreter is fuzz-tested against CMake 4.0 output
Not bug-for-bug identical - but handles edge cases well enough
FindXXX.cmake? We just use CMake's own modules. Best compliance test possible.
Qt6 is harder than LLVM. Sprawling codebase, unusual genex usage. It works.
LLVM and Qt6 not tested there. Takes way too long.
CMake won the build system wars. That was genuinely hard. Credit where it's due.
But it won in 2010. The design reflects 2010 constraints.
The goal isn't to replace CMake overnight.
It's
to show what the next 15 years could look like.
Kiln becomes the thing
or
Kiln forces CMake to do better
Either way, C++ wins
One engineer. Spare time. If this is possible, imagine what a team could do.
-P)Things I don't have answers to yet
The long tail of CMake behaviour is very long.
Differential fuzzing catches the obvious cases.
Real
projects catch the rest.
Is 95% compatibility useful? 99%? Where does the cost curve go vertical?
Probably project-dependent. Which means the tracker is the only real answer.
Dynamic variable names make static analysis hard.
Dynamic scoping makes optimisation harder.
16x faster today. Is there a ceiling? Where is it?
Parallel interpretation of independent subtrees
might be possible.
Nobody has tried.
CMake policies exist to paper over breaking
changes
while maintaining backwards
compatibility.
Kiln is a clean break at the engine level.
Do
we inherit that debt?
Probably yes for real-world projects.
Probably
not for the semantics we actually care about.
Found only a handful of project that needs it .
That
might just mean I haven't found dark
daemon.
FetchContent is better than ExternalProject.
Cargo
is better than FetchContent.
Kiln can own the full build graph including
dependencies.
How far should it go? Meson
support?
Package identity. Version resolution. Hermetic
builds.
Each one is a rabbit hole.
vcpkg and Conan exist. Are they the right answer,
or
what we settled for? Or we go full Cargo and have
kiln.toml
CMake ships new versions. Kiln has to keep up.
The tracker tells me when I break. It doesn't fix it.
This is an open question about the project's
sustainability,
not just its architecture.
Which is why this talk exists.
Source coming soon · Apache 2.0
Martin Chang