Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

cmakefmt

CI Pages Coverage

A blazing-fast, workflow-first CMake formatter — built in Rust, built to last.

cmakefmt was born from frustration with cmake-format, the beloved-but-aging Python tool from the cmakelang project. Instead of patching around its limits, cmakefmt starts from scratch: a native Rust binary that respects your time, your CI budget, and your build system.

Same spirit. No Python. No compromises.

Crates.io package name: cmakefmt-rust (CLI binary remains cmakefmt). This project is independent from other Rust implementations, including azais-corentin/cmakefmt and yamadapc/cmakefmt.

Why cmakefmt?

  • 20× faster — not a typo. cmakefmt hits a 20.69x geometric-mean speedup over cmake-format on real-world CMake corpora. Pre-commit hooks that once made you wince now finish before you blink.
  • Zero dependencies. One binary. No Python environment, no virtualenv bootstrap, no “works on my machine” dependency drift. Drop it in CI and forget about it.
  • Built for actual workflows. --check, --diff, --staged, --changed, --files-from, --show-config, --explain-config, JSON reporting — the power-user features that cmake-format made you script around are all first- class citizens here.
  • Knows your commands. Teach cmakefmt about your project’s custom CMake functions and macros. No more generic token wrapping for functions you wrote.
  • Errors that actually help. Parse and config failures come with file/line context, source snippets, and reproduction hints — not a wall of opaque parser noise.
  • Designed for real repositories. Comment preservation, disabled regions, config discovery, ignore files, Git-aware file selection, and opt-in parallelism are core features, not afterthoughts.

Performance Snapshot

The numbers speak for themselves:

MetricSignal
Geometric-mean speedup vs cmake-format20.69x
End-to-end format_source, large synthetic input (1000+ lines)estimate 8.8248 ms (95% CI 8.8018–8.8519 ms)
Parser-only, large synthetic inputestimate 7.1067 ms (95% CI 7.0793–7.1359 ms)
Serial whole-corpus batch (220 files)184.5 ms ± 1.3 ms
--parallel 8 whole-corpus batch48.5 ms ± 1.5 ms

95% CI is the Criterion-reported confidence interval: the range within which the true mean is expected to fall 95% of the time.

Fast enough for local dev, pre-commit hooks, editor integrations, and CI — all at once. The only question is: why settle for slower?

Curious about methodology? Evaluating whether it’s worth switching? Read Performance and Migration From cmake-format.

Quick Start

Install from this repository:

cargo install --path .

Dump a starter config:

cmakefmt --dump-config > .cmakefmt.yaml

Check your entire repository without touching a single file:

cmakefmt --check .

Rewrite everything in place:

cmakefmt --in-place .

Format only the CMake files you’re about to commit:

cmakefmt --staged --check

What cmakefmt Covers Today

  • recursive discovery of CMakeLists.txt, *.cmake, and *.cmake.in
  • in-place formatting, stdout formatting, --check, --diff, and JSON reports
  • YAML or TOML config files, with YAML preferred for larger user configs
  • custom command specs and per-command formatting overrides
  • comment preservation, fence/barrier passthrough, and markup-aware handling
  • config introspection with --show-config, --show-config-path, and --explain-config
  • Git-aware workflows: --staged, --changed, and --since
  • rich debug output for discovery, config resolution, barriers, command forms, and layout choices
  • opt-in parallel execution for multi-file runs
  • a built-in command registry audited through CMake 4.3.1

Suggested Reading Order

New here?

  1. Install
  2. Coverage
  3. CLI Reference
  4. Config Reference
  5. Formatter Behavior

Migrating from cmake-format?

  1. Migration From cmake-format
  2. Coverage
  3. Config Reference
  4. Troubleshooting

Embedding cmakefmt as a library?

  1. Library API
  2. Architecture

Common Workflows

Preview which files would change before touching anything:

cmakefmt --list-changed-files .

See the exact patch instead of applying it:

cmakefmt --diff CMakeLists.txt

Trace which config file a target will actually use:

cmakefmt --show-config-path src/CMakeLists.txt

Inspect the fully resolved, effective config:

cmakefmt --show-config src/CMakeLists.txt

Current Status

The repository is stable and actively maintained. cmakefmt is still pre-1.0, and active development is focused on:

  • polishing documentation and the onboarding experience
  • tightening release and distribution channels
  • staying well ahead of cmake-format on performance and workflow ergonomics

Hit something unexpected? Start with Troubleshooting, then reach for --debug and --explain-config before filing a bug report.

Install

Get cmakefmt running, wire it into your project, and never think about CMake formatting again.

Current Installation Options

This repository is stable and actively maintained. Today, the supported install paths are repository-based:

  • build from source with cargo build --release
  • install from this checkout with cargo install --path .

First-party package-manager distribution is still being rolled out. Until then, Cargo is the fastest path to a working binary.

Support Levels

The release plan separates channels into explicit support levels so users know what to trust:

ChannelPlanned support levelNotes
cargo install cmakefmt-rustOfficially maintainedThe reference install path for developers already using Rust.
GitHub Releases binariesOfficially maintainedNative binaries for Linux, macOS, and Windows.
Docs site / CLI referenceOfficially maintainedShould stay in lock-step with each tagged release.
Homebrew / winget / ScoopOfficially maintainedPlanned first-party package-manager channels.
Additional package managers (npm, AUR, Nix, containers, etc.)Automated or best-effortUseful channels, but not the first rollout priority.

Until tagged distribution channels land, repository-based installs remain the fully supported path.

Build From This Repository

git clone <this-repo>
cd cmake-format-rust
cargo build --release
./target/release/cmakefmt --help

This is the right path if you are actively developing cmakefmt, reviewing changes, or benchmarking local modifications.

Install With Cargo

cargo install --path .

Verify the binary is on your path:

cmakefmt --version
cmakefmt --help

You can also inspect release-oriented helper outputs directly from the built binary:

cmakefmt --generate-completion bash > cmakefmt.bash
cmakefmt --generate-man-page > cmakefmt.1

Those outputs are intended for packaging and release artifacts, but they are also useful for local shell setup.

First Project Setup

Dump a starter config into your repo root:

cmakefmt --dump-config > .cmakefmt.yaml

Why YAML by default?

  • it is easier to read for larger custom-command specs
  • it is the recommended user-facing format for cmakefmt
  • --dump-config toml still exists if you prefer TOML

Do a dry run — check your whole project without rewriting a single file:

cmakefmt --check .

When you are happy with what you see, apply the formatting:

cmakefmt --in-place .

Typical Local Workflow

The four commands you will reach for every day:

cmakefmt --check .
cmakefmt --in-place .
cmakefmt --verify CMakeLists.txt
cmakefmt --cache --check .
cmakefmt --require-pragma --check .
cmakefmt --staged --check
cmakefmt --changed --since origin/main --check

What each one does:

  • --check .: CI-safe validation for a repository or directory
  • --in-place .: rewrite all discovered CMake files, with semantic verification by default
  • --verify CMakeLists.txt: do a safe stdout-format run when you want the extra parse-tree check
  • --cache --check .: speed up repeated whole-repo checks when your config is stable
  • --require-pragma --check .: roll formatting out gradually, only touching opted-in files
  • --staged --check: pre-commit guard — only touches staged files
  • --changed --since origin/main --check: PR-scoped check for branch-only changes

