CMake Builds skill for Codex

Target-based CMake that configures the same everywhere. Codex audits CMakeLists for directory-wide legacy commands with a bundled script, then moves requirements onto targets with the right PUBLIC, PRIVATE or INTERFACE scope. Builds run from committed presets, dependencies come from one mechanism with pinned versions, and libraries ship install and export rules that are proven by a throwaway consumer calling find_package.

  • Languages
  • $cmake-builds
  • 223 lines
  • 3 files
  • ~2,350 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- cmake-builds

Installs to ~/.agents/skills/cmake-builds/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $cmake-builds, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and repairs modern target-based CMake - target_link_libraries with PUBLIC/PRIVATE/INTERFACE, CMakePresets.json, find_package and FetchContent, vcpkg or Conan toolchains, install and export rules for libraries, and CMake 4 minimum-version breakage - and audits legacy CMakeLists for global flags. Use when a CMake build fails to configure, a dependency will not link, CMakeLists.txt uses include_directories or CMAKE_CXX_FLAGS, or a library needs to be consumable with find_package.

Use it when

  • A CMake configure or link step fails
  • CMakeLists uses include_directories, add_definitions or CMAKE_CXX_FLAGS
  • Adding a dependency via find_package, FetchContent, vcpkg or Conan
  • Setting up CMakePresets.json for local and CI builds
  • Packaging a C or C++ library for downstream consumers

Use something else when

  • The issue is C++ code style or ownership, not the build: use cpp-modern
  • The project builds with Make or Task runners only: use makefiles-taskfiles
  • You are configuring a microcontroller toolchain via PlatformIO: use platformio

How to check the result

A fresh configure with the CI preset is warning-free, ctest passes, the legacy audit script reports nothing new, and a scratch consumer can find_package the installed library.

The mistake it prevents

Marking a dependency PRIVATE while its headers are included from your public headers, so your own build passes and every consumer fails.

~/.agents/skills/cmake-builds/SKILL.md

--- name: cmake-builds description: Writes and repairs modern target-based CMake - target_link_libraries with PUBLIC/PRIVATE/INTERFACE, CMakePresets.json, find_package and FetchContent,… license: MIT ---

A modern CMake project describes targets and their usage requirements; nothing is set globally that one target needs. Every developer and CI job configures with the same CMakePresets.json, dependencies come from one mechanism, out-of-source builds are the only kind, and a library installs a package config so consumers just call find_package.

Before changing anything

  1. Read AGENTS.md, the top-level CMakeLists.txt, CMakePresets.json / CMakeUserPresets.json, vcpkg.json or conanfile.*, and the CI build script.
  2. cmake --version locally and in CI. The cmake_minimum_required line plus the installed version decide available commands.
  3. Audit the existing files for legacy patterns: python3 scripts/cmake_legacy_audit.py . (stdlib only; lists file:line findings and exits 1 if any exist). Fix the ones in files you are touching; do not rewrite the whole build in a feature change.
  4. Configure into a fresh build directory to reproduce the failure. Never debug against a stale CMakeCache.txt; delete only the build directory you created.

Project skeleton

cmake_minimum_required(VERSION 3.25...3.31)
project(acme VERSION 1.4.0 LANGUAGES CXX)

option(ACME_BUILD_TESTS "Build tests" ${PROJECT_IS_TOP_LEVEL})

add_library(acme_core
  src/parser.cpp
  src/store.cpp)
add_library(acme::core ALIAS acme_core)

target_compile_features(acme_core PUBLIC cxx_std_20)
target_include_directories(acme_core
  PUBLIC  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
          $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
  PRIVATE src)

find_package(fmt CONFIG REQUIRED)
target_link_libraries(acme_core PUBLIC fmt::fmt PRIVATE Threads::Threads)

add_executable(acme_cli app/main.cpp)
target_link_libraries(acme_cli PRIVATE acme::core)

if(ACME_BUILD_TESTS)
  enable_testing()
  add_subdirectory(tests)
