cmakefmt
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.
cmakefmthits a20.69xgeometric-mean speedup overcmake-formaton 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 thatcmake-formatmade you script around are all first- class citizens here. - Knows your commands. Teach
cmakefmtabout 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:
| Metric | Signal |
|---|---|
Geometric-mean speedup vs cmake-format | 20.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 input | estimate 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 batch | 48.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?
Migrating from cmake-format?
Embedding cmakefmt as a library?
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-formaton 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:
| Channel | Planned support level | Notes |
|---|---|---|
cargo install cmakefmt-rust | Officially maintained | The reference install path for developers already using Rust. |
| GitHub Releases binaries | Officially maintained | Native binaries for Linux, macOS, and Windows. |
| Docs site / CLI reference | Officially maintained | Should stay in lock-step with each tagged release. |
Homebrew / winget / Scoop | Officially maintained | Planned first-party package-manager channels. |
Additional package managers (npm, AUR, Nix, containers, etc.) | Automated or best-effort | Useful 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 tomlstill 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:
- what the first public release means
- which install channels are official
- 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.
| Channel | Support level | What to expect |
|---|---|---|
| GitHub Releases binaries | Officially maintained | Release artifacts and checksums published for supported platforms. |
cargo install cmakefmt-rust | Officially maintained | Curated crates.io package with the same source tree used for releases. |
| Documentation site | Officially maintained | Updated as part of the tagged release. |
Homebrew / winget / Scoop | Officially maintained | These are the first package-manager targets after GitHub Releases and crates.io. |
| Additional package managers / wrappers | Best effort | Useful distribution channels, but not all are blockers for the initial release. |
Planned Release Artifacts
Each tagged release is expected to ship:
cmakefmtbinaries for supported platformsSHA256SUMS- 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:
| Metric | Current local signal |
|---|---|
Geometric-mean speedup vs cmake-format | 20.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 input | estimate 1.7545 ms (95% CI 1.7425–1.7739 ms) |
End-to-end format_source, large synthetic input | estimate 8.8248 ms (95% CI 8.8018–8.8519 ms) |
| Debug/barrier-heavy formatting | estimate 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:
cmakefmtwas faster on every single fixture- speedup ranged from
10.91xto48.49x - geometric-mean speedup:
20.69x
Parallel Batch Throughput
Multi-file runs are single-threaded by default, but opt-in parallelism scales well:
| Mode | Time |
|---|---|
| serial | 184.5 ms ± 1.3 ms |
--parallel 2 | 111.5 ms ± 11.9 ms |
--parallel 4 | 64.7 ms ± 1.1 ms |
--parallel 8 | 48.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):
| Mode | Time |
|---|---|
| serial | 412.5 ms ± 9.0 ms |
--parallel 2 | 296.0 ms ± 3.5 ms |
--parallel 4 | 191.8 ms ± 4.7 ms |
--parallel 8 | 152.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 srealcmakefmtserial:0.47 sreal (~97xfaster)cmakefmt --parallel 8:0.19 sreal (~240xfaster)
What The Numbers Mean In Practice
The headline numbers matter not as abstract benchmarks, but because they change what feels viable:
- repository-wide
--checkin 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-darwin10logical CPUsrustc 1.94.1hyperfine 1.20.0cmake-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
Related Reading
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
| Pattern | What it does |
|---|---|
cmakefmt CMakeLists.txt | Format one file to stdout. |
cmakefmt dir/ | Recursively discover CMake files under that directory. |
cmakefmt | Recursively 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
| Flag | Meaning |
|---|---|
--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-gitignore | Stop honoring .gitignore during recursive discovery. |
--staged | Use staged Git-tracked files instead of explicit input paths. |
--changed | Use 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
| Flag | Meaning |
|---|---|
-i, --in-place | Rewrite files on disk instead of printing formatted output. |
--check | Exit with code 1 when any selected file would change. |
--list-changed-files | Print only the files that would change after formatting. |
--list-input-files | Print the selected input files after discovery and filtering, without formatting them. |
--diff | Print 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
| Flag | Meaning |
|---|---|
--debug | Emit discovery, config, barrier, and formatter diagnostics to stderr. |
--quiet | Suppress per-file human output and keep only summaries plus actual errors. |
--keep-going | Continue 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. |
--verify | Parse the original and formatted output and reject the result if the CMake semantics change. |
--fast | Skip semantic verification, including the default rewrite-time verification used by --in-place. |
--cache | Cache 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-pragma | Format 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-bar | Show a progress bar on stderr during --in-place multi-file runs. |
Config And Conversion Flags
| Flag | Meaning |
|---|---|
--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-page | Print 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-path | Print the selected config file path for a single target and exit. --find-config-path is an alias. |
--explain-config | Explain 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
| Flag | Meaning |
|---|---|
-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-config | Ignore 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: success1:--checkor--list-changed-filesfound files that would change2: 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
.cmakefmtignoreand, by default,.gitignore. --ignore-pathadds more ignore files for discovered directories only.--files-from,--staged, and--changedstill pass through normal discovery filters when they produce directories or paths that need filtering.--show-config-path,--show-config, and--explain-configresolve a single target context and make the selected config path(s) visible.--no-configdisables 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.
Related Reading
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:
- repeated
--config-file <PATH>files, if provided - the nearest
.cmakefmt.yaml,.cmakefmt.yml, or.cmakefmt.tomlfound by walking upward from the target ~/.cmakefmt.yaml,~/.cmakefmt.yml, or~/.cmakefmt.toml- 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
Recommended Starter File
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
- Format Options
- Style Options
- Markup Options
- Per-command Overrides
- Custom Command Specs
- Old Draft Key Names
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:
prefixopenclose
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:
lowerupperunchanged
style:
command_case: lower
keyword_case
Controls the casing of recognized keywords and flags.
Allowed values:
lowerupperunchanged
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_casekeyword_caseline_widthtab_sizedangle_parensdangle_alignmax_hanging_wrap_positional_argsmax_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
QUIETis a standalone flagSOURCESstarts a keyword section with one or more valuesLIBRARIESstarts 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_tabcharsmax_pargs_hwrapmax_subgroups_hwrapseparate_ctrl_name_with_spaceseparate_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.
Related Reading
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_caseis notunchanged - keyword and flag case when
keyword_caseis notunchanged - 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:
- keep a call on one line when it fits
- use a hanging-wrap layout when that stays readable
- 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_markupmarkup.reflow_commentsmarkup.first_comment_is_literalmarkup.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_widthformessage - 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
.gitignoreunless you pass--no-gitignore --path-regexfilters 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) withper_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 .
--verifyreparses the original and formatted output and rejects the run if the parsed CMake semantics change.--required-versionmakes scripts fail fast when the installed binary is not the exact version they expect.--in-placealready performs semantic verification by default; add--fastonly 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.yamlor.cmakefmt.toml - the full stderr output
--debugoutput 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.
Recommended Rollout
- start with
--checkin CI on a small target directory - generate a starter config with
--dump-config(YAML by default,tomlavailable explicitly if needed) - if you already have a
cmake-formatconfig file, convert it automatically with--convert-legacy-config - compare output on a representative corpus
- switch pre-commit and CI once the output looks good
CLI Mapping
cmake-format intent | cmakefmt equivalent |
|---|---|
| format file to stdout | cmakefmt FILE |
| in-place format | cmakefmt -i FILE |
| CI check | cmakefmt --check PATH |
| recursive target filtering | cmakefmt --path-regex REGEX PATH |
| convert old config file | cmakefmt --convert-legacy-config OLD.py > .cmakefmt.toml |
| disable formatting regions | supports 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
--configis still accepted as an alias for--config-file--path-regexreplaces 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
cmakefmtin-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_sourceformat_source_with_debugformat_source_with_registryformat_source_with_registry_debugConfigCaseStyleDangleAlignPerCommandConfigErrorResult
Lower-level access is available through:
cmakefmt::parsercmakefmt::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(), ®istry)?;
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 kind | Meaning |
|---|---|
Error::Parse | the input was not valid CMake under the current grammar |
Error::Config | a user config file failed to parse or validate |
Error::Spec | a command-spec override or built-in spec failed to parse |
Error::Io | file I/O failed |
Error::Formatter | a 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:
- CLI overrides
- explicit
--config-filefiles, if any - nearest discovered
.cmakefmt.yaml,.cmakefmt.yml, or.cmakefmt.toml - home-directory fallback config
- 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-formaton 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
Related Pages
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
Unreleaseduntil 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