CMake

The CMake integration is for projects whose CMake model should remain the source of truth. cmakeProject() configures the project with Ninja and discovers every real CMake target (add_library/add_executable) as a separately selectable, separately buildable/testable child, keyed by its CMake target name.

Declare the project root

import { cmakeProject } from "//rules/c/cmake";
import { BUILD } from "//rules/workflows/build";
import { PACKAGE } from "//rules/workflows/package";
import { TEST } from "//rules/workflows/test";

const project = cmakeProject({
    cmakeArgs: ["-DCMAKE_BUILD_TYPE=Release"],
});

export const my_library = {
    [BUILD]: project.get("my_library", BUILD),
    [PACKAGE]: project.get("my_library", PACKAGE),
};
export const my_test = {
    [BUILD]: project.get("my_test", BUILD),
    [TEST]: { unit: project.get("my_test", TEST, "unit") },
};

path identifies the CMake source directory (defaults to the declaring BUILD.js's own directory). srcs controls the files staged from that directory, while dirs adds complete auxiliary directories needed by configure or build steps. cmakeArgs appends project-specific options to cmake -S -B.

Configure receives the full srcs input once. Before replay, imp runs a syntax-only Ninja dependency scan over C and C++ compiler edges. Ninja reads GCC depfiles and MSVC /showIncludes output, then replay gives each compiler edge its direct source and only the workspace headers that scan found. extraGlobs, dirs, generated CMake files, and declared deps stay inputs. Link and custom edges retain the full srcs input because Ninja does not state all files those commands can read.

If Ninja cannot produce a complete dependency record for one compiler edge, imp keeps that edge correct by using the former broad include-like input set. The action display marks this as a broad input fallback. CMake C++ module edges that use dyndep are not supported yet and fail with an explicit error when a selected target reaches one.

Use extraGlobs for compiler inputs with another suffix, such as generated metadata. Use deps for artifact handles that CMake must see at configure and replay time. The CMake arguments still define how CMake includes or links those artifacts.

cmakeProject() returns {get(cmakeTargetName, workflow, facet?), all(workflow, facet?)} — an expand(), not a plain object — so each selectable target must be re-exported at the BUILD.js top level wrapped in the usual {[BUILD]: ..., [PACKAGE]: ..., [TEST]: {...}} shape (a bare project.get(...) call is not itself a valid export). workflow is one of BUILD/PACKAGE/TEST (imported from imp:core); TEST's facet is always "unit".

The toolchain a CMake project builds with is gcc-only today — pass an explicit toolchain: gccGraphToolchain(version) (//rules/c/gcc) or rely on the declared gcc default. Zig-as-CMake-compiler is a known, deferred gap (zig's own graph toolchain has no named-cache-backed real path yet for CMake to bake CMAKE_C_COMPILER against).

The default gcc toolchain (//rules/c/gcc) is a Bootlin external toolchain whose compiler wrapper rejects any -I/-isystem/-L flag pointing under /usr/include or /usr/lib ("unsafe header/library path used in cross-compilation"), which blocks linking against host system packages (e.g. libwebkit2gtk-4.1 discovered via CMake's own pkg_check_modules). Pass unsafeSystemPaths: true to bypass that guard for this project — same toolchain sysroot, just without the check:

const project = cmakeProject({
    cmakeArgs: ["-DWEBVIEW_WEBKITGTK_MODULE_NAME=webkit2gtk-4.1"],
    unsafeSystemPaths: true,
});

Discovery and build execution

CMake configuration is deferred until the selected graph actually reaches the project, and runs at most once no matter how many targets get selected across however many goals (expand()'s own memoization — this is the entire reason the graph-native rule replaced the legacy, label-based one, which reconfigured on every single call). The generated Ninja graph is parsed for named libraries and executables; each becomes a keyed child. Executables referenced by add_test() get a [TEST] facet that scopes CTest to just their correlated case(s).

imp build //native/project:my_library
imp test //native/project:my_test

Build execution replays reachable Ninja edges as one coarse task per selected target (not one task per edge — see the module's own source comments for why), so an unrelated target's rebuild doesn't force this one's. CTest itself is always run rather than replaying a previous successful result.

Consuming a discovered target from raw ccLibrary()/ccBinary()

A discovered CMake target's project.get(name, BUILD) is a plain resolved graph handle — unlike a raw ccLibrary() result, it does not itself carry transitiveArchives/transitiveSharedLibs/transitiveIncludeDirs/transitiveLinkopts, so a bare project.get("mylib", BUILD) does not work directly as a deps entry. Wrap it with cmakeLibraryDep() instead:

import { cmakeLibraryDep, cmakeProject } from "//rules/c/cmake";
import { ccBinary } from "//rules/c";

const project = cmakeProject({ path: "third_party/mylib" });

export const app = ccBinary({
    srcs: ["main.c"],
    deps: [
        cmakeLibraryDep(project, "mylib", {
            includeDirs: ["third_party/mylib/include"],
        }),
    ],
});

includeDirs is supplied by the caller rather than auto-discovered: CMake's Ninja graph isn't parsed for per-target -I flags today, and even if it were, that data is only known once the CMake configure task has actually run — too late for ccTask()'s own compiler-flag construction, which needs plain strings synchronously at BUILD.js declare time. This is the same kind of manual knowledge a plain ccLibrary({hdrs}) glob already requires.

Shared library targets

If the CMake target is add_library(... SHARED ...), say so with shared: true:

cmakeLibraryDep(project, "mylib", {
    includeDirs: ["third_party/mylib/include"],
    shared: true,
});

That routes the artifact to transitiveSharedLibs instead of transitiveArchives — the same bucket ccLibrary({ shared: true }) uses, so both kinds of dep behave alike from a consumer's side. Without it a .so is reported as an archive, where a consumer's ar step can reach it.

shared is caller-supplied for the same reason includeDirs is: the CMake target's own type is known only once the configure task has run, while cmakeLibraryDep() must report the bucket synchronously at BUILD.js declare time.

A ccBinary() consuming a shared CMake dep both links and runs: its product becomes a directory holding the executable plus that library, linked with -Wl,-rpath,$ORIGIN, so LD_LIBRARY_PATH need not be set. See //rules/c's own docs for the product shape. //rules/c/cmake/example's uses_cmake_lib_test is the in-tree fixture that runs one.

If the CMake target is a shared library with its own shared-library dependencies (e.g. pkg-config-discovered libwebkit2gtk-4.1), the final consumer's own link step needs those flags too — the same unsafeSystemPaths escape hatch (above) only fixes this target's own compile/link, not what a downstream ccBinary()/odinPackage() needs to resolve it. Supply them via linkopts, for the same "not structurally discoverable" reason as includeDirs:

cmakeLibraryDep(project, "webview", {
    includeDirs: ["third_party/webview/include"],
    linkopts: ["-L/usr/lib/x86_64-linux-gnu", "-lwebkit2gtk-4.1", "-lgtk-3"],
});

These flow through as transitiveLinkopts — a ccBinary() consumer folds them into its own link step automatically, and an odinPackage() consumer needs unsafeSystemPaths: true of its own (see //rules/odin's own docs) to actually accept -L flags under /usr/lib on its own linker invocation.

Configuration

cmakeToolchain()

cmakeToolchain(version, opts = {})

Declare a CMake toolchain version and optionally set it as the default. matching lockfile entry (warns instead of failing). to use instead of the shipped one.

ParameterTypeDescription
versionstring
[opts]object
[opts.default=false]boolean
[opts.unverified=false]booleanAllow downloading without a
[opts.lockfile]stringAddress of a workspace-owned lockfile

Returns: object Target handle for this CMake toolchain.

Targets

cmakeProject()

cmakeProject(opts = {})

Public entry point for a graph-native CMake project — see cmakeProjectExpansion() for the returned {get, all} shape. for compiler edges, for inputs that do not use a standard header suffix. configure and replay. CMake arguments define how the project uses them.

ParameterTypeDescription
[opts]object
[opts.path]stringWorkspace-relative CMakeLists.txt directory. Defaults to the calling BUILD.js's own directory (".").
[opts.buildDir]stringBuild directory; defaults to build/<path>.
[opts.srcs]string[]Source glob CMake configure/replay depends on.
[opts.dirs]string[]Extra directories (e.g. vendored includes) to mount.
[opts.extraGlobs=[]]string[]Extra project-relative files to mount
[opts.deps=[]]ArrayArtifact handles to mount for CMake
[opts.cmakeArgs]string[]Extra cmake -S -B arguments.
[opts.toolchain]objectgccGraphToolchain() result, or the workspace default. zig isn't supported yet — see graph_replay.js's own docstring.
[opts.unsafeSystemPaths=false]booleanBypass Bootlin's toolchain-wrapper unsafe-path guard (which rejects -I/-isystem/-L flags under /usr/include or /usr/lib) so this project's compile/link steps can use host system packages (e.g. libwebkit2gtk-4.1). Same sysroot and hardening flags as normal, just without that one guard.

Returns: object {get(cmakeTargetName, workflow, facet?), all(workflow, facet?)}.

cmakeLibraryDep()

cmakeLibraryDep(project, name, opts = {})

Adapt a discovered CMake target for use as a raw ccLibrary()/ccBinary() deps entry. CMake's own per-target include paths aren't structurally discoverable today (see this module's own docstring) and expand().get() is resolved too late for ccTask()'s synchronous include-flag construction anyway, so includeDirs must be supplied by the caller.

ParameterTypeDescription
projectobjectA cmakeProject()/cmakeProjectExpansion() result.
namestringCMake target name (as passed to add_library/add_executable).
[opts]object
[opts.includeDirs=[]]string[]Include dirs downstream ccLibrary()/ccBinary() targets need, e.g. the CMake project's own public header directory.
[opts.linkopts=[]]string[]Link flags downstream targets need to resolve this target's own shared-library dependencies, e.g. pkg-config-derived -L/-l flags for a .so linked against host system packages. CMake's own per-target link flags aren't structurally discoverable any more than its include paths are (see includeDirs above) — supplied by the caller for the same reason.
[opts.shared=false]booleanDeclare that this CMake target is a shared library (add_library(... SHARED ...)), so its artifact travels as transitiveSharedLibs rather than transitiveArchives — the same bucket ccLibrary({shared: true}) uses. Caller-supplied for the same reason as includeDirs and linkopts: the target's own CMake type is known only after the configure task has run, while this function must report the bucket synchronously at BUILD.js declare time.

Returns: object {[BUILD], archive, transitiveArchives, transitiveSharedLibs, transitiveIncludeDirs, transitiveLinkopts} — usable directly as a ccLibrary()/ccBinary() deps entry.