endif()
  • VERSION min...max sets policies up to max while accepting older CMake down to min.
  • Use PROJECT_IS_TOP_LEVEL (3.21+) so the project does not build tests or install rules when consumed via add_subdirectory/FetchContent.
  • Always link through namespaced ALIAS / imported targets (fmt::fmt): a typo is a configure error instead of a silent -lfmtt at link time.
  • List sources explicitly. file(GLOB) does not notice added files without a re-configure; if the repo already uses it, add CONFIGURE_DEPENDS at minimum.

PUBLIC, PRIVATE, INTERFACE

KeywordThe requirement is used byExample
PRIVATEthis target onlya .cpp includes <sqlite3.h>
INTERFACEconsumers onlyheader-only library include dir
PUBLICthis target and consumersa public header includes <fmt/format.h>

The rule: if a dependency appears in your public headers, it is PUBLIC; otherwise PRIVATE. Over-using PUBLIC leaks include paths and slows builds; under-using it gives consumers “header not found”.

Legacy to modern

LegacyModern
include_directories(x)target_include_directories(tgt PRIVATE x)
link_directories + target_link_libraries(tgt foo)find_package(Foo) + target_link_libraries(tgt PRIVATE Foo::Foo)
add_definitions(-DFOO)target_compile_definitions(tgt PRIVATE FOO)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")target_compile_options(tgt PRIVATE -Wall) or a warnings INTERFACE target
set(CMAKE_CXX_STANDARD 20) at top (acceptable)target_compile_features(tgt PUBLIC cxx_std_20) for libraries
${FOO_INCLUDE_DIRS} / ${FOO_LIBRARIES} variablesimported targets Foo::Foo
ExternalProject_Add for a source depFetchContent_MakeAvailable or a package manager

Shared warning flags without globals:

add_library(acme_warnings INTERFACE)
target_compile_options(acme_warnings INTERFACE
  $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Wpedantic -Wconversion>
  $<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->)
target_link_libraries(acme_core PRIVATE acme_warnings)

Use CMAKE_COMPILE_WARNING_AS_ERROR (3.24+) from a CI preset rather than hard-coding -Werror, so consumers with newer compilers are not broken.

Presets

{
  "version": 6,
  "configurePresets": [
    {
      "name": "dev",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "ci",
      "inherits": "dev",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "RelWithDebInfo",
        "CMAKE_COMPILE_WARNING_AS_ERROR": "ON"
      }
    },
    {
      "name": "asan",
      "inherits": "dev",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer"
      }
    }
  ],
  "buildPresets": [ { "name": "dev", "configurePreset": "dev" }, { "name": "ci", "configurePreset": "ci" } ],
  "testPresets": [
    { "name": "dev", "configurePreset": "dev", "output": { "outputOnFailure": true } },
    { "name": "ci", "configurePreset": "ci", "output": { "outputOnFailure": true } }
  ],
  "workflowPresets": [
    { "name": "ci", "steps": [
      { "type": "configure", "name": "ci" }, { "type": "build", "name": "ci" }, { "type": "test", "name": "ci" } ] }
  ]
}
cmake --preset dev && cmake --build --preset dev && ctest --preset dev
cmake --workflow --preset ci          # configure, build, test in one command (3.25+)

CMakePresets.json is committed; personal tweaks go in the git-ignored CMakeUserPresets.json. Setting CMAKE_CXX_FLAGS from a preset (as in asan) is fine: it is a per-build choice, not something baked into targets.

Dependencies: pick one mechanism

SituationChoose
Already a vcpkg or Conan projectkeep it; add the dep to vcpkg.json / conanfile
Few small deps, no package managerFetchContent with FIND_PACKAGE_ARGS so a system package wins when present
System libraries (OpenSSL, zlib, Threads)find_package with the imported target
Large deps (Boost, Qt, LLVM)package manager or system install, not FetchContent
include(FetchContent)
FetchContent_Declare(Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG v3.7.1                 # pin a tag or a commit hash, never a branch
  FIND_PACKAGE_ARGS 3)           # 3.24+: try find_package(Catch2 3) first
