Write C++ builds in Python, get clean CMakeLists.txt: A modern, type-safe alternative to hand-written CMake.
Project description
CMakeless
CMakeless is a pure-Python frontend for CMake: a modern CMake alternative that lets you describe C++ builds in real Python instead of the CMake language, then generates clean, human-readable CMakeLists.txt and drives CMake for you.
You get the entire CMake ecosystem, every generator, toolchain, IDE, and library, without ever writing CMake by hand again.
# cmakelessfile.py
from cmakeless import Project
project = Project("hello", version="1.0.0", cpp_std=20)
project.add_executable("hello", sources=["src/main.cpp"])
project.build()
$ python cmakelessfile.py # or: cmakeless build
That is a complete, cross-platform C++ build. No cmake_minimum_required, no PARENT_SCOPE, no semicolon-lists, no guessing whether a variable needs quotes. If you make a mistake, you get a Python exception with a real message, at author time, not a cryptic configure-time failure three modules deep.
If you have ever committed a build fix with the message
fix buildand been afraid to touch the file again, this project is for you.
Table of contents
- Why CMakeless exists
- The design philosophy
- Why Python, specifically
- Install
- A concrete, end-to-end workflow
- Feature tour
- Where CMakeless fits in the ecosystem
- What CMakeless will not do
- FAQ
- Requirements
- Learn more
Why CMakeless exists
Let us be honest and precise, because CMake deserves both.
The CMake engine is a marvel. It configures builds for every compiler, every platform, and every IDE that matters. It is the de facto standard of the C++ world: vcpkg, Conan, CLion, Visual Studio, and thousands of libraries all agree on it. Nobody sane wants to rebuild that.
The CMake language is another story. It is the single most criticized part of the C++ toolchain, and the complaints have been remarkably consistent for over a decade:
- Everything is a string. No integers, no booleans, no real lists. A "list" is a string with semicolons in it, and you discover the difference between
"a b c"anda b cat configure time, or worse, at link time. - Scoping is a trap. Variables have dynamic scope and flow into subdirectories but not back out, unless you reach for
PARENT_SCOPE. Functions cannot return a value; they set magic variables in the caller's scope. - The syntax cannot be memorized. Is it
target_link_libraries(app PRIVATE fmt)ortarget_link_libraries(app fmt)? When do you needPUBLICvsPRIVATEvsINTERFACE? Fifteen-year veterans keep the docs open in a permanent tab. - You cannot debug it. There is no breakpoint. There is
message(STATUS "WHY: ${VAR}")sprinkled through your build like archaeological evidence of past suffering. - Twenty years of legacy never dies. Every historical mistake lives on behind a policy flag. "Modern CMake" is a genuinely good set of ideas that most projects never adopted, because the old examples still rank first in search results.
None of this is controversial. People do not use the CMake language because they enjoy it; they use it because they feel they have no choice.
CMakeless gives you the choice.
The design philosophy: replace the language, keep the engine
Every previous attempt to fix this pain, Meson, xmake, premake, Bazel, tried to replace CMake entirely. All capable tools, and all of them ask you to walk away from the largest build ecosystem in C++. That price is why they remain the exception, not the rule.
CMakeless takes the opposite bet:
CMake is not the enemy. Writing CMake is.
Like serverless, where the servers never went away, CMakeless still has CMake at its core. You just never write it again. Four principles hold the line:
- A tiny API you can hold in your head. A handful of classes:
Project,Executable,Library,Test,PythonModule,Preset,Toolchain. If you need the documentation open in a permanent tab, we have failed. - Fail early, fail in Python. Every error that can be caught before CMake runs is caught before CMake runs, and reported as a normal Python exception with a helpful message.
- Boring, readable output. The generated
CMakeLists.txtis modern, target-centric, deterministic, and diffable, clean enough to commit and to leave behind. Deleting CMakeless must always be a boring afternoon, never a migration project. - Delegate, never reimplement. CMake configures, generates, and builds. CMakeless is a frontend, not a build system.
Why Python, specifically
Because C++ and Python have been best friends for years.
- Your team already knows it. Python is the second language of nearly every C++ shop: it runs your test scripts, your code generators, your CI glue. There is nothing new to learn.
- The interop story is already written. pybind11 and nanobind made Python bindings a standard part of serious C++ projects. A Python-native build frontend turns
add_python_module("core")into a one-liner instead of a page of ritual, and the tool that builds your C++ is already inside the interpreter that will import it. - Real tooling, for free. Autocomplete on every function. Type checking on every argument.
breakpoint()inside your build script. Unit tests for your build logic. Things the CMake language will never have. - Built for the free-threaded future. On a free-threaded interpreter, dependency resolution and multi-preset configuration run in parallel threads with no GIL in the way, and degrade gracefully everywhere else.
To be clear about what CMakeless is not: scikit-build-core and meson-python solve the reverse problem, using CMake to build Python packages. CMakeless is for C++ projects, full stop. Python is the pen, not the product.
Install
$ pip install cmakeless
Requirements: Python 3.12+ and CMake 3.25+ on PATH (CMake is needed only to build; generating CMakeLists.txt works without it).
Scaffold a new project in one command:
$ cmakeless init
A concrete, end-to-end workflow
Here is what a real project looks like as it grows, from a single file to a shippable, tested, Python-importable library. Every step is a few lines of cmakelessfile.py, and every verb is one command.
1. Start with an executable and a library
# cmakelessfile.py
from cmakeless import Project
project = Project("mygame", version="1.0.0", cpp_std=23, warnings="strict")
engine = project.add_library(
"engine",
sources=["src/engine/*.cpp"], # globs expand in Python and are validated
public_headers="include/",
kind="static", # "static" | "shared" | "header_only"
)
app = project.add_executable("mygame", sources=["src/main.cpp"])
app.link(engine) # visibility inferred; no PUBLIC/PRIVATE guessing
project.build()
$ cmakeless build
2. Add a dependency in one line
app.depends("fmt/10.2.1") # find_package first, else FetchContent, pinned in cmakeless.lock
CMakeless remembers that the target is fmt::fmt, not fmt, writes a lockfile so CI and teammates get byte-identical trees, and can generate a vcpkg.json or conanfile.txt if you opt into a package manager.
3. Test as a first-class verb (GoogleTest by default)
tests = project.add_test("engine_tests", sources=["tests/*.cpp"]) # framework="gtest" by default
tests.link(engine)
$ cmakeless test # fetches GoogleTest, registers every case with CTest, runs them
$ cmakeless test --sanitize=address # the same suite in a sanitized build tree
Prefer Catch2 or doctest? Pass framework="catch2" or framework="doctest".
4. Ship Python bindings (pybind11 by default)
bindings = project.add_python_module("mygame_core", sources=["src/bindings.cpp"]) # binding="pybind11"
bindings.link(engine)
$ cmakeless build
$ python -c "import mygame_core; print(mygame_core.__doc__)"
CMakeless locates the invoking interpreter's development headers, fetches pybind11 (or nanobind, with binding="nanobind", which also gets .pyi stubs), builds the extension, and copies it into your current environment, so import just works. This is the flagship of the whole idea.
The generated find_package(Python ...) floor defaults to CMakeless's own supported Python version, not whichever interpreter happens to run cmakeless — so the same cmakelessfile.py always emits the same CMakeLists.txt, on any machine. Pass python_version="3.13" to add_python_module(...) to raise it.
5. Configurations, install, and package
from cmakeless import Preset
project.add_preset(Preset("debug", optimize="none", sanitize=["address"]))
project.add_preset(Preset("release", optimize="release", lto=True))
project.add_preset(Preset("ci", inherits="release", options={"MYGAME_BUILD_TOOLS": False}))
project.install(engine, headers=True) # export set + Config.cmake, so others can find_package(mygame)
project.install(app)
project.package(formats=["zip", "deb"]) # CPack
$ cmakeless build --preset release # from a generated CMakePresets.json, its own build tree
$ cmakeless install --prefix dist # GNUInstallDirs-correct layout
$ cmakeless package # CPack archives
Prefer a project-wide default without presets? Set them right on the project:
project.optimize = "release"
project.lto = True
6. Commit the output, walk away any time
compile_commands.json always lands at the project root (clangd, clang-tidy, and every editor just work), and ccache/sccache is wired in automatically when found. The generated CMakeLists.txt is honest, standalone CMake: commit it, open it in CLion or Visual Studio, or delete CMakeless entirely. Your build keeps working either way.
Feature tour
| You write (Python) | We handle (the CMake ritual you skip) |
|---|---|
project.add_library(..., kind="shared") |
add_library, PIC, __declspec(dllexport) export headers, visibility |
app.link(engine) / lib.link(dep, public=True) |
the correct PUBLIC/PRIVATE/INTERFACE keyword, every time |
app.depends("fmt/10.2.1") |
find_package-then-FetchContent fallback, pinned hashes, cmakeless.lock, vcpkg/Conan manifests |
project.warnings = "strict" |
/W4 /permissive- on MSVC, -Wall -Wextra -Wconversion ... on GCC/Clang |
target.sanitize = ["address"] |
sanitizer flags on both compile and link, per-compiler, rejected loudly where unsupported |
project.add_test(...) |
GoogleTest/Catch2/doctest fetch, enable_testing(), per-case CTest discovery, Windows DLL paths |
project.add_python_module(...) |
pybind11/nanobind fetch, find_package(Python), <backend>_add_module, stubs, env install |
project.add_preset(Preset(..., options=, env=, inherits=)) |
CMakePresets.json, per-preset out-of-source build trees, multi-config support |
app.link_options(...) / When.compiler(...) |
target_link_options, generator-expression guards, no manual $<...> syntax |
project.option(...) / cmakeless options |
option()/set(... CACHE ...), discoverable without reading the script |
project.add_command(...) / add_custom_target(...) |
add_custom_command(OUTPUT ...)/add_custom_target wiring, argv-safe (VERBATIM) |
project.install(...) / project.package(...) |
install(TARGETS ...), export sets, Config.cmake, version files, CPack |
target.raw_cmake("...") / project.raw_cmake_file("...") |
the escape hatch: verbatim CMake, fenced with its cmakelessfile.py origin |
Watch progress through the Observer API, and read the configured build as Python objects via the CMake File API:
from cmakeless import Observer, Project, StepFinished
class Timer:
def on_event(self, event):
if isinstance(event, StepFinished):
print(f"{event.step} finished ({event.exit_code})")
project = Project("app", cpp_std=20)
project.add_executable("app", sources=["src/main.cpp"])
project.add_observer(Timer())
for target in project.targets_info(): # read from CMake's File API, not scraped text
print(target.name, target.type, target.artifacts)
The full before/after catalog lives in FEATURES.md.
Where CMakeless fits in the C++ build ecosystem
| Tool | Approach | You keep the CMake ecosystem? |
|---|---|---|
| CMakeless | Python frontend that generates CMake | Yes, entirely |
| Raw CMake | Write the CMake language by hand | Yes |
| Meson / xmake / premake | Replace CMake with a new build system | No |
| Bazel | Replace with a hermetic build system | No |
| scikit-build-core / meson-python | Use CMake/Meson to build Python packages | Reverse problem |
CMakeless is the only one of these that keeps 100% of the CMake ecosystem while removing the CMake language. If a tool understands CMake, it understands your CMakeless project, because the output is CMake.
What CMakeless will not do
Boundaries, stated as promises:
- It will not become a build system. Compilation, incremental rebuilds, and object-file graphs belong to CMake and Ninja, which are better at it than anything we would write.
- It will not invent a DSL.
cmakelessfile.pyis plain Python forever. - It will not hold your project hostage. The generated CMake is readable, committable, and standalone. Leaving must always be boring.
FAQ
Is this production-ready? No. CMakeless is pre-1.0, alpha software. The API can still change without a deprecation cycle. If you need stability today, pin the exact version and read the changelog before upgrading.
Why not just use Meson, Bazel, or xmake? Because you would be leaving the CMake ecosystem behind: vcpkg, Conan, every IDE, every CI action, every existing library's build. CMakeless keeps all of that and only replaces the part everyone actually hates: writing the CMake language by hand.
Does this only work with Ninja and Clang, or does it support MSVC/Visual Studio too? CMake's generator selection is untouched. CMakeless drives whichever generator CMake supports on your platform (Ninja, Visual Studio, Makefiles). MSVC works like any other CMake-driven MSVC project.
Can I still hand-edit the generated CMakeLists.txt?
You can, but the point is you should not have to. It regenerates from your cmakelessfile.py on every build, so hand edits get silently overwritten. Use target.raw_cmake(...) or project.raw_cmake_file(...) for anything the API does not model yet.
How is this different from scikit-build-core or meson-python? Those solve the reverse problem: using CMake or Meson to build a Python package that happens to contain C++. CMakeless is for C++ projects, full stop; Python is the authoring language, not the packaging target.
Do I need CMake installed?
To build, yes: CMake 3.25+ on PATH, same as any CMake project. Generating CMakeLists.txt from a cmakelessfile.py works without CMake present at all.
What happens if I stop using CMakeless later?
Delete it. The generated CMakeLists.txt is standalone, readable, modern CMake with no CMakeless runtime dependency. Commit it and walk away.
Requirements
- Python 3.12+
- CMake 3.25+ on
PATH(only for building; generation works without it)
Learn more
- INTRODUCTION.md: the full story of why CMakeless exists.
- FEATURES.md: everything it does for you, with before/after comparisons against raw CMake.
- ARCHITECTURE.md: how it is designed, layer by layer.
- ROADMAP.md: where it is going.
- docs/benchmarks.md: measured free-threaded parallelism wins, with the method.
- CONTRIBUTING.md: why your scars from CMake make you exactly the contributor we need.
- Runnable examples/, smallest first, up to a full real-world capstone.
Your build script should be the most boring file in your repository. Let us make it boring together.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cmakeless-0.5.2.tar.gz.
File metadata
- Download URL: cmakeless-0.5.2.tar.gz
- Upload date:
- Size: 190.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
afedc3a1f5c2741f558cc22b6056eb44eceedd95eddee16acd64857108141e5d
|
|
| MD5 |
2def200dfef3e7d48b40ed3b24fcda27
|
|
| BLAKE2b-256 |
dd69cc7f98b677dcf54fecbd861032c836e46335f19e88917df8ea7cfd992974
|
Provenance
The following attestation bundles were made for cmakeless-0.5.2.tar.gz:
Publisher:
release.yml on bbalouki/cmakeless
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cmakeless-0.5.2.tar.gz -
Subject digest:
afedc3a1f5c2741f558cc22b6056eb44eceedd95eddee16acd64857108141e5d - Sigstore transparency entry: 2065282592
- Sigstore integration time:
-
Permalink:
bbalouki/cmakeless@a4f8e4ef33c086cbbc0f48492e3155d6e4e68dda -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/bbalouki
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a4f8e4ef33c086cbbc0f48492e3155d6e4e68dda -
Trigger Event:
push
-
Statement type:
File details
Details for the file cmakeless-0.5.2-py3-none-any.whl.
File metadata
- Download URL: cmakeless-0.5.2-py3-none-any.whl
- Upload date:
- Size: 121.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71d53d6d356443d1c45ab3efa6e1621db934025c44bf641d5e0b84143628a400
|
|
| MD5 |
b20ed593b206880aca0a12bcb2b677ed
|
|
| BLAKE2b-256 |
998f6ce366cbfd03db7dc09ebe998a0580a0361f2f1d2a940f386f51657b5fe4
|
Provenance
The following attestation bundles were made for cmakeless-0.5.2-py3-none-any.whl:
Publisher:
release.yml on bbalouki/cmakeless
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cmakeless-0.5.2-py3-none-any.whl -
Subject digest:
71d53d6d356443d1c45ab3efa6e1621db934025c44bf641d5e0b84143628a400 - Sigstore transparency entry: 2065283005
- Sigstore integration time:
-
Permalink:
bbalouki/cmakeless@a4f8e4ef33c086cbbc0f48492e3155d6e4e68dda -
Branch / Tag:
refs/tags/v0.5.2 - Owner: https://github.com/bbalouki
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a4f8e4ef33c086cbbc0f48492e3155d6e4e68dda -
Trigger Event:
push
-
Statement type: