Skip to main content

Write C++ builds in Python, get clean CMakeLists.txt: a modern, type-safe alternative to hand-written CMake.

Project description

CMakeless

CI CodeFactor

License: MPL 2.0 Typed

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 build and been afraid to touch the file again, this project is for you.


Table of contents


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" and a b c at 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) or target_link_libraries(app fmt)? When do you need PUBLIC vs PRIVATE vs INTERFACE? 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:

  1. 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.
  2. 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.
  3. Boring, readable output. The generated CMakeLists.txt is 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.
  4. 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.

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.py is 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cmakeless-0.5.0.tar.gz (187.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cmakeless-0.5.0-py3-none-any.whl (120.1 kB view details)

Uploaded Python 3

File details

Details for the file cmakeless-0.5.0.tar.gz.

File metadata

  • Download URL: cmakeless-0.5.0.tar.gz
  • Upload date:
  • Size: 187.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cmakeless-0.5.0.tar.gz
Algorithm Hash digest
SHA256 2f491831b08b3e2a5a6475c95457664483b3197a87f362840c3fe1c58ca9a010
MD5 495885fa5bf363244bd24c8ef633d721
BLAKE2b-256 85cf3659cdebbbef77231fd7836647c3c3026840227471944206af8f8b96495b

See more details on using hashes here.

Provenance

The following attestation bundles were made for cmakeless-0.5.0.tar.gz:

Publisher: release.yml on bbalouki/cmakeless

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cmakeless-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: cmakeless-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 120.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cmakeless-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6034bfffad28d030d30cbc215b7b2203e696c42672747cd05b1cb1150ec91108
MD5 736e7e75e1e250a644a01abd2e7f4c71
BLAKE2b-256 9e3ace3bedc18b637c845611dd73388cf45ff6128588f7acf4ae6e78ab10738e

See more details on using hashes here.

Provenance

The following attestation bundles were made for cmakeless-0.5.0-py3-none-any.whl:

Publisher: release.yml on bbalouki/cmakeless

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page