FetchContent_MakeAvailable(Catch2)

vcpkg manifest mode: set "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" in the configure preset. Conan 2: conan install . --build=missing -s build_type=Debug generates conan_toolchain.cmake and presets you include. Fetching dependencies needs network access; ask before the first configure in a sandbox without it.

Installing and exporting a library

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

install(TARGETS acme_core EXPORT acmeTargets)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT acmeTargets NAMESPACE acme:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/acme)

configure_package_config_file(cmake/acmeConfig.cmake.in
  ${CMAKE_CURRENT_BINARY_DIR}/acmeConfig.cmake
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/acme)
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/acmeConfigVersion.cmake
  COMPATIBILITY SameMajorVersion)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/acmeConfig.cmake
              ${CMAKE_CURRENT_BINARY_DIR}/acmeConfigVersion.cmake
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/acme)

acmeConfig.cmake.in must call find_dependency(fmt) for every PUBLIC dependency, then include("${CMAKE_CURRENT_LIST_DIR}/acmeTargets.cmake"). Set EXPORT_NAME core on the target so consumers see acme::core.

Common failures

SymptomCause and fix
Compatibility with CMake < 3.5 has been removed (CMake 4.x)a dependency’s old cmake_minimum_required; upgrade the dep, or configure with -DCMAKE_POLICY_VERSION_MINIMUM=3.5 as a temporary workaround
Could not find a package configuration file provided by "Foo"set CMAKE_PREFIX_PATH to the install prefix or use the package manager toolchain; do not hard-code paths in CMakeLists
Header found when building, missing for consumersdependency is PRIVATE but used in a public header; make it PUBLIC
Undefined references only in static buildstransitive deps not propagated; link them with the right keyword, check link order is not hand-managed
Changes to options ignoredstale cache; reconfigure in a fresh preset binary dir
Works with Make, fails with Ninjamissing dependency edge, often a generated header without add_custom_command OUTPUT listed in sources

Verification

  • Fresh configure from a clean preset binary dir succeeds with no warnings (cmake --preset ci --log-level=WARNING).
  • Build and ctest --preset ci pass.
  • python3 scripts/cmake_legacy_audit.py <changed dirs> reports nothing new.
  • For libraries: cmake --install build/ci --prefix /tmp/acme-install, then a throwaway consumer project with find_package(acme CONFIG REQUIRED) and -DCMAKE_PREFIX_PATH=/tmp/acme-install configures and links.
  • compile_commands.json is generated for tooling (clangd, clang-tidy).

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "CMake Builds"
  short_description: "Target-based CMake that configures the same everywhere"
  default_prompt: "$cmake-builds Modernise this CMakeLists.txt and get rid of the global include_directories"
scripts/cmake_legacy_audit.py116 lines, 4.9 KB
#!/usr/bin/env python3
"""Audit CMakeLists.txt and *.cmake files for legacy, directory-scoped patterns.

Usage: python3 cmake_legacy_audit.py [PATH ...]   (default: current directory)
Prints file:line: CODE message for each finding. Exits 1 when findings exist,
0 when clean, 2 on usage errors. Build trees, _deps and .git are skipped.
"""
import os
import re
import sys

