cmakefmt — a Rust reimplementation of cmake-format (from the dead
cmakelang Python project). Goal: fast, correct, configurable CMake
formatter distributed as a single binary.
- Parser: hand-written recursive descent over a streaming scanner. Public
AST stays in
src/parser/ast.rs; parser internals live insrc/parser/{cursor,scanner,grammar,lower}.rs. - Formatter: Wadler-Lindig algorithm via the
prettycrate. AST -> Doc IR -> string. - Config: YAML/TOML via
serde. Files:.cmakefmt.yaml,.cmakefmt.yml,.cmakefmt.toml. - CLI:
clap(derive API). Subcommands:config,dump,lsp,completions,install-hook. - Snapshot tests:
instacrate. Snapshots live intests/snapshots/. - Idempotency invariant:
format(format(x)) == format(x)must always hold. - Semantic verification: formatted output is re-parsed and compared to the original AST (with comments stripped) to ensure formatting never changes CMake semantics.
cargo build # debug build
cargo build --release # release build
cargo run -- path/to/CMakeLists.txt
cargo run -- --check path/to/CMakeLists.txt
cargo run -- -i path/to/CMakeLists.txt # in-place
cargo run -- dump ast path/to/CMakeLists.txt # debug: show parser AST
cargo run -- dump parse path/to/CMakeLists.txt # debug: show spec-resolved treecargo test # all tests
cargo test parser # parser unit tests
cargo test formatter # formatter unit tests
cargo test --test snapshots # snapshot tests only
cargo test --test cli # CLI integration tests
cargo test --test real_world # real-world corpus tests
cargo test --test idempotency # idempotency invariant
cargo insta review # review snapshot changes interactivelyIdempotency tests run automatically as part of cargo test.
cargo clippy -- -D warnings # must pass clean
cargo fmt --check # must pass cleanCI and pre-commit hooks run both of these.
cmakefmt/
+-- AGENTS.md <- this file
+-- Cargo.toml
+-- docs/
| +-- README.md <- docs site contributor notes
| +-- astro.config.mjs <- Astro + Starlight site configuration
| +-- package.json <- docs app dependencies and scripts
| +-- public/ <- static site assets
| +-- src/
| +-- content/docs/ <- published docs pages
| +-- assets/ <- docs site branding assets
| +-- styles/ <- Starlight theme customizations
+-- src/
| +-- main.rs <- CLI entry point (clap derive API)
| +-- lib.rs <- public API (format_source, format_file)
| +-- error.rs <- unified error type (thiserror)
| +-- files.rs <- CMake file discovery, .cmakefmtignore support
| +-- dump.rs <- parse-tree dump (dump ast, dump parse)
| +-- config/
| | +-- mod.rs <- Config struct, defaults, resolution logic
| | +-- file.rs <- YAML/TOML config file loading + merging
| | +-- editorconfig.rs <- .editorconfig fallback support
| | +-- legacy.rs <- conversion from legacy cmake-format config files
| +-- parser/
| | +-- mod.rs <- public parse() fn, parser glue
| | +-- cursor.rs <- byte cursor over source text
| | +-- scanner.rs <- streaming scanner for literals/comments/args
| | +-- grammar.rs <- recursive-descent parser to private parse tree
| | +-- lower.rs <- parse-tree -> public AST normalization
| | +-- ast.rs <- AST node types (File, Statement, Argument, Comment)
| +-- spec/
| | +-- mod.rs <- NArgs, PosSpec, KwargSpec, CommandForm, CommandSpec
| | +-- registry.rs <- CommandRegistry: loads builtins + user overrides
| | +-- builtins.yaml <- built-in specs for all ~150 CMake commands
| +-- formatter/
| | +-- mod.rs <- public format() fn, barrier/fence handling
| | +-- node.rs <- AST -> Doc IR (sections, wrapping, sorting)
| | +-- comment.rs <- comment formatting and reflow
| +-- lsp.rs <- LSP server (textDocument/formatting, rangeFormatting)
+-- tests/
| +-- snapshots/ <- insta snapshot files (committed to repo)
| +-- cli.rs <- CLI integration tests (~190 tests)
| +-- idempotency.rs <- format(format(x)) == format(x) + line width checks
| +-- parser_fixtures.rs <- parser unit tests driven by fixture files
| +-- real_world.rs <- formatting tests against pinned real-world corpus
| +-- snapshots.rs <- snapshot-based formatting tests (~66 tests)
| +-- fixtures/ <- .cmake input files for tests
| +-- basic/
| +-- comments/
| +-- edge_cases/
| +-- real_world/
+-- benches/
| +-- formatter.rs <- criterion benchmarks
+-- packaging/ <- Homebrew, winget, Scoop, conda-forge manifests
+-- scripts/ <- sync-changelog.py, check-docs.sh, etc.
+-- fuzz/ <- cargo-fuzz targetsComments are first-class AST nodes — they are NOT stripped and reattached. The parser preserves their position relative to surrounding nodes, and the formatter treats them like any other node in the Doc IR.
Types of comments in CMake:
- Line comment:
# ...(to end of line) - Bracket comment:
#[=[ ... ]=](multi-line)
Long trailing comments that exceed line_width are reflowed with
continuation lines aligned to the #. The parser merges column-aligned
continuation comments back into the trailing comment on re-parse, ensuring
idempotent round-trips. This is gated behind enable_markup.
When a standalone line comment immediately follows a command's trailing
comment and the # is at the same column, the parser merges it into the
trailing comment text. This ensures the formatter's reflowed output
round-trips correctly. See merge_trailing_comment_continuation in
src/parser/mod.rs.
- CLI flags (
--line-width,--tab-size, etc.) .cmakefmt.yaml/.cmakefmt.tomlin the directory of the file being formatted- Same files walking up to repo root (or git root)
.editorconfigfallback forindent_styleandindent_size- Built-in defaults
Config sections: format:, markup:, per_command_overrides:, commands:.
The full schema is at cmakefmt config schema.
format.line_width(default 80) — maximum line widthformat.tab_size(default 2) — spaces per indent levelformat.command_case/format.keyword_case—lower,upper, orunchangedformat.dangle_parens— closing paren on own lineformat.wrap_after_first_arg— keep first arg on command line (default forset())format.wrap_after_first_arg_align— where that continuation starts:under-first-arg(default) ordefault-indentformat.argument_comment_style— preserve original argument comment placement (default), or put all argument comments on standalone linesformat.enable_sort/format.autosort— sort arguments in keyword sectionsmarkup.enable_markup— controls comment reflow and markup handlingcommands:— user-defined command specs (pargs, kwargs, flags, sortable)per_command_overrides:— per-command format option overrides
cmakefmt config dump|schema|check|show|path|explain|convert|initcmakefmt dump ast|parse— debug: print parser AST or spec-resolved treecmakefmt lsp— LSP server on stdiocmakefmt completions <shell>— shell completion scriptscmakefmt install-hook— install git pre-commit hook
-i/--in-place— rewrite files on disk--check— exit 1 if any file would change--diff— show unified diff--verify/--no-verify— semantic verification (on by default for-i)--debug— verbose diagnostics to stderr--explain— show why a specific command was formatted its way--color auto|always|never— ANSI color--parallel [N]— parallel formatting (default: CPUs - 1)--staged/--changed— VCS-aware file selection--lines START:END— range formatting--summary— per-file status lines--report-format human|json|github|checkstyle|junit|sarif|edit
The semantic verifier (verify_semantics in src/main.rs) parses both
original and formatted output, normalizes them, and compares:
- Strips all
Statement::CommentandStatement::BlankLines - Strips
trailing_comment,Argument::InlineCommentandArgument::StandaloneCommentfrom commands - Normalizes command names and keyword case
- Zeroes out spans
Comments are excluded because they have no CMake semantic meaning and
comment reflow changes their structure. The idempotency test in
tests/idempotency.rs uses the same normalization — keep them in sync.
Built-in specs live in src/spec/builtins.yaml. Format:
commands:
set:
pargs: '2+'
kwargs:
CACHE:
nargs: 2
flags: [FORCE]
layout:
wrap_after_first_arg: trueregistry.get("command_name")->&CommandSpecspec.form_for(first_arg)->&CommandForm(handles discriminated commands likeinstall)split_sections(command, form)classifies arguments as keywords, flags, or positionals
enable_sort: truesorts sections markedsortable: truein the specautosort: trueheuristically sorts sections where all non-comment arguments are simple unquoted tokens (no variables/generator expressions)- Sort is case-insensitive and stable
- Inline comments do not prevent autosort from activating
-
SPDX headers on all source files:
// SPDX-FileCopyrightText: Copyright 2026 Puneet Matharu // SPDX-License-Identifier: MIT OR Apache-2.0
-
All public types derive
Debug,Clone. -
Errors use
thiserror. Never use.unwrap()in library code; use?. -
Pest rule names:
snake_case. AST enum variants:PascalCase. -
New formatter rules go in
formatter/node.rsalongside the relevant AST variant. -
Every new formatting behaviour needs a snapshot test.
-
Pre-commit hooks:
trim-trailing-whitespace,cargo fmt,reuse lint,check-docs. -
Do not add
Co-Authored-Bylines to commits. -
Use
clapconflicts_withfor mutually exclusive flags — never rely on silent no-ops.
Users can protect blocks from formatting:
# cmakefmt: off
set(SPECIAL keep this exactly)
# cmakefmt: onAlso supported: # cmake-format: off/on, # fmt: off/on, # ~~~.
Three steps are owned by .github/workflows/prepare-release.yml and
must not be done by hand:
- bumping
Cargo.tomlversion(and theCargo.lockrefresh that follows) - stamping
CHANGELOG.md— turning the working## Unreleasedblock into## <version> — <date> - running
scripts/sync-changelog.pysodocs/src/content/docs/changelog.mdmirrorsCHANGELOG.md
The same workflow also copies docs/public/schemas/latest/schema.json to
docs/public/schemas/v${VERSION}/schema.json, creates the release
commit, tags, and pushes. release.yml then builds, verifies, and
publishes from the tag.
Before triggering the workflow:
- make sure the
## Unreleasedblock inCHANGELOG.mdis complete and attributed to the right semver-impact buckets - run
cmakefmt config schema > docs/public/schemas/latest/schema.jsonif any config-shape change landed since the previous release (CI's "Check JSON schema is up to date" gate enforces this anyway) - add a v${VERSION} datapoint to
docs/src/components/VersionTrendChart.astrowith the wall-time + binary-size measurement and a one-line annotation. Generate the measurement with./benches/version-trend-datapoint.sh, which prints the chart-ready{ version: "...", time: ..., binary: ... }line directly to stdout.
If a CI failure forces a re-run, prefer fixing the underlying issue and
re-triggering rather than hand-editing Cargo.toml/CHANGELOG.md to
match a partial release state.
pest,pest_derive— PEG parserpretty— Wadler-Lindig pretty-printingserde,serde-saphyr,toml— config serializationclap,clap_complete,clap_mangen— CLIthiserror— error typesinsta— snapshot testing (dev)walkdir,ignore— file discoveryrayon— parallel formattingindicatif— progress barsregex— pattern matchingtempfile— temporary files (tests)notify— file watching (--watch)lsp-server,lsp-types— LSP support