Pre-commit

The repository ships a pre-commit configuration out of the box. Install both commit and pre-push hooks:

pre-commit install
pre-commit install --hook-type pre-push

Useful spot checks:

pre-commit run --all-files
cmakefmt --staged --check

The shipped hook set covers code-quality checks and REUSE/license metadata validation — worth installing early in any contributor workflow.

CI-Friendly Shell Usage

The simplest CI baseline:

cmakefmt --check .

For quieter CI logs:

cmakefmt --check --quiet .

For machine-readable output that scripts or dashboards can consume:

cmakefmt --check --report-format json .

Editor And Stdin Workflows

Many editor integrations pipe a buffer through stdin rather than passing a real file path. Use --stdin-path to give config discovery and diagnostics the on-disk context they need:

cat src/CMakeLists.txt | cmakefmt - --stdin-path src/CMakeLists.txt

This is also the right pattern for ad-hoc scripts and custom editor commands.

Config Bootstrap Tips

If your project uses many custom CMake functions or macros:

  • start from --dump-config
  • keep the file as .cmakefmt.yaml
  • define command syntax under commands:
  • use per_command_overrides: only for layout and style tweaks

If you are debugging config discovery:

cmakefmt --show-config-path src/CMakeLists.txt
cmakefmt --show-config src/CMakeLists.txt
cmakefmt --explain-config

Local Docs Preview

Preview the published docs locally with mdbook:

mdbook serve docs

Then open the local URL that mdbook prints.

Upgrade And Uninstall

Upgrade a local source install

git pull --ff-only
cargo install --path . --force

Remove a Cargo-installed binary

cargo uninstall cmakefmt-rust

Pin a specific release in CI later

Once release tags exist, prefer explicit version pins:

cargo install cmakefmt-rust --version <tagged-version>

The release docs and release notes will also publish SHA-256 sums for release artifacts so non-Cargo installs can verify downloads.

Troubleshooting Install Issues

cmakefmt is not found after cargo install

Make sure Cargo’s install bin directory is on your PATH.

The formatter is using the wrong config

cmakefmt --show-config-path path/to/CMakeLists.txt
cmakefmt --explain-config

A hook or script only sees stdin and ignores my project config

Pass --stdin-path with the buffer’s real project-relative path.

I want TOML instead of YAML

cmakefmt --dump-config toml > .cmakefmt.toml

YAML is simply the recommended default for larger configs.

Coverage

cmakefmt treats coverage as a contributor tool, not as a vanity number.

The goal is simple: make it obvious which parts of the formatter are exercised by the default test suite, and make it easy to inspect the results locally and in CI.

What The Coverage Workflow Runs

GitHub Actions runs coverage with cargo llvm-cov on the default test suite:

cargo llvm-cov clean --workspace
cargo llvm-cov --workspace --all-targets --summary-only
cargo llvm-cov report --workspace --all-targets --html

That means coverage includes:

  • library code under src/
  • the CLI binary under src/main.rs
  • unit tests
  • integration tests

The workflow publishes:

  • a text summary in the GitHub Actions job summary
  • the raw summary as an artifact
  • an HTML report as an artifact for line-by-line inspection

What Coverage Is Not Trying To Measure

Coverage is helpful, but it is not the whole quality story. cmakefmt still leans heavily on:

  • snapshot tests for formatter behavior
  • idempotency checks
  • real-world corpus tests
  • performance benchmarks

High coverage with weak corpus coverage would still be a bad trade.

Local Coverage

Install cargo-llvm-cov once:

cargo install cargo-llvm-cov

Then run coverage locally:

cargo llvm-cov clean --workspace
cargo llvm-cov --workspace --all-targets --summary-only
cargo llvm-cov report --workspace --all-targets --html

The HTML report is written under target/llvm-cov/html/.

Reading The Results

When coverage changes, pay attention to where the delta lands:

  • parser and formatter core paths matter more than trivial getters
  • config discovery and CLI integration matter because they are user-facing
  • new CLI features should ship with direct integration coverage
  • performance-sensitive hot paths should still keep behavior tests around them

In short: coverage is a guide to missing tests, not a substitute for good test design.

Release Channels

This page answers three practical questions:

  1. what the first public release means
  2. which install channels are official
  3. what ships with a release

Release Contract

The first public release should mean:

  • the formatter is fast enough and complete enough for real project use
  • Linux, macOS, and Windows are first-class supported platforms
  • the CLI, config surface, and diagnostics are intentionally designed rather than experimental
  • formatting output may still change between early releases when bugs are fixed or layouts are improved

In other words: usable now, but not yet promising 1.0-level formatting stability.

Support Levels

cmakefmt distinguishes between channels that are part of the core release contract and channels that are convenient but lower-priority in the initial release rollout.

ChannelSupport levelWhat to expect
GitHub Releases binariesOfficially maintainedRelease artifacts and checksums published for supported platforms.
cargo install cmakefmt-rustOfficially maintainedCurated crates.io package with the same source tree used for releases.
Documentation siteOfficially maintainedUpdated as part of the tagged release.
Homebrew / winget / ScoopOfficially maintainedThese are the first package-manager targets after GitHub Releases and crates.io.
Additional package managers / wrappersBest effortUseful distribution channels, but not all are blockers for the initial release.

Planned Release Artifacts

Each tagged release is expected to ship:

  • cmakefmt binaries for supported platforms
  • SHA256SUMS
  • a curated source package on crates.io
  • release notes with installation examples
  • shell completions generated from the same CLI metadata as --help
  • a generated man page for packagers and Unix-like installs

You can preview the packaging helper outputs from a local build:

cmakefmt --generate-completion bash > cmakefmt.bash
cmakefmt --generate-completion zsh > _cmakefmt
cmakefmt --generate-man-page > cmakefmt.1

Version Output

cmakefmt --version reports the package version, and local development builds also include a short Git commit when available.

That keeps local binaries identifiable without forcing Git metadata into published release packages.

Early-Release Stability Expectations

Before 1.0, formatting behavior may still change between releases. The goal is to keep those changes understandable and intentional:

  • bug fixes that make output more obviously correct are expected
  • formatter churn should be documented in the changelog
  • teams should pin an explicit released version in CI if output stability matters

Release notes and support policy updates are published with each tagged release and reflected in the project changelog.

Performance

cmakefmt is fast enough that you never have to think twice about running it — in local workflows, editor integrations, pre-commit hooks, or CI. That is not an accident. Speed is a design goal, not a side effect.

Current Benchmark Signal

The headline numbers from the current local benchmark set:

MetricCurrent local signal
Geometric-mean speedup vs cmake-format20.69x
Parser-only, large synthetic input (1000+ lines)estimate 7.1067 ms (95% CI 7.0793–7.1359 ms)
Formatter-only from parsed AST, large synthetic inputestimate 1.7545 ms (95% CI 1.7425–1.7739 ms)
End-to-end format_source, large synthetic inputestimate 8.8248 ms (95% CI 8.8018–8.8519 ms)
Debug/barrier-heavy formattingestimate 313.98 µs (95% CI 311.89–317.54 µs)

