Repository navigation
Conversation
Apache-2.0 is the more common choice for developer tooling that teams vendor into their own build, and its explicit patent grant removes a question MPL leaves open for a tool that generates files into a proprietary repository. The per-file headers are gone rather than rewritten: one LICENSE and NOTICE at the root state the terms once, and 155 files repeating them added noise to the top of every module. Files that changed for other reasons in this release drop their header in their own commit. Also fixes the LICENSE file itself, which was a reflowed copy pasted from a web page with curly quotes and an Exhibit B truncated mid-sentence; the Apache text is the canonical one. CONTRIBUTING gains an inbound=outbound clause (Apache-2.0 section 5), so contributors know no separate CLA is required.
An argument carrying a backslash, a space, or a quote reached CMake's
parser raw. Any absolute Windows path did it: sys.executable emitted
COMMAND C:\Python\python.exe, where CMake reads \P and \p as escapes
and the configure step dies with a syntax error. An argument with a
space silently became two arguments instead.
VERBATIM never covered this. It governs how CMake hands a parsed
argument to the shell, not how the CMake language parses the argument
list in the first place, so the "argument lists, never shell strings"
promise had a hole underneath it.
Each argument is now escaped and wrapped in quotes, which also closes
the injection path where an argument containing a quote could end the
argument early and have the rest read as CMake code. Dollar signs are
left alone on purpose, so a caller passing ${CMAKE_COMMAND} still gets
it expanded.
The custom-command example invoked a bare "python", which resolves to the Microsoft Store stub on a default Windows install and does not exist at all on Linux images that ship only "python3". The example that teaches code generation could therefore not run the code it generated. Using the interpreter that is already running the build description is the portable answer, and the comment says so, because this is exactly the kind of thing a reader is about to copy into their own project. Found by the new integration suite on its first run.
The last feature Phase 5.6 owed. A library declares its module interfaces with modules=[...] and consumers just write "import geometry": no header, no include directory, no include guard, and for a module-only library no sources= either. Visibility is the part worth getting right. A library's interfaces are PUBLIC because consumers need the built module interface to import them at all; an executable's are PRIVATE, since nothing links against one. That is why the block is emitted per target kind rather than from the shared settings path. The CMake floor is computed per project rather than raised globally. FILE_SET CXX_MODULES needs 3.28, but making every user of every feature pay for one they may never touch is not a trade worth making, so a project declaring no modules still emits 3.25, byte for byte as before. The existing golden files are the proof: none of them moved. Four freeze-time checks, each saying what to try next: the target must compile as C++20 or later, a file cannot be both a source and a module interface, a header-only library cannot declare one because an INTERFACE library compiles nothing, and installing a target that exports one is refused while CMake's own support for shipping built module interfaces is still behind an experimental flag. Refusing beats emitting CMake that breaks on the next CMake release. Modules also need a generator that scans for dependencies, so "cmakeless doctor" gained a check that names whichever of the CMake version or the generator falls short.
cmakeless.api's docstring claimed it "re-exports every public class and function from its submodules" and quietly omitted three names. The other four layer packages never made the claim but had the same gap, between one and eleven names each, so reaching parts of a layer meant importing from a sibling module that the architecture calls private. Public names defined in underscore-prefixed modules now surface from cmakeless itself, for the same reason: a user should never have to write cmakeless._parallel to reach parallel_map. That covers the version floors, BUILD_SCRIPT_NAME, and the free-threading helpers. deps had two different constants both named MANIFEST_NAME, in conan and vcpkg, which is precisely the collision a package-level re-export surfaces. They are now CONAN_MANIFEST_NAME and VCPKG_MANIFEST_NAME; both are private machinery, so renaming them costs nothing. A new test enforces all of it, so the docstring cannot drift from the truth again: every public name a submodule defines must appear in its package's __all__, and every __all__ entry must resolve.
argparse had the CLI describing itself twice: a chain of helper functions registering flags by verb-name membership tests, then a _run_verb that pulled every possible flag back out of one flat Namespace with getattr defaults, because no verb accepts them all. Typer lets each verb declare exactly the options it takes, as typed parameters mypy checks, and the getattr guessing disappears. Nothing a user or a script can observe changes: same twelve verbs, same flags and short flags, same output strings, same exit codes (0, 1 on a CMakeless error, 2 on a usage error), and main(argv) keeps its signature for the console script and python -m. The existing CLI test suite passes untouched, which is the point of keeping it. Help text now comes from each command's own docstring rather than a parallel dict of help strings that could drift from it. Rich markup and pretty tracebacks are switched off deliberately. This CLI's output is plain text that scripts and CI logs read line by line, and a terminal-width-sensitive renderer would break that. Typer becomes the one runtime dependency, and only cli.py imports it, so a cmakelessfile.py that does "import cmakeless" still pulls in nothing but the standard library.
"Public API frozen and audited" was going to mean whatever a reviewer happened to notice. It now means a test: every name in cmakeless.__all__, every public method those classes inherit as well as define, and every full signature, snapshotted to a golden file. Inherited members are included deliberately, because dropping add_sources from the shared target base is exactly as breaking as dropping a method a class declares itself, and a vars()-only snapshot would have missed it. Regenerating the snapshot is a deliberate act with an obvious moment to write the CHANGELOG entry, which is the behaviour we want from an API freeze. Nothing was ever deprecated during 0.x, so there was nothing to delete; what was missing was the mechanism. cmakeless.deprecated and warn_deprecated_argument give the first 1.x deprecation one house style: a DeprecationWarning naming the replacement and the release that removes it, with the note appended to the docstring so readers of the API reference see it too.
The unit suite proves each feature against a synthetic model. Nothing proved the thing users actually copy from: the examples tree. Each example is now copied to a temp directory and its cmakelessfile.py is executed exactly as a user would run it, so an API change that breaks documented usage fails here instead of in somebody's first five minutes. It earned its place immediately, finding two real bugs on its first run: unquoted custom-command arguments, and an example invoking a bare "python" that does not exist on Windows. Gated the way the existing end-to-end tests are, so a machine without CMake still runs the rest, and examples that fetch dependencies need CMAKELESS_NETWORK_TESTS=1.
A new stability page states what the version number actually covers, because "1.0" on its own tells a reader nothing about whether their import will still work next year. It names the three things that are public (the __all__ surface, the generated file formats, the CLI), says plainly that everything else changes freely, and points at the three tests that enforce it rather than asking anyone to take it on trust. FEATURES gains the modules section, including the honest part: what is deferred and why. The cookbook gets the three recipes someone adopting modules will actually look for, and the migration guide records module installation and import std as things that still have no equivalent. ARCHITECTURE's zero-dependency paragraph was no longer true once the CLI moved to Typer. It now states the rule that replaced it, which is more useful anyway: the dependency is confined to cli.py, so the four layers stay standard library only.
Phase 5.6 is complete, so the version number says so: Development Status moves to Production/Stable and the roadmap records the phase as shipped along with what it deliberately deferred. From here the version number is a contract rather than a description. Breaking changes mean 2.0, and anything removed gets a deprecation warning naming its replacement for at least one minor version first.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…bject A project with a Python module or a shared library failed to link on Linux as soon as it depended on anything fetched from source: the dependency's static archive was compiled without -fPIC, and ld refuses to put that inside a shared object. Our own targets already carry POSITION_INDEPENDENT_CODE, but a FetchContent dependency is a subproject we do not own and cannot set properties on, so the directory-level variable is the only lever that reaches it. That is why this one global is deliberate rather than an oversight, and the comment in the generated file says so. Emitted only when the project actually builds something shared, so a project of static libraries and executables produces the same bytes as before. Found by the new integration suite building examples/08_capstone on Linux; macOS and Windows never showed it, since PIC is the default on one and meaningless on the other.
The snapshot walked the whole MRO down to object, so every exception class carried BaseException's add_note, with_traceback and args into the golden file. Those are CPython's API, not ours, and CPython renders them differently per version: add_note reads as "add_note(...)" on 3.12 and "add_note(self, object, /)" on 3.13, so the file could only ever match the interpreter that generated it. Every 3.12 job failed on it. Walking only the bases defined inside cmakeless keeps what the snapshot is for, catching a method dropped from a shared target base, while leaving the standard library to version itself.
Adding tests/integration to the default test path put every example build, including the ones that fetch and compile fmt, GoogleTest and pybind11, into all six matrix jobs. That is minutes of compiler time repeated per Python version to test something that does not depend on the Python version, and it made a plain local pytest slow enough that contributors would start skipping it. The two build tests now carry an "integration" marker and are deselected by default. CI runs them in a dedicated job across the three operating systems on one Python, which is the matrix that actually matters for compiler and generator behaviour. The two consistency checks stay in the default run: they only read files, cost nothing, and catch an example that was added without a catalogue entry.
Patch coverage flagged the paths a passing machine never takes, and each one turned out to be worth pinning rather than waving through. The CLI's exit-code mapping is the interesting one: scripts branch on those numbers, so a normal return, a bare exit, an integer exit and a message-style exit are now asserted against a stubbed command layer instead of being whatever the command layer happens to do. Also covers doctor reporting a failed required check as exit 1, the scaffold deriving a valid project name from a directory name that is not one, an unparseable cmake --version, and the generator helpers that decide whether a generator can scan for modules. cli.py, doctor.py and generators.py are now fully covered.
A .coverage database slipped into the previous commit. It is a local, machine-specific binary that changes on every test run, so it has no business in the history; the ignore rules now cover it and the report formats alongside it.
Patch coverage was reporting 95.57% against a 97.56% target, and splitting the number apart showed two unrelated things wearing one label. The real gap: a module interface produced by add_command() skips the exists-on-disk check, because a code-generation step's output does not exist until the step runs. That branch shipped untested, so generated module interfaces are now covered both ways, one that is a declared command output and one that is simply missing. The rest was measurement. A bare --cov measures the test suite as well as the package, so deselecting the example builds counted their own bodies as untested code, and the patch number moved for a reason that said nothing about whether cmakeless is tested. Coverage is now scoped to the package, which is the thing the number is supposed to describe.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes Phase 5.6 of the roadmap and ships v1.0.0, plus the two
cross-cutting changes requested alongside it: the license moves to Apache-2.0
and the CLI moves to Typer.
api/modules.pyis CMakeinclude()reflection from Phase 5.3; a repo-wide search forCXX_MODULES,FILE_SET,.cppmand.ixxreturned nothing. Nowadd_library(..., modules=[...])emitstarget_sources(<t> <vis> FILE_SET CXX_MODULES FILES ...),PUBLICfor alibrary so consumers can import it and
PRIVATEotherwise. A module-onlylibrary needs no
sources=at all.deleted" was a no-op. What was missing was the mechanism, now
cmakeless.deprecated(...).tests/unittests/test_public_api.pysnapshots every exported name, itsinherited methods and full signatures.
tests/integration/buildsevery
examples/project from its realcmakelessfile.py.docs/stability.mdstates what the version numbercovers and what it does not.
The CMake floor moves per project, not globally
FILE_SET CXX_MODULESneeds CMake 3.28, but a project that declares nomodules still emits
cmake_minimum_required(VERSION 3.25), byte for byte asbefore. Every pre-existing golden file is unchanged, which is the evidence
that non-module projects are unaffected.
Two real bugs the new integration suite found on its first run
82a9f61). Any argumentwith a backslash (every absolute Windows path, e.g.
sys.executable), aspace, or a quote reached CMake's parser raw, producing a configure-time
syntax error or silently splitting into several arguments.
VERBATIMgoverns the shell handoff and never protected the CMake-language parse, so
the "argument lists, never shell strings" promise had a hole under it. This
also closes an injection path.
examples/09_build_languageinvoked a barepython(1ef1ab6), whichis the Store stub on Windows and absent on Linux images shipping only
python3.Checklist
release tracked in
docs/ROADMAP.md.tests/unittests/, mirroringsrc/.Emitter changes include golden-file tests with deterministic inputs
(
golden/cxx_modules.cmake, regeneratedgolden/custom_commands.cmake).docstring (enforced by
test_code_standards.py).CHANGELOG.mdupdated — a full[1.0.0]entry, folding in the itemsthat were sitting under
[Unreleased].FEATURES.mdsection 13,cookbook.md,migration.md,ARCHITECTURE.md,ROADMAP.md,README.md, newstability.md.ruff check .andruff format --check .pass locally.mypy srcpasses locally (strict mode).Locally: 912 passed, 13 skipped;
mkdocs build --strictclean.