SKIP_DIRS = {".git", "_deps", "node_modules", "vcpkg_installed", ".cache"}
SKIP_PREFIXES = ("build", "cmake-build-", "out")
CMD = re.compile(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(")
KEYWORDS = {"PUBLIC", "PRIVATE", "INTERFACE", "LINK_PUBLIC", "LINK_PRIVATE", "LINK_INTERFACE_LIBRARIES"}

SIMPLE = {
    "include_directories": ("C001", "include_directories is directory-wide; use target_include_directories"),
    "link_directories": ("C002", "link_directories hides where libraries come from; link imported targets"),
    "add_definitions": ("C003", "add_definitions is directory-wide; use target_compile_definitions"),
    "add_compile_options": ("C004", "add_compile_options is directory-wide; prefer target_compile_options or an INTERFACE warnings target"),
    "link_libraries": ("C005", "link_libraries is directory-wide; use target_link_libraries"),
}


def strip_comments(text):
    out = []
    for line in text.splitlines():
        in_str = False
        cut = len(line)
        for i, ch in enumerate(line):
            if ch == '"' and (i == 0 or line[i - 1] != "\\"):
                in_str = not in_str
            elif ch == "#" and not in_str:
                cut = i
                break
        out.append(line[:cut])
    return "\n".join(out)


def commands(text):
    """Yield (name_lower, args_string, line_number) for each command invocation."""
    pos = 0
    while True:
        m = CMD.search(text, pos)
        if not m:
            return
        depth, i = 1, m.end()
        while i < len(text) and depth:
            if text[i] == "(":
                depth += 1
            elif text[i] == ")":
                depth -= 1
            i += 1
        yield m.group(1).lower(), text[m.end():i - 1], text.count("\n", 0, m.start()) + 1
        pos = i


def audit_file(path):
    with open(path, encoding="utf-8", errors="replace") as fh:
        text = strip_comments(fh.read())
    found = []
    for name, args, line in commands(text):
        tokens = args.split()
        if name in SIMPLE:
            found.append((line, *SIMPLE[name]))
        elif name == "set" and tokens and re.fullmatch(r"CMAKE_(C|CXX)_FLAGS(_[A-Z]+)?", tokens[0]):
            found.append((line, "C010", f"setting {tokens[0]} in CMakeLists affects every target; use target_compile_options (presets/toolchains may set it)"))
        elif name == "file" and tokens and tokens[0] in ("GLOB", "GLOB_RECURSE") and "CONFIGURE_DEPENDS" not in tokens:
            found.append((line, "C011", "file(GLOB) without CONFIGURE_DEPENDS misses new files until reconfigure; list sources explicitly"))
        elif name == "target_link_libraries" and len(tokens) > 1 and not (KEYWORDS & set(tokens[1:])):
            found.append((line, "C012", f"target_link_libraries({tokens[0]} ...) without PUBLIC/PRIVATE/INTERFACE"))
        elif name == "cmake_minimum_required":
            m = re.search(r"VERSION\s+(\d+)\.(\d+)", args)
            if m:
                ver = (int(m.group(1)), int(m.group(2)))
                if ver < (3, 5):
                    found.append((line, "C020", "minimum below 3.5 fails to configure on CMake 4.x"))
                elif ver < (3, 16):
                    found.append((line, "C021", f"minimum {ver[0]}.{ver[1]} enables old policies; raise it (e.g. 3.25...3.31)"))
        if re.search(r"\$\{\w+_(LIBRARIES|INCLUDE_DIRS?)\}", args):
            found.append((line, "C030", "uses Find-module result variables; link the imported target (Foo::Foo) instead"))
        if "${CMAKE_SOURCE_DIR}" in args or "${CMAKE_BINARY_DIR}" in args:
            found.append((line, "C031", "CMAKE_SOURCE_DIR/BINARY_DIR break when this project is a subproject; use PROJECT_SOURCE_DIR or CMAKE_CURRENT_SOURCE_DIR"))
    return found


def cmake_files(root):
    if os.path.isfile(root):
        yield root
        return
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(SKIP_PREFIXES)]
        for f in filenames:
            if f == "CMakeLists.txt" or f.endswith(".cmake"):
                yield os.path.join(dirpath, f)


def main(argv):
    roots = argv[1:] or ["."]
    total = files = 0
    for root in roots:
        if not os.path.exists(root):
            print(f"no such path: {root}", file=sys.stderr)
            return 2
        for path in cmake_files(root):
            files += 1
            for line, code, msg in audit_file(path):
                print(f"{path}:{line}: {code} {msg}")
                total += 1
    print(f"{total} finding(s) in {files} CMake file(s)", file=sys.stderr)
    return 1 if total else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem