why cmake tools
If you’re a C++ developer, you’ve probably drowned in the abyss of "build hell": scratching your head over nested Makefile syntax, sifting through hundreds of lines to modify a single compile flag; getting tangled in CMake’s target_link_libraries PUBLIC/PRIVATE logic, where writing a simple library configuration file takes longer than the business code itself; or daring to try Bazel, Blade, or Xmake, only to find each tool has its own "jargon system" that demands hours of learning—let alone the ultimate production nightmare: spending half a month troubleshooting dependencies, debugging compile flags, and barely getting a third-party library to work, only to face the "works on my machine" curse. Must building a C++ project require becoming a "build tool expert" first?
It wasn’t until I reexamined the problem through the lens of first principles that I realized we’re all held hostage by tools: what we need is never "mastering CMake," but "quickly setting up runnable, integrable, and deployable projects"—and the efficiency to avoid wasting half a month on a single library. Thus, kmcmake was born: a CMake framework designed around CMake’s underlying workflow, taming complex logic into a 5-minute setup with zero intrusion. Its core mission? End the absurdity of "spending half a month compiling one library" while letting you understand the "why" behind it, not just blindly using it.
First, Understand CMake’s Underlying Workflow (The Foundation of First Principles)
First principles boil down to "returning to fundamentals and eliminating redundancy." For CMake, its essence is an "organizer of cross-platform build processes"—it doesn’t compile code directly, but generates platform-specific build files (Makefiles, Visual Studio solutions, etc.) in fixed phases. To simplify building, you must first dissect these three core phases, understanding the responsibilities and operational logic of each:
Phase 1: Configuration Phase – "Find Stuff + Define Rules"
- Core Tasks: Parse CMakeLists.txt, locate dependent libraries, set compile options, define targets (libraries/executables), and configure installation rules.
- Key Operations:
- Dependency Discovery: Use
find_package,find_library, orfind_pathto locate third-party libraries (e.g., spdlog, Protobuf), determining library and header file paths. - Target Definition: Create targets with
add_library/add_executable, set include directories withtarget_include_directories, and link dependencies withtarget_link_libraries. - Variable Configuration: Set core variables like
CMAKE_BUILD_TYPE(Debug/Release),CMAKE_INSTALL_PREFIX(installation root), andCMAKE_CXX_STANDARD(C++ standard). - Conditional Logic: Use
if-elseto adapt to different platforms (Linux/macOS/Windows) and compilers (GCC/Clang/MSVC).
- Dependency Discovery: Use
- Production Pitfalls: I once spent 2 days manually setting
CMAKE_PREFIX_PATH,PROTOBUF_INCLUDE_DIR, andPROTOBUF_LIBRARYto locate a custom-installed Protobuf library. Another time, confusing CMake’sCONFIGandMODULEmodes forfind_packagecaused CI to fail, taking a week to diagnose.
Phase 2: Generation Phase – "Write Files"
- Core Tasks: Generate platform-specific build files (e.g., Makefiles for Linux, Xcode projects for macOS, VS solutions for Windows) based on rules defined in the configuration phase.
- Key Operations:
- Auto-generate Compile Commands: Convert flags from
target_compile_optionsintoCXXFLAGSin Makefiles. - Generate Dependency Graphs: Translate
target_link_librariesdependencies into link commands in Makefiles, handling static/shared library dependency propagation. - Generate Installation Rules: Convert
installdirectives intoinstalltargets in Makefiles, defining paths for headers, libraries, and executables.
- Auto-generate Compile Commands: Convert flags from
- Production Pitfalls: When writing
installrules in raw CMake, I ignored platform differences inCMAKE_INSTALL_LIBDIR(library install directory) andCMAKE_INSTALL_INCLUDEDIR(header install directory). This worked on Linux but misaligned paths on macOS, leaving third-party projects unable to find headers.
Phase 3: Build/Install Phase – "Execute Commands"
- Core Tasks: Invoke compilers (GCC/Clang/MSVC) to compile code, link libraries, and generate final targets (libraries/executables); copy files to specified directories during
make install. - Key Operations:
- Compilation: Execute compile commands from generated build files to turn source code (.cc/.cpp) into object files (.o/.obj).
- Linking: Link object files and dependencies into static libraries (.a/.lib), shared libraries (.so/.dll), or executables.
- Installation: Copy libraries, headers, and executables to paths specified by
CMAKE_INSTALL_PREFIX(e.g.,/usr/local/lib,/usr/local/include).
- Production Pitfalls: Misconfiguring
PUBLIC/PRIVATEintarget_link_librariesonce broke dependency propagation for a shared library. My library compiled fine, but third-party projects linking to it failed to find spdlog—turns out I’d usedPRIVATEinstead ofPUBLIC, preventing spdlog’s link information from being passed along.
These phases are interdependent; a mistake in any step leads to build failure. First principles teach us: Build tools should be servants, not teachers. We shouldn’t need to learn their syntax—we just need to tell them "what library I want, what code to compile, and where to deploy it," and let them handle the rest. Every design decision in kmcmake adheres strictly to CMake’s underlying workflow, with "precision encapsulation" at each phase. It preserves CMake’s native logic while simplifying complex operations—turning CMake from a "must-learn skill" into an "invisible implementation detail," and integrating a library from a "half-month project" into a "5-minute task."
7 Core Goals: How kmcmake Implements First Principles on CMake’s Workflow
My core requirements are clear—end frustration and boost efficiency. kmcmake is a "direct translation" of these goals into CMake’s three phases, with every feature mapped to specific CMake operations, no ambiguity:
1. 5-Minute Project Setup: Modular Encapsulation in the Configuration Phase
Raw CMake requires writing dozens of lines for basic library configuration: add_library, target_include_directories, target_link_libraries, install, plus handling static/shared library toggles, aliases, and dependency propagation. For example, creating a project supporting both static and shared libraries in raw CMake needs:
# Raw CMake: Static + Shared Library + Installation + Alias (50+ lines)
option(BUILD_SHARED_LIBS "Build shared library" ON)
add_library(my_lib src/my_lib.cc)
add_library(my_lib::my_lib ALIAS my_lib)
target_include_directories(my_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(my_lib PRIVATE spdlog::spdlog)
target_compile_features(my_lib PUBLIC cxx_std_17)
install(TARGETS my_lib
EXPORT my_lib_targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT my_lib_targets
FILE my_libTargets.cmake
NAMESPACE my_lib::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/my_lib
)
Adding compile options or conditional logic only increases complexity. kmcmake’s modular encapsulation condenses all this into one macro during the configuration phase. You just specify "target name, sources, include paths, dependencies"—kmcmake handles the rest:
# kmcmake: One macro for all configuration phase operations (5-minute setup)
kmcmake_cc_library(
NAME my_lib
SOURCES src/my_lib.cc
INCLUDES include
LINKS spdlog::spdlog
CXXOPTS -std=c++17
)
- Underlying Logic: During configuration, kmcmake automatically:
- Calls
add_libraryand supportsBUILD_SHARED_LIBSfor static/shared toggling. - Sets
target_include_directorieswithBUILD_INTERFACEandINSTALL_INTERFACE—no manual generator expressions. - Creates
{NAME}::{NAME}aliases for consistent target referencing. - Adds
installrules followingGNUInstallDirs—no need to manageCMAKE_INSTALL_LIBDIR. - Smartly propagates dependencies:
LINKSautomatically distinguishesPUBLIC/PRIVATE(private for library dependencies, public for toolchain dependencies).
- Calls
I once spent 3 hours writing and debugging 50+ lines of raw CMake for a static/shared library project. With kmcmake, it’s done in 5 minutes—no more generator expression mistakes or path misalignments.
2. Seamless CI Integration: Cross-Platform Auto-Adaptation in the Configuration Phase
The biggest CI/CD pain is "cross-platform configuration adaptation"—differences in dependency paths and compile options across Unix-like systems require endless if-else in raw CMake. For example, adapting compile options for GCC and Clang:
# Raw CMake: Cross-platform compile option adaptation (verbose + error-prone)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
target_compile_options(my_lib PRIVATE -Wall -Wextra -Werror)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(my_lib PRIVATE -Wall -Wextra -Werror -Wno-gnu-zero-variadic-macro-arguments)
endif()
# Dependency path adaptation
if(UNIX AND NOT APPLE)
set(PROTOBUF_LIBRARY_PATH /usr/lib/x86_64-linux-gnu)
elseif(APPLE)
set(PROTOBUF_LIBRARY_PATH /usr/local/lib)
endif()
find_library(PROTOBUF_LIBRARY protobuf PATHS ${PROTOBUF_LIBRARY_PATH})
kmcmake’s cross-platform auto-adaptation directly hooks into CMake’s platform-detection APIs during configuration:
- Auto-detects
CMAKE_CXX_COMPILER_IDand embeds common flags for GCC/Clang—no manualif-else. - Follows
GNUInstallDirsand prioritizesfind_package’sCONFIGmode (falling back toMODULE), letting CMake handle cross-platform path differences natively. - Embeds test macros like
kmcmake_cc_testthat auto-register test targets during configuration—just add-DKMCMAKE_BUILD_TEST=ONin CI, no manualadd_test.
I once spent a week adapting protoc paths and compile options for a Protobuf-dependent project in CI. With kmcmake, the CI script is simplified to:
# Configure + Build + Test + Install—no extra steps
cmake --preset default -DKMCMAKE_BUILD_TEST=ON
cmake --build build
ctest --test-dir build
cmake --install build
No more cross-platform headaches—kmcmake encapsulates all adaptation logic in the configuration phase.
3. Effortless Deployment: Rule Automation in Generation + Installation Phases
Deployment success means "third-party projects can find your library." This requires generating Config.cmake during the generation phase and copying files correctly during installation. Raw CMake needs:
# Raw CMake: Generate Config.cmake (verbose + error-prone)
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
my_libConfigVersion.cmake
VERSION 1.0.0
COMPATIBILITY SameMajorVersion
)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/my_libConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/my_libConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/my_lib
PATH_VARS CMAKE_INSTALL_INCLUDEDIR CMAKE_INSTALL_LIBDIR
)
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/my_libConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/my_libConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/my_lib
)
You also need to manually create a my_libConfig.cmake.in template—one mistake breaks find_package for third parties. kmcmake’s solution:
- Generation Phase: Automatically calls
write_basic_package_version_fileandconfigure_package_config_fileto generateConfig.cmakeandConfigVersion.cmake—no manual templates. - Installation Phase: Auto-installs config files to
${CMAKE_INSTALL_LIBDIR}/cmake/{NAME}, ensuring third parties can find your library viafind_package. - Underlying Logic: kmcmake extracts target include paths, dependencies, and compile options during generation, writing them to config files. When third parties call
find_package, CMake loads this information for "one-click linking."
I once spent hours debugging a library where users couldn’t link to it—turns out I’d messed up Config.cmake. With kmcmake, make install is all you need; third parties just use find_package(my_lib REQUIRED) to link, no more "library not found" issues.
4. No Hidden Rules: Transparent Logic in the Configuration Phase
Most CMake newbies struggle with "configuration phase execution order" and "variable scope": find_package must come before add_library, PUBLIC/PRIVATE in target_include_directories must be correctly used—these "hidden rules" are daunting. kmcmake’s "transparent logic" follows CMake’s native execution order with no secrets:
- Clear Parameters:
INCLUDESmaps totarget_include_directories(PUBLIC),PINCLUDEStoPRIVATE;LINKStotarget_link_libraries(PUBLIC),PLINKStoPRIVATE—no ambiguity. - Fixed Execution Order: kmcmake macros first run
find_package(if dependencies exist), thenadd_library, and finallytarget_*commands—aligning perfectly with CMake’s native flow. - Clear Error Messages: If a dependency is missing, kmcmake directly prints "Could not find XXX—please install or set XXX path" instead of failing silently, simplifying debugging.
I once wasted hours troubleshooting a find_package failure—turns out I’d misconfigured CMAKE_INSTALL_PREFIX. These hidden rules are impossible for newbies to guess, but kmcmake makes all logic explicit: configure intuitively, and it works.
5. Unix-like System Compatibility: Standard Adaption in Configuration + Generation Phases
Compatibility issues on Unix-like systems stem from differences in "dependency discovery" (configuration phase) and "path rules" (generation phase). kmcmake’s solution:
- Configuration Phase: Auto-includes
GNUInstallDirs, using standard variables likeCMAKE_INSTALL_LIBDIRandCMAKE_INSTALL_INCLUDEDIR—no manual path setting. - Generation Phase: Generated Makefiles follow Unix-like compile standards, installing libraries to
/usr/local/liband headers to/usr/local/include—matching default third-party library search paths. - Dependency Discovery: Prioritizes
find_package’s standard modes and supportsCMAKE_PREFIX_PATH, eliminating manual dependency path setting on Unix-like systems.
I once wrote two sets of if-else in raw CMake to adapt dependency paths for Linux and macOS. With kmcmake, one configuration works for both—no system-specific logic needed.
6. Zero Intrusion: Native Compatibility in the Configuration Phase
kmcmake’s core is "encapsulation, not replacement"—it’s fully compatible with raw CMake directives. You can extend configurations with native CMake after kmcmake macros without breaking existing logic:
# kmcmake macro + raw CMake: zero-intrusion extension
kmcmake_cc_library(
NAME my_lib
SOURCES src/my_lib.cc
INCLUDES include
)
# Raw CMake extension: add custom compile options (runs during configuration)
target_compile_options(my_lib PRIVATE -Wno-unused-parameter)
# Raw CMake extension: add conditional logic (runs during configuration)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
- Underlying Logic: kmcmake creates standard CMake targets (e.g.,
my_lib) that can be modified with subsequent rawtarget_*commands—fully aligning with CMake’s target management.
I once used a framework that forced source code into a src directory, requiring a week of project restructuring. kmcmake imposes no directory structure rules—it doesn’t hold your project hostage; add or modify as needed.
7. No New Tools: A Wrapper Layer on CMake’s Native Workflow
First principles emphasize "avoiding redundancy": CMake is an industry standard with a mature ecosystem supporting all Unix-like systems. Building a new build tool is unnecessary (time-consuming, labor-intensive, and adoption-challenged). kmcmake positions itself as a "wrapper layer" for CMake, with all features built on its three core phases:
- It doesn’t modify CMake’s native workflow—only automates repetitive
target_*andinstallcommands during configuration. - It requires no extra tools—just CMake 3.21+—no additional binary installations.
- It doesn’t break CMake’s ecosystem—supports all libraries discoverable via
find_packageand all native CMake variables/directives.
You don’t need to understand chip design to use a phone, and you don’t need to master CMake’s underlying workflow to use kmcmake—but if you want to, every feature maps to specific CMake operations, fully credible and transparent.
Conclusion: First Principles + CMake Workflow = Simplified Building
We find C++ project building cumbersome because we confuse "tool syntax" with "core needs"—we don’t need to "master CMake’s three phases and all directives," but to "avoid spending half a month on a single library"; we don’t need to "grasp complex concepts like generator expressions and dependency propagation," but to "set up projects in 5 minutes, integrate with CI seamlessly, and deploy effortlessly."
First principles help us cut through the noise and return to CMake’s workflow essence: modularizing and automating complex operations at each phase, so developers focus on "end goals" instead of "phase details." kmcmake doesn’t invent new build logic—it translates CMake’s three core phases into simple, usable interfaces via first principles. Every macro corresponds to a series of standard CMake operations in configuration, generation, and installation—precise, transparent, and unambiguous.
Now, you no longer need to spend weeks learning CMake, struggle with Makefile syntax, worry about toolchain migration costs, or waste half a month integrating a library—kmcmake handles it all. You can focus on writing great business code.
After all, we’re C++ developers, not "build tool engineers." Let tools return to their essence (automating repetitive work via underlying workflows), and let us return to ours (focusing on business logic)—that’s the true power of first principles.
If you’re tired of build tool chaos and the pain of spending half a month compiling one library, give kmcmake a try—5-minute setup, zero learning curve, and C++ project building as easy as breathing, with full transparency into the "why" behind it.