All Criterion estimates show a point estimate with a 95% confidence interval — the range within which the true mean is expected to fall 95% of the time. “Large synthetic input” refers to a 1000+ line stress-test CMakeLists.txt generated for benchmarking purposes. “AST” (Abstract Syntax Tree) is the structured in-memory representation produced by parsing, before formatting.

Real-World Comparison

The current local corpus comparison measured cmakefmt against cmake-format on real CMakeLists.txt files drawn from projects including:

  • Abseil
  • Catch2
  • CLI11
  • GoogleTest
  • ggml
  • llama.cpp
  • MariaDB Server
  • LLVM
  • Qt
  • nlohmann/json
  • protobuf
  • spdlog

Fetch the pinned local corpus before rerunning those comparisons:

python3 scripts/fetch-real-world-corpus.py

Results across that corpus:

  • cmakefmt was faster on every single fixture
  • speedup ranged from 10.91x to 48.49x
  • geometric-mean speedup: 20.69x

Parallel Batch Throughput

Multi-file runs are single-threaded by default, but opt-in parallelism scales well:

ModeTime
serial184.5 ms ± 1.3 ms
--parallel 2111.5 ms ± 11.9 ms
--parallel 464.7 ms ± 1.1 ms
--parallel 848.5 ms ± 1.5 ms

Peak RSS (Resident Set Size — the RAM physically held in memory by the process) rises from 13.2 MB (serial) to 20.7 MB (--parallel 8) on this batch. That is why the tool defaults to single-threaded execution unless you explicitly ask for more.

Large Repository Parallelism Survey

Phase 12 validation was also run against oomph-lib (local checkout with 612 discovered CMake files):

ModeTime
serial412.5 ms ± 9.0 ms
--parallel 2296.0 ms ± 3.5 ms
--parallel 4191.8 ms ± 4.7 ms
--parallel 8152.5 ms ± 2.8 ms

That corresponds to a 2.71x speedup at --parallel 8 versus serial, with peak RSS moving from 11.3 MB to 17.0 MB.

For a direct tool baseline on the same full oomph-lib tree (612 discovered files), /usr/bin/time -l measured:

  • cmake-format (sequential over discovered files): 45.69 s real
  • cmakefmt serial: 0.47 s real (~97x faster)
  • cmakefmt --parallel 8: 0.19 s real (~240x faster)

What The Numbers Mean In Practice

The headline numbers matter not as abstract benchmarks, but because they change what feels viable:

  • repository-wide --check in CI — comfortable
  • pre-commit hooks on staged files — instant
  • repeated local formatting during development — no delay you will notice
  • editor-triggered format-on-save — faster than the save dialog

Benchmark Environment

Current headline measurements were captured on:

  • macOS 26.3.1
  • aarch64-apple-darwin
  • 10 logical CPUs
  • rustc 1.94.1
  • hyperfine 1.20.0
  • cmake-format 0.6.13

Exact numbers vary by machine. What matters release to release is that relative trends stay strong and regressions are caught quickly.

How To Reproduce

Run the formatter benchmark suite:

cargo bench --bench formatter

Save a baseline before a risky change:

cargo bench --bench formatter -- --save-baseline before-change

Compare a later run against that baseline:

cargo bench --bench formatter -- --baseline before-change

CLI Reference

The complete reference for everything cmakefmt can do from the command line. If you just want to get up and running, start with Install first and come back here when you want the full picture.

Synopsis

cmakefmt [OPTIONS] [FILES]...

The Four Main Ways To Run cmakefmt

PatternWhat it does
cmakefmt CMakeLists.txtFormat one file to stdout.
cmakefmt dir/Recursively discover CMake files under that directory.
cmakefmtRecursively discover CMake files under the current working directory.
cmakefmt -Read one file from stdin and write formatted output to stdout.

How Input Selection Works

One rule governs everything:

  • direct file arguments always win

If you pass a file path explicitly, cmakefmt processes it even if an ignore file or regex would have excluded it during discovery.

Ignore rules only affect:

  • directory discovery
  • --files-from
  • --staged
  • --changed

Input Selection Flags

FlagMeaning
--files-from <PATH>Read more input paths from a file, or - for stdin. Accepts newline-delimited or NUL-delimited path lists.
--path-regex <REGEX>Filter discovered CMake paths. Direct file arguments are not filtered out.
--ignore-path <PATH>Add extra ignore files during recursive discovery. Direct file arguments still win.
--no-gitignoreStop honoring .gitignore during recursive discovery.
--stagedUse staged Git-tracked files instead of explicit input paths.
--changedUse modified Git-tracked files instead of explicit input paths.
--since <REF>Choose the Git base ref used by --changed. Without it, HEAD is the base.
--stdin-path <PATH>Give stdin formatting a virtual on-disk path for config discovery and diagnostics.
--lines <START:END>Restrict formatting to one or more inclusive 1-based line ranges on a single target.

Output Mode Flags

FlagMeaning
-i, --in-placeRewrite files on disk instead of printing formatted output.
--checkExit with code 1 when any selected file would change.
--list-changed-filesPrint only the files that would change after formatting.
--list-input-filesPrint the selected input files after discovery and filtering, without formatting them.
--diffPrint a unified diff instead of the full formatted output.
--report-format <human|json|github|checkstyle|junit|sarif>Switch between human output and CI/editor-friendly machine reporters.
--colour <auto|always|never>Highlight changed formatted output lines in cyan. auto only colors terminal output.

Execution Flags

FlagMeaning
--debugEmit discovery, config, barrier, and formatter diagnostics to stderr.
--quietSuppress per-file human output and keep only summaries plus actual errors.
--keep-goingContinue processing later files after a file-level parse/format error.
--required-version <VERSION>Refuse to run unless the current cmakefmt version matches exactly. Useful for pinned CI and editor wrappers.
--verifyParse the original and formatted output and reject the result if the CMake semantics change.
--fastSkip semantic verification, including the default rewrite-time verification used by --in-place.
--cacheCache formatted file results for repeated runs on the same files.
--cache-location <PATH>Override the cache directory. Supplying it also enables caching.
--cache-strategy <metadata|content>Choose whether cache invalidation tracks file metadata or file contents.
--require-pragmaFormat only files that opt in with a # cmakefmt: enable style pragma.
-j, --parallel [JOBS]Enable parallel file processing when explicitly requested. If no value is given, use the available CPU count.
--progress-barShow a progress bar on stderr during --in-place multi-file runs.

Config And Conversion Flags

FlagMeaning
--dump-config [FORMAT]Print a starter config template and exit. Defaults to YAML; pass toml for TOML.
--generate-completion <SHELL>Print shell completions for packaging or local shell setup.
--generate-man-pagePrint a roff man page for packagers and Unix-like installs.
--show-config [FORMAT]Print the effective config for a single target and exit. Defaults to YAML; pass toml for TOML.
--show-config-pathPrint the selected config file path for a single target and exit. --find-config-path is an alias.
--explain-configExplain config resolution for a single target, or for the current working directory when no explicit file is given.
--convert-legacy-config <PATH>Convert a legacy cmake-format JSON/YAML/Python config file to .cmakefmt.toml on stdout.

Config Override Flags

FlagMeaning
-c, --config-file <PATH>Use one or more specific config files instead of config discovery. Later files override earlier ones. --config remains a compatibility alias.
--no-configIgnore discovered config files and explicit --config-file entries. Only built-in defaults plus CLI overrides remain.
-l, --line-width <N>Override format.line_width.
--tab-size <N>Override format.tab_size.
--command-case <lower|upper|unchanged>Override style.command_case.
--keyword-case <lower|upper|unchanged>Override style.keyword_case.
--dangle-parens <true|false>Override format.dangle_parens.

Exit Codes

  • 0: success
  • 1: --check or --list-changed-files found files that would change
  • 2: parse, config, regex, or I/O error

Common Examples

Format One File To Stdout

cmakefmt CMakeLists.txt

Prints the formatted file to stdout. The file on disk is untouched.

Rewrite Files In Place

cmakefmt --in-place .

The “apply formatting now” mode. Every discovered CMake file gets rewritten. In-place rewrites also verify parse-tree stability by default; use --fast only when you are intentionally trading that extra safety check for throughput.

Verify A Dry Run Semantically

cmakefmt --verify CMakeLists.txt

This keeps stdout output, but also reparses both the original and formatted source and rejects the result if the parsed CMake structure changes.

Use --check In CI

cmakefmt --check .

Typical human-mode output:

would reformat src/foo/CMakeLists.txt
would reformat cmake/Toolchain.cmake

summary: selected=12 changed=2 unchanged=10 failed=0

Exit code 0 means nothing would change. Exit code 1 means at least one file is out of format — exactly what CI needs.

If your CI system prefers structured annotations or standard interchange formats, switch reporters:

cmakefmt --check --report-format json .
cmakefmt --check --report-format github .
cmakefmt --check --report-format checkstyle .
cmakefmt --check --report-format junit .
cmakefmt --check --report-format sarif .

Pin The Formatter Version In Automation

cmakefmt --required-version 0.0.1 --check .

This makes shell scripts and editor wrappers fail fast when the installed binary is not the exact version the workflow expects.

List Only The Files That Would Change

cmakefmt --list-changed-files --path-regex 'cmake|toolchain' .

Typical output:

cmake/Toolchain.cmake
cmake/Warnings.cmake

Useful for editor integration, scripts, and review tooling that needs a precise list without actually reformatting anything.

List The Selected Input Files Without Formatting Them

cmakefmt --list-input-files --path-regex 'cmake|toolchain' .

Typical output:

cmake/Toolchain.cmake
cmake/Warnings.cmake
cmake/modules/CompilerOptions.cmake

This is the pure discovery mode. It walks the file tree, applies ignore files and filters, then prints the selected CMake inputs without parsing or formatting them.

Show The Actual Patch

cmakefmt --diff CMakeLists.txt

Typical output:

--- CMakeLists.txt
+++ CMakeLists.txt.formatted
@@
-target_link_libraries(foo PUBLIC bar baz)
+target_link_libraries(
+  foo
+  PUBLIC
+    bar
+    baz)

Quiet CI Output

cmakefmt --check --quiet .

Typical effect:

summary: selected=48 changed=3 unchanged=45 failed=0

A clean log with a reliable exit code — ideal for high-volume CI pipelines.

Cache Repeated Runs

cmakefmt --cache --check .
cmakefmt --cache-location .cache/cmakefmt --cache-strategy content --check .

Use metadata-based invalidation for speed or content-based invalidation when you want the cache to ignore timestamp-only churn.

Roll Out Formatting Gradually

cmakefmt --require-pragma --check .

Then opt individual files in with a short marker:

# cmakefmt: enable

cmakefmt also accepts # fmt: enable and # cmake-format: enable as equivalent opt-in pragmas.

Continue Past Bad Files

cmakefmt --check --keep-going .

Typical effect:

error: failed to parse cmake/generated.cmake:...
error: failed to read vendor/missing.cmake:...

summary: selected=48 changed=3 unchanged=43 failed=2

Without --keep-going, the run stops at the first file-level error.

Format Only Staged Files

cmakefmt --staged --check

The easiest pre-commit or pre-push workflow — only touches files that are already part of the current Git change.

Format Only Changed Files Since A Ref

cmakefmt --changed --since origin/main --check

Perfect for PR workflows. Checks only “what this branch changed” rather than the entire repository.

Feed Paths From Another Tool

git diff --name-only --diff-filter=ACMR origin/main...HEAD | \
  cmakefmt --files-from - --check

--files-from accepts newline-delimited or NUL-delimited path lists, so it composites cleanly with any tool that can emit file paths.

Stdin With Correct Config Discovery

cat src/CMakeLists.txt | cmakefmt - --stdin-path src/CMakeLists.txt

Without --stdin-path, stdin formatting has no on-disk context for config discovery or path-sensitive diagnostics.

Partial Formatting For Editor Workflows

cmakefmt --stdin-path src/CMakeLists.txt --lines 10:25 -

Use this when an editor wants to format only a selected line range instead of rewriting the whole buffer.

See Which Config Was Selected

cmakefmt --show-config-path src/CMakeLists.txt

Typical output:

/path/to/project/.cmakefmt.yaml

Inspect The Effective Config

cmakefmt --show-config src/CMakeLists.txt
cmakefmt --show-config=toml src/CMakeLists.txt

Prints the fully resolved config after discovery plus any CLI overrides. No more guessing what the formatter is actually using.

Explain Config Resolution

cmakefmt --explain-config

Typical output includes:

  • the target path being resolved
  • config files considered
  • config file selected
  • CLI overrides applied

Generate A Starter Config

cmakefmt --dump-config > .cmakefmt.yaml
cmakefmt --dump-config toml > .cmakefmt.toml

YAML is the default because it is easier to maintain once you start defining larger custom command specs.

Convert An Old cmake-format Config

cmakefmt --convert-legacy-config .cmake-format.py > .cmakefmt.toml

The fastest path through a legacy config migration.

Discovery Precedence And Filtering Rules

  • Direct file arguments are always processed, even if an ignore rule would skip them.
  • Recursive discovery honors .cmakefmtignore and, by default, .gitignore.
  • --ignore-path adds more ignore files for discovered directories only.
  • --files-from, --staged, and --changed still pass through normal discovery filters when they produce directories or paths that need filtering.
  • --show-config-path, --show-config, and --explain-config resolve a single target context and make the selected config path(s) visible.
  • --no-config disables config discovery entirely.

Diagnostic Quality

For parse and config failures, cmakefmt prints:

  • the file path
  • line and column information
  • source context
  • likely-cause hints when possible
  • a repro hint using --debug --check

When formatting results surprise you rather than hard-failing, reach for --debug first.

Config Reference

Everything you need to know to tune cmakefmt for your project.

The short version:

  • user config may be YAML or TOML
  • YAML is the recommended default for hand-edited configs
  • custom command syntax goes under commands:
  • command-specific layout and style tweaks go under per_command_overrides:

Config Discovery Order

For a given target file, cmakefmt resolves config in this order:

  1. repeated --config-file <PATH> files, if provided
  2. the nearest .cmakefmt.yaml, .cmakefmt.yml, or .cmakefmt.toml found by walking upward from the target
  3. ~/.cmakefmt.yaml, ~/.cmakefmt.yml, or ~/.cmakefmt.toml
  4. built-in defaults

If multiple supported config filenames exist in the same directory, YAML is preferred over TOML.

When you want to see exactly what happened:

cmakefmt --show-config-path src/CMakeLists.txt
cmakefmt --show-config src/CMakeLists.txt
cmakefmt --explain-config

YAML is the recommended user-facing format:

format:
  line_width: 80
  tab_size: 2

style:
  command_case: lower
  keyword_case: upper

Generate the full starter template with:

cmakefmt --dump-config > .cmakefmt.yaml

If you prefer TOML:

cmakefmt --dump-config toml > .cmakefmt.toml

Table Of Contents

Defaults

format:
  line_width: 80
  tab_size: 2
  use_tabs: false
  max_empty_lines: 1
  max_hanging_wrap_lines: 2
  max_hanging_wrap_positional_args: 6
  max_hanging_wrap_groups: 2
  dangle_parens: false
  dangle_align: prefix
  min_prefix_length: 4
  max_prefix_length: 10
  space_before_control_paren: false
  space_before_definition_paren: false

style:
  command_case: lower
  keyword_case: upper

markup:
  enable_markup: true
  reflow_comments: false
  first_comment_is_literal: true
  literal_comment_pattern: ""
  bullet_char: "*"
  enum_char: "."
  fence_pattern: "^\\s*[`~]{3}[^`\\n]*$"
  ruler_pattern: "^[^\\w\\s]{3}.*[^\\w\\s]{3}$"
  hashruler_min_length: 10
  canonicalize_hashrulers: true

Format Options

line_width

Target maximum output width before wrapping is attempted.

format:
  line_width: 100

Raise this if your project prefers wider CMake calls.

tab_size

Indent width in spaces when use_tabs is false.

format:
  tab_size: 4

use_tabs

Use tab characters for indentation instead of spaces.

format:
  use_tabs: true

This affects leading indentation only. Internal alignment rules use the configured indentation unit but are not otherwise changed.

max_empty_lines

Maximum number of consecutive blank lines to preserve.

format:
  max_empty_lines: 1

Blank-line runs larger than this limit are clamped down. Vertical breathing room is preserved; runaway gaps are not.

max_hanging_wrap_lines

Maximum number of lines a hanging-wrap layout may consume before the formatter falls back to a more vertical layout.

format:
  max_hanging_wrap_lines: 2

Lower values force more aggressively vertical output.

max_hanging_wrap_positional_args

Maximum positional arguments to keep in a hanging-wrap layout before falling back to a more vertical layout.

format:
  max_hanging_wrap_positional_args: 6

Most noticeable on commands with long source or header lists.

max_hanging_wrap_groups

Maximum number of keyword/flag subgroups to keep in a hanging-wrap layout.

format:
  max_hanging_wrap_groups: 2

Lower this to push keyword-heavy commands toward vertical layout sooner.

dangle_parens

Place the closing ) on its own line when a call wraps.

format:
  dangle_parens: true

Effect:

target_link_libraries(
  foo
  PUBLIC
    bar
    baz
)

dangle_align

Alignment strategy for a dangling closing ).

Allowed values:

  • prefix
  • open
  • close
format:
  dangle_align: prefix

min_prefix_length

Lower heuristic bound used when deciding between compact and wrapped layouts.

format:
  min_prefix_length: 4

Leave this alone unless you are deliberately tuning layout behavior.

max_prefix_length

Upper heuristic bound used when deciding between compact and wrapped layouts.

format:
  max_prefix_length: 10

Like min_prefix_length, this is a layout-tuning knob rather than a day-one config option.

space_before_control_paren

Insert a space before ( for control-flow commands such as if, foreach, and while.

format:
  space_before_control_paren: true

Effect:

if (WIN32)
  message(STATUS "Windows")
endif ()

space_before_definition_paren

Insert a space before ( for function and macro definitions.

format:
  space_before_definition_paren: true

Effect:

function (my_helper arg)
  ...
endfunction ()

Style Options

command_case

Controls the casing of command names.

Allowed values:

  • lower
  • upper
  • unchanged
style:
  command_case: lower

keyword_case

Controls the casing of recognized keywords and flags.

Allowed values:

  • lower
  • upper
  • unchanged
style:
  keyword_case: upper

Example — with command_case: lower and keyword_case: upper:

target_link_libraries(foo PUBLIC bar)

stays:

target_link_libraries(foo PUBLIC bar)

With command_case: upper and keyword_case: lower:

TARGET_LINK_LIBRARIES(foo public bar)

Markup Options

enable_markup

Enable comment-markup awareness.

markup:
  enable_markup: true

When enabled, the formatter can recognize lists, fences, and rulers inside comments rather than treating them as opaque text.

reflow_comments

Reflow plain line comments to fit within the configured line width.

markup:
  reflow_comments: true

Leave this false if you want comments preserved more literally.

first_comment_is_literal

Preserve the first comment block in a file without any reflowing or markup processing.

markup:
  first_comment_is_literal: true

Useful for license headers or hand-crafted introductory comments that must stay exactly as written.

literal_comment_pattern

Regex for comments that should never be reflowed.

markup:
  literal_comment_pattern: "^\\s*NOTE:"

Use this for project-specific comment conventions that must stay untouched.

bullet_char

Preferred bullet character when normalizing markup lists.

markup:
  bullet_char: "*"

enum_char

Preferred punctuation for numbered lists when normalizing markup.

markup:
  enum_char: "."

fence_pattern

Regex describing fenced literal comment blocks.

markup:
  fence_pattern: "^\\s*[`~]{3}[^`\\n]*$"

Keep the default unless your project has a strong house style.

ruler_pattern

Regex describing ruler-style comments that should be treated specially.

markup:
  ruler_pattern: "^[^\\w\\s]{3}.*[^\\w\\s]{3}$"

hashruler_min_length

Minimum length before a hash-only line is treated as a ruler.

markup:
  hashruler_min_length: 10

canonicalize_hashrulers

Normalize hash-ruler comments when markup handling is enabled.

markup:
  canonicalize_hashrulers: true

If your project uses decorative comment rulers and wants them normalized consistently, keep this enabled.

Per-command Overrides

Use per_command_overrides: to change formatting knobs for one command name without touching that command’s argument syntax.

Example:

per_command_overrides:
  my_custom_command:
    line_width: 120
    command_case: unchanged
    keyword_case: upper
    tab_size: 4
    dangle_parens: false
    dangle_align: prefix
    max_hanging_wrap_positional_args: 8
    max_hanging_wrap_groups: 3

Supported override fields:

  • command_case
  • keyword_case
  • line_width
  • tab_size
  • dangle_parens
  • dangle_align
  • max_hanging_wrap_positional_args
  • max_hanging_wrap_groups

Use this when you want a command to format differently from the global defaults. Do not use it to define a command’s argument structure — that belongs in commands:.

Custom Command Specs

Use commands: to teach cmakefmt about custom functions and macros, or to override the built-in shape of an existing command.

Example:

commands:
  my_custom_command:
    pargs: 1
    flags:
      - QUIET
    kwargs:
      SOURCES:
        nargs: "+"
      LIBRARIES:
        nargs: "+"

This tells cmakefmt that:

  • the command starts with one positional argument
  • QUIET is a standalone flag
  • SOURCES starts a keyword section with one or more values
  • LIBRARIES starts a keyword section with one or more values

Once the formatter knows the structure, it can group and wrap the command intelligently — instead of treating every token as an undifferentiated argument. For larger custom specs, YAML is much easier to maintain than TOML, which is why the default starter config is YAML.

Old Draft Key Names

The current cmakefmt config schema only accepts the clearer names on this page. If you have an older local config using names such as:

  • use_tabchars
  • max_pargs_hwrap
  • max_subgroups_hwrap
  • separate_ctrl_name_with_space
  • separate_fn_name_with_space

update them before use. cmakefmt fails fast on unknown config keys rather than silently ignoring them — so you will know immediately.

Formatter Behavior

What cmakefmt preserves, what it intentionally changes, and how to reason about the output when you run it across a real codebase.

Core Principles

cmakefmt is designed to be:

  • safe: formatting must never change the meaning of the file
  • idempotent: formatting the same file twice must produce the same result
  • predictable: line wrapping and casing follow explicit config, not heuristics you have to reverse-engineer
  • respectful of structure: comments, disabled regions, and command shapes are all first-class

What cmakefmt Preserves

  • comments and comment ordering
  • bracket arguments and bracket comments
  • disabled regions (# cmakefmt: off / # cmakefmt: on)
  • command structure as defined by the built-in or user-supplied command spec
  • blank-line separation, bounded by max_empty_lines
  • parse-tree equivalence for formatted output on supported inputs

What cmakefmt Intentionally Changes

  • command name case when command_case is not unchanged
  • keyword and flag case when keyword_case is not unchanged
  • indentation and wrapping
  • blank-line runs that exceed the configured limit
  • line-comment layout when markup or comment-reflow options are enabled

Layout Strategy

cmakefmt tries the simplest layout first and only escalates when necessary:

  1. keep a call on one line when it fits
  2. use a hanging-wrap layout when that stays readable
  3. fall back to a more vertical layout when width and grouping thresholds are exceeded

Compact Layout

Input:

target_link_libraries(foo PUBLIC bar)

Output:

target_link_libraries(foo PUBLIC bar)

Wrapped Layout

Input:

target_link_libraries(foo PUBLIC very_long_dependency_name another_dependency)

Typical output:

target_link_libraries(
  foo
  PUBLIC
    very_long_dependency_name
    another_dependency)

The exact shape depends on the command spec, line width, and wrapping thresholds in your config.

Blank Lines

cmakefmt preserves meaningful vertical separation, but clamps runaway blank-line gaps according to format.max_empty_lines.

Input:

project(example)



add_library(foo foo.cc)

Output with max_empty_lines = 1:

project(example)

add_library(foo foo.cc)

Comments

Comments are not stripped and reattached later. They are first-class parsed elements that move through the entire formatter pipeline.

That distinction matters. It means cmakefmt can reliably preserve:

  • standalone comments above a command
  • inline argument-list comments
  • trailing same-line comments
  • bracket comments

Example:

target_sources(foo
  PRIVATE
    foo.cc # platform-neutral
    bar.cc)

cmakefmt keeps the trailing comment attached to the relevant argument.

Comment Markup

When markup handling is enabled, cmakefmt can recognize and treat some comments as lists, fences, or rulers rather than opaque text.

The key knobs:

  • markup.enable_markup
  • markup.reflow_comments
  • markup.first_comment_is_literal
  • markup.literal_comment_pattern

To leave comments almost entirely alone, keep reflow_comments = false.

Control Flow And Blocks

Structured commands — if/elseif/else/endif, foreach/endforeach, while/endwhile, function/endfunction, macro/endmacro, block/endblock — are treated as block constructs rather than flat calls. This affects indentation and spacing around their parentheses.

With space_before_control_paren = true:

if (WIN32)
  message(STATUS "Windows build")
endif ()

Without it:

if(WIN32)
  message(STATUS "Windows build")
endif()

Disabled Regions And Fences

Need to protect a block from formatting? Use a disabled region:

# cmakefmt: off
set(SPECIAL_CASE   keep   this   exactly)
# cmakefmt: on

All of the following markers work:

  • # cmakefmt: off / # cmakefmt: on
  • # cmake-format: off / # cmake-format: on
  • # fmt: off / # fmt: on
  • # ~~~

This is the escape hatch for generated blocks, unusual macro DSLs, or legacy sections you are not ready to normalize yet.

Custom Commands

Custom commands format well only when cmakefmt understands their structure. That is what commands: in your config is for. Once you tell the registry what counts as positional arguments, standalone flags, and keyword sections, the formatter groups and wraps those commands intelligently — instead of treating every token as an undifferentiated lump.

Per-command Overrides

per_command_overrides: changes formatting knobs for a single command name without touching its argument structure.

Use it when you want:

  • a wider line_width for message
  • different casing for one specific command
  • different wrapping thresholds for a single noisy macro

Do not use it to describe a command’s argument structure. That belongs in commands:.

Range Formatting

--lines START:END formats only selected line ranges. This is mainly for editor workflows and partial-file automation.

Important: the selected range still lives inside a full CMake file. Surrounding structure still applies. Partial formatting is best-effort, not an isolated mini-file pass.

Debug Mode

When a formatting result surprises you, --debug is the first thing to reach for. It surfaces everything the formatter normally keeps to itself:

  • file discovery
  • selected config files and CLI overrides
  • barrier and fence transitions
  • chosen command forms
  • effective per-command layout thresholds
  • chosen layout families
  • changed-line summaries

Known Differences From cmake-format

cmakefmt is a practical replacement for cmake-format, not a byte-for-byte clone. That means:

  • some outputs differ while still being valid and stable
  • the config surface has been cleaned up in places
  • workflow features are intentionally broader
  • diagnostics are intentionally much more explicit

When comparing outputs during migration, judge by readability, stability, semantic preservation, and ease of automation — not solely by whether every wrapped line matches historical cmake-format output exactly.

Troubleshooting

Your diagnostic companion for the most common failure modes and confusing situations when using cmakefmt.

cmakefmt Did Not Find The Files I Expected

Check how you invoked it:

  • direct file arguments are always processed
  • directories are recursively discovered
  • discovery respects .cmakefmtignore
  • discovery respects .gitignore unless you pass --no-gitignore
  • --path-regex filters only discovered paths, not direct file arguments

See exactly what was found and why:

cmakefmt --list-input-files .
cmakefmt --debug --list-input-files .

The Wrong Config File Was Used

Find out exactly what config was selected and what the formatter is actually running with:

cmakefmt --show-config-path path/to/CMakeLists.txt
cmakefmt --show-config path/to/CMakeLists.txt
cmakefmt --explain-config

These commands tell you:

  • which config file was selected
  • which files were considered
  • what CLI overrides changed the final result

A Config Key Was Rejected

cmakefmt deliberately fails fast on unknown config keys instead of silently ignoring them. This is by design — a typo that disappears into a warning is a much worse experience than an immediate error.

Common causes:

  • typo in a key name
  • using an old draft key name
  • confusing commands: (argument structure) with per_command_overrides: (layout/style)

If you are migrating from legacy cmake-format:

cmakefmt --convert-legacy-config .cmake-format.py > .cmakefmt.toml

Then adapt the result to .cmakefmt.yaml if you want YAML as your final format.

Parse Error On Valid-Looking CMake

Run the failing file with full diagnostics:

cmakefmt --debug --check path/to/CMakeLists.txt

cmakefmt should surface:

  • file path
  • line/column
  • source context
  • a likely-cause hint when one can be inferred

If the failure is inside a highly custom DSL region, exclude that block temporarily with barrier markers:

# cmakefmt: off
...
# cmakefmt: on

My Custom Command Formats Poorly

This almost always means the command registry does not know the command’s syntax yet.

Add a commands: entry to your config:

commands:
  my_custom_command:
    pargs: 1
    flags:
      - QUIET
    kwargs:
      SOURCES:
        nargs: "+"

Once cmakefmt understands the argument structure, it can produce keyword-aware, properly grouped output instead of flattening everything into a token stream.

If you only want layout or style tweaks for an already-known command, use per_command_overrides: instead.

Stdin Formatting Ignores My Project Config

When formatting stdin, cmakefmt has no real file path and cannot discover config automatically. Fix this with --stdin-path:

cat src/CMakeLists.txt | cmakefmt - --stdin-path src/CMakeLists.txt

The Output Surprises Me

Turn on formatter diagnostics and let cmakefmt explain itself:

cmakefmt --debug path/to/CMakeLists.txt

The debug stream shows:

  • which command form was chosen
  • which layout family was chosen
  • whether barriers or fences were active
  • which effective thresholds applied

I Want Quieter CI Logs

cmakefmt --check --quiet .

For machine-readable output that scripts and dashboards can consume:

cmakefmt --check --report-format json .

I Want The Run To Continue After A Bad File

cmakefmt --keep-going --check .

--keep-going lets the formatter process remaining files and print an aggregated summary instead of aborting at the first file-level error. Useful for triage on large repositories.

I Want Safer Rewrites Or Stricter Version Pins

cmakefmt --verify CMakeLists.txt
cmakefmt --required-version 0.0.1 --check .
  • --verify reparses the original and formatted output and rejects the run if the parsed CMake semantics change.
  • --required-version makes scripts fail fast when the installed binary is not the exact version they expect.
  • --in-place already performs semantic verification by default; add --fast only when you explicitly want to skip that extra check.

I Suspect A Performance Regression

Run the benchmark suite:

cargo bench --bench formatter

For one-off workflow timing, compare representative commands with hyperfine. The repository’s full benchmark process is documented in the performance notes.

If the workload is a repeated whole-repo check, also try the built-in cache:

cmakefmt --cache --check .
cmakefmt --cache-location .cache/cmakefmt --cache-strategy content --check .

Use --debug if you want to see cache hits and misses explicitly.

I Want A Gradual Rollout Instead Of Formatting Everything

cmakefmt --require-pragma --check .

Then opt files in explicitly with a top-level marker:

# cmakefmt: enable

Equivalent markers:

  • # fmt: enable
  • # cmake-format: enable

Files without one of those markers are skipped and show up as skipped=N in the human summary.

Still Stuck?

When reporting an issue, include:

  • the exact command you ran
  • the file that failed, or a minimized reproduction
  • your .cmakefmt.yaml or .cmakefmt.toml
  • the full stderr output
  • --debug output when the problem is about formatting behavior rather than a hard parse error

Migration From cmake-format

Switching to cmakefmt is designed to be straightforward. The goal is easy adoption, not a risky big-bang rewrite — roll out incrementally, compare output at each step, and flip the switch once you are satisfied.

  1. start with --check in CI on a small target directory
  2. generate a starter config with --dump-config (YAML by default, toml available explicitly if needed)
  3. if you already have a cmake-format config file, convert it automatically with --convert-legacy-config
  4. compare output on a representative corpus
  5. switch pre-commit and CI once the output looks good

CLI Mapping

cmake-format intentcmakefmt equivalent
format file to stdoutcmakefmt FILE
in-place formatcmakefmt -i FILE
CI checkcmakefmt --check PATH
recursive target filteringcmakefmt --path-regex REGEX PATH
convert old config filecmakefmt --convert-legacy-config OLD.py > .cmakefmt.toml
disable formatting regionssupports both cmake-format and cmakefmt spellings

Compatibility Notes

  • the goal is easy adoption, not output identity
  • the built-in command registry is audited through CMake 4.3.1
  • --config is still accepted as an alias for --config-file
  • --path-regex replaces the older --file-regex
  • any unsupported compatibility should be treated as a bug, not silently assumed

Operational Advice

Roll out with snapshots or branch-local diffs first. Formatter migrations become painful when the first exposure is a large repository-wide rewrite without comparison data. Start small, build confidence, then go wide.

Library API

cmakefmt is primarily a CLI tool, but the crate already exposes a capable embedded API for Rust code that wants to parse or format CMake sources in-process — no subprocess, no shell escape, no overhead.

When To Use The Library

The crate is a strong fit when you want to:

  • format generated CMake from Rust code
  • build editor or IDE tooling around CMake formatting
  • run cmakefmt in-process instead of spawning a subprocess
  • experiment with custom command registries
  • parse CMake and inspect the AST directly

Crate Status

The library API is usable today. This repository is stable, but the crate is still pre-1.0, so expect some surface evolution before long-term compatibility guarantees settle.

Public Entry Points

The most important items today:

  • format_source
  • format_source_with_debug
  • format_source_with_registry
  • format_source_with_registry_debug
  • Config
  • CaseStyle
  • DangleAlign
  • PerCommandConfig
  • Error
  • Result

Lower-level access is available through:

  • cmakefmt::parser
  • cmakefmt::spec::registry::CommandRegistry

Minimal Formatting Example

use cmakefmt::{Config, format_source};

fn main() -> Result<(), cmakefmt::Error> {
    let src = "target_link_libraries(foo PUBLIC bar)";
    let out = format_source(src, &Config::default())?;
    println!("{out}");
    Ok(())
}

The simplest entry point when you already have source text in memory.

Formatting With A Tweaked Config

use cmakefmt::{CaseStyle, Config, format_source};

fn main() -> Result<(), cmakefmt::Error> {
    let mut config = Config::default();
    config.line_width = 100;
    config.command_case = CaseStyle::Lower;
    config.keyword_case = CaseStyle::Upper;
    config.dangle_parens = true;

    let src = r#"
add_library(foo STATIC a.cc b.cc)
target_link_libraries(foo PUBLIC bar baz)
"#;

    let out = format_source(src, &config)?;
    println!("{out}");
    Ok(())
}

The right pattern when your application needs to supply formatter policy at runtime rather than discovering it from disk.

Loading Config From Disk

To use the same config-loading behavior the CLI uses:

use std::path::Path;

use cmakefmt::Config;

fn main() -> Result<(), cmakefmt::Error> {
    let config = Config::from_file(Path::new(".cmakefmt.yaml"))?;
    println!("line width: {}", config.line_width);
    Ok(())
}

Merge multiple explicit config files in precedence order:

use std::path::PathBuf;

use cmakefmt::Config;

fn main() -> Result<(), cmakefmt::Error> {
    let config = Config::from_files(&[
        PathBuf::from("base.yaml"),
        PathBuf::from("team.yaml"),
    ])?;
    println!("{:#?}", config);
    Ok(())
}

Ask which config files would be discovered for a given target:

use std::path::Path;

use cmakefmt::Config;

fn main() {
    let sources = Config::config_sources_for(Path::new("src/CMakeLists.txt"));
    for path in sources {
        println!("{}", path.display());
    }
}

Formatting With Debug Decisions

Building tooling and want insight into what the formatter decided? Use the debug variant:

use cmakefmt::{Config, format_source_with_debug};

fn main() -> Result<(), cmakefmt::Error> {
    let src = "install(TARGETS mylib DESTINATION lib)";
    let (formatted, debug_lines) = format_source_with_debug(src, &Config::default())?;

    println!("{formatted}");
    for line in debug_lines {
        eprintln!("{line}");
    }

    Ok(())
}

The returned debug lines are the same formatter-decision detail that the CLI emits under --debug.

Using A Custom Command Registry

For syntax that is not part of the built-in registry, use CommandRegistry directly:

use cmakefmt::{Config, format_source_with_registry};
use cmakefmt::spec::registry::CommandRegistry;

fn main() -> Result<(), cmakefmt::Error> {
    let mut registry = CommandRegistry::load()?;
    registry.merge_override_str(
        r#"
[commands.my_custom_command]
pargs = 1
flags = ["QUIET"]

[commands.my_custom_command.kwargs.SOURCES]
nargs = "+"
"#,
        "inline-override.toml",
    )?;

    let src = "my_custom_command(foo QUIET SOURCES a.cc b.cc)";
    let out = format_source_with_registry(src, &Config::default(), &registry)?;
    println!("{out}");
    Ok(())
}

This is the primary embedded path for generated or custom CMake DSLs.

Parsing Without Formatting

When you only need the AST:

use cmakefmt::parser::parse;

fn main() -> Result<(), cmakefmt::Error> {
    let file = parse("project(example LANGUAGES CXX)")?;
    println!("{:#?}", file);
    Ok(())
}

Useful for analysis tools, migration tooling, or experiments that want the CMake parse tree but not the formatter.

Error Model

The library uses a shared cmakefmt::Error type across parsing, config loading, registry loading, and formatting:

Error kindMeaning
Error::Parsethe input was not valid CMake under the current grammar
Error::Configa user config file failed to parse or validate
Error::Speca command-spec override or built-in spec failed to parse
Error::Iofile I/O failed
Error::Formattera formatter-layer invariant or unsupported case was hit

For parse, config, and spec errors, the library retains file-path and location context so callers can surface useful diagnostics to users.

Current Limits

  • the public API is useful today, but still smaller than the CLI feature surface
  • library stability is not promised yet — the crate is still pre-1.0
  • workflow features like Git-aware selection and ignore-file handling live in the CLI layer, not the formatting API itself

For deeper implementation details, continue with Architecture.

Architecture

A user-facing overview of how cmakefmt works and why it is built the way it is.

Mental Model

cmakefmt is not a regex-based text rewriter. It is a structured pipeline:

discover files
  -> resolve config
  -> parse CMake source
  -> classify commands using the command registry
  -> build formatted layout decisions
  -> emit text / diff / check result / in-place rewrite

That structure is what makes the tool safe and predictable — and what separates it from a simple line-by-line formatter.

Main Layers

Parser

The parser is built on a pest PEG grammar. It understands:

  • command invocations
  • quoted, unquoted, and bracket arguments
  • comments
  • variable references
  • generator expressions
  • continuation lines

Comments are preserved as real syntax nodes throughout — they are never stripped and guessed at later.

Command Registry

The registry is what gives cmakefmt its semantic awareness.

Without it, every argument in:

target_link_libraries(foo PUBLIC bar PRIVATE baz)

looks like a generic positional token. The registry knows that PUBLIC and PRIVATE are not generic tokens — they start new argument groups. That knowledge is what lets the formatter produce keyword-aware, correctly grouped output instead of flattened token streams.

The registry is populated from two sources:

  • built-in specs for CMake commands and supported module commands (audited through CMake 4.3.1)
  • optional user config under commands:

Formatter

Once the source is parsed and command shapes are known, the formatter converts the AST into layout decisions using a Wadler-Lindig-style document model.

In practice, this means it can ask:

  • can this stay on one line?
  • if not, should it hang-wrap?
  • if not, should it go fully vertical?

That is how cmakefmt gets stable, principled wrapping behavior instead of ad-hoc line splitting that changes every time you touch a file.

Config

Config resolution is layered — later layers only apply when earlier ones are absent:

  1. CLI overrides
  2. explicit --config-file files, if any
  3. nearest discovered .cmakefmt.yaml, .cmakefmt.yml, or .cmakefmt.toml
  4. home-directory fallback config
  5. built-in defaults

Make the resolution process visible with:

cmakefmt --show-config-path src/CMakeLists.txt
cmakefmt --show-config src/CMakeLists.txt
cmakefmt --explain-config

CLI Workflow Layer

The CLI is far more than a thin wrapper around format_source. It handles:

  • recursive file discovery
  • ignore files and Git-aware selection
  • --check, --diff, and JSON reporting
  • in-place rewrites
  • partial and range formatting
  • progress bars and parallel execution
  • diagnostics and summary reporting

That workflow layer is a large part of what makes cmakefmt useful in real repositories rather than just in toy examples.

Diagnostics

When something goes wrong, cmakefmt tries hard to explain:

  • which file failed
  • where it failed
  • what source text was involved
  • what config was active
  • what likely caused the failure

This is possible because the architecture keeps spans, config provenance, and formatter decision context around long enough to report them meaningfully — rather than discarding context as soon as each stage completes.

Design Priorities

The codebase is intentionally optimized around:

  • correctness over cleverness — no surprising heuristics
  • speed that is visible in day-to-day workflows — 20× faster than cmake-format on real corpora
  • strong diagnostics — failures explain themselves
  • configurability without scriptable config files — powerful without being dangerous
  • maintainability of the grammar/registry/formatter pipeline — easy to extend correctly

Changelog

The canonical changelog lives in the repository root as CHANGELOG.md. This page documents the release-note policy for the published docs site.

Policy

  • keep user-visible work under Unreleased until the next cut
  • group changes by impact — what users feel, not what files changed
  • call out migration and compatibility notes explicitly
  • never bury user-visible behavior changes inside implementation-only commit messages

What Release Notes Must Cover

  • new CLI or config surface
  • formatter behavior changes
  • compatibility differences from cmake-format
  • performance changes that matter to users
  • breaking changes and rollout advice