Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6f4c056
0.21.0: deps-cmake compiles a cross build's subproject from the engin…
Sunrisepeak Oct 29, 2026
d82c89b
ci: the AppImage fixture asks that the refusal lists appimage, not th…
Sunrisepeak Oct 29, 2026
2b0d90c
deps-cmake: a contract word with a space is quoted in the flags strin…
Sunrisepeak Oct 29, 2026
3451dba
Merge remote-tracking branch 'origin/main' into wip/deps-cmake-contract
Sunrisepeak Nov 4, 2026
8d4b150
fix: keep accelerator targets with their implementation
Sunrisepeak Nov 4, 2026
d215e0d
feat: report optional action progress from plugin workers
Sunrisepeak Nov 4, 2026
3e9fc36
fix: propagate embedded payload changes through object storage
Sunrisepeak Nov 4, 2026
aa2c16e
fix: keep the progress writer usable by standalone workers
Sunrisepeak Nov 4, 2026
26ff543
fix: preserve Unicode progress channel paths on Windows
Sunrisepeak Nov 5, 2026
9e44847
ci: qualify plugins with published mcpp 2026.11.5.1
Sunrisepeak Nov 5, 2026
79f7ba6
fix: preserve CMake target arguments and cache selections
Sunrisepeak Nov 5, 2026
2bb2333
test(deps-cmake): verify independent cross subproject link
Sunrisepeak Nov 6, 2026
6fae199
refactor: take the C library from std; include only platform headers
Sunrisepeak Nov 7, 2026
6deadfb
ci: mcpp 2026.11.7.1 for every lane; cross subprojects that link need it
Sunrisepeak Nov 7, 2026
450d756
build(deps): plugins-progress takes mcpplibs.os ^0.3.1
Sunrisepeak Nov 7, 2026
7e3aa65
docs(sycl): a static library of several NVIDIA architectures is not s…
Sunrisepeak Nov 7, 2026
7bab6ae
docs(sycl): the archive limitation is tracked in mcpp#899
Sunrisepeak Nov 7, 2026
1f0b42e
test(sycl): a library of two NVIDIA architectures links into a program
Sunrisepeak Nov 7, 2026
37497fc
build: mcpplibs.os as one release, named by its identity
Sunrisepeak Nov 7, 2026
8d0afd7
refactor: every unit reaches the system through mcpplibs.os; no heade…
Sunrisepeak Nov 7, 2026
6305d89
fix: capture appends a program's standard error read from its own pipe
Sunrisepeak Nov 7, 2026
e77b920
fix: a helper that reaches mcpplibs.os is not inline, which GCC 16 ca…
Sunrisepeak Nov 7, 2026
85fdb0c
ci: MCPP_VERSION is the newest engine a lane needs, the floor the old…
Sunrisepeak Nov 7, 2026
c8ed527
build: mcpplibs.os 0.4.0, the release mcpp's own modules name
Sunrisepeak Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .agents/docs/2026-11-05-cmake-contract-argv-and-link-inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
subject: cmake-contract
status: active
---

# CMake contract arguments and consumer link inputs

Audience: maintainers implementing and reviewing the cross-build adapter.

This record explains the failure boundaries, the selected representation and
the evidence needed before releasing the changes in PR #52. It does not claim
qualification of every CMake generator or every foreign executable platform.

## Confirmed failures and ownership

| Failure | Evidence | Owner |
|---|---|---|
| Joining JSON arguments loses literal quotes, backslashes and tabs | The previous reader changes values and argument boundaries under the native parser | Plugin adapter |
| LLVM's GNU response parser drops a quoted empty argument | A native `-D` followed by an empty word is invalid; the corresponding response-file invocation consumes the next valid definition and succeeds | Compiler-parser limitation; adapter rejects the lossy representation |
| A single raw flags string needs different dollar escaping in Make compilation and its direct link script | Actual compiler controls fail despite successful configuration | Plugin adapter |
| An independent subproject executable requires the library that the same subproject has yet to install | Fresh actual musl cross-build fails at that future archive | Core target-contract export |
| Reconfiguration ignores a newly introduced toolchain while retaining old flags | Actual previously configured CMake directory warns that the toolchain is unused | Plugin build-directory identity |
| Switching an existing cross-build from Ninja to Unix Makefiles reuses its incompatible cache | Actual mcpp build fails with the generator mismatch diagnostic | Plugin build-directory identity |
| Changing `options::system_name` retains the previous target system | Actual mcpp build requests Generic but the configured project still reports Linux | Plugin build-directory identity |
| A retargeted Clang driver is identified as a Linux compiler although it emits Windows COFF | A no-SDK static-library control reports `Clang\|\|GNU\|Linux` and names the archive `libprobe.a` | Plugin compiler-ID configuration |
| A source path containing an apostrophe is split in the dependency graph | Pure CMake 4.4.2/Ninja control reproduces without mcpp, a toolchain or response files | External CMake/Ninja path handling |

The consuming package's own libraries are not target configuration. Core PR
#889 excludes package link flags and libraries from the foreign-build contract
while retaining them on the consumer's actual link. No CMake primitive belongs
in that correction.

## Selected representation

The action supplies a generated CMake toolchain. It reads the engine contract
when configuration runs, after the build program has planned the action.
The generated toolchain is an explicit action input. Its contract path is
propagated through `CMAKE_TRY_COMPILE_PLATFORM_VARIABLES`, so ordinary compiler
ABI checks and source-file compile probes load the same settings.

JSON arrays are read by index and written to compiler response files. GNU
driver quoting and MSVC command-line quoting are separate encodings selected
by the contract's dialect. MSVC files include a UTF-8 BOM. CMake receives each
response-file reference as one native option and performs generator escaping;
literal argument contents do not pass through Ninja, Make or a shell.
One grouped reference also preserves repeated options and their order.
Literal empty GNU argument words are rejected with a configure diagnostic:
Clang's GNU response parser discards them even when quoted. Empty string
values within nonempty options remain supported. No process wrapper or
second argument grammar is introduced for this invalid-invocation boundary.

The response-file name hashes the dialect and encoded contents. Changed
arguments therefore change the generated compile command. Identical contents
are retained without rewriting the file. A new representation uses its own
build-directory key, since an old cache does not begin loading a toolchain
merely because a later configure command names one.

Immutable generator selections (`-G`, `-A`, `-T`, and
`CMAKE_GENERATOR*` cache definitions) and the effective cross system name
participate in the build-directory identity. Joined and separated short
options, and typed and untyped cache definitions, use the same identity.
Ordinary `-D` definitions reconfigure the existing directory. Returning to
a previous generator and system reuses that selection's directory.

Cross-build contract options follow CMake's language and configuration flags.
`options::config` continues to select the CMake configuration; a conflicting
contract profile option takes precedence in the compiler argument order.
The subproject still owns its language standard and target-specific options.
Compiler-ID probing does not consume directory compile options. The adapter
therefore also supplies `CMAKE_<LANG>_COMPILER_TARGET` from the last actual
`--target` or `-target` compiler option, retaining the original argument words.
It does not substitute `contract.target`, which may be a logical platform
alias. This follows CMake's documented [Clang cross-toolchain configuration](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-using-clang).
CMake 3.19 is the minimum for reading the JSON contract.

## Actual controls

The adapter passes all 52 current plugin-logic cases and the existing actual
musl cross-build. Compiler controls use its generated toolchain, rather than
a second implementation of the serializer. GCC and Clang execute C and C++
programs under Ninja and Unix Makefiles with literal quotes, backslashes,
dollar signs, semicolons, tabs, empty strings and Unicode. They also check
language isolation, repeated-option order, actual compile probes, unchanged
objects after reconfiguration and invalidation when one contract word changes.
Linux controls inspect the actual ELF runpath for literal `$ORIGIN` content.
A separate empty-word control places an empty argument after `-D`. GCC and
Clang reject that native invocation; removing the word consumes a valid
fallback definition and succeeds. The generated helper rejects that empty
word during configuration, preventing LLVM's response parser from silently
changing the invalid invocation into a valid one.

An actual clang-cl invocation consumes the MSVC response file and produces
COFF; its initialized data retains the expected literal values. This is a
compiler-parser control, not native MSVC or Visual Studio generator coverage.

An actual GNU-style Clang 22 driver is retargeted to Windows without an SDK.
Ninja, Unix Makefiles and Ninja Multi-Config build static libraries whose
members are AMD64 COFF. The resulting identity is
`Clang|MSVC|GNU|Windows`, pointer width is eight, and the archive is named
`probe.lib`. A deliberately different logical contract target remains unused
for compiler identity. The same control fails with the old reader at its
identity assertion, although that reader can emit COFF.

The real adapter also passes ten successive selections: Ninja/Linux,
Make/Linux, Make no-op, Ninja return, Ninja no-op, Ninja/Generic, Generic
no-op, Linux return, Ninja Multi-Config/Linux and Multi-Config no-op.
Each build executes the actual musl program. Incompatible selections have
distinct CMake directories; return and no-op cases retain object timestamps.
This matrix passes with the diagnostic corrected core and independently with
the published mcpp 2026.11.5.1 binary. It does not depend on the consumer-link
correction. Before the system-name key correction, the first five selections
pass and the Generic selection still reports Linux.

The actual sidecar failure remains with the response-file adapter and the old
core. Replacing only two core modules fixes it: the library installs, both the
independent sidecar and the consuming musl program execute, and the consumer's
Ninja graph retains its archive. Extended core E2E 982 fails with the old module
and passes with the corrected contract while retaining the consumer link flag.

The apostrophe negative remains separate. The positive argument test keeps an
apostrophe in the response-file directory; source paths contain spaces,
Unicode and dollar signs. It does not claim to fix native dependency parsing
of source paths containing apostrophes.

## Remaining qualification

The response-file and cache-selection regressions can run against the
currently published mcpp 2026.11.5.1. They do not require waiting for the core
release. Native MSVC, Visual Studio and Xcode generators, custom link drivers
and standalone executables over graph-supplied runtimes are not established
by the Linux compiler controls. The original platform CI remains required.

Core 2026.11.5.2 must complete its final CI, independent ecosystem review,
publication and ordinary installation. Then the plugin CI pin and real
sidecar fixture can follow the public version. The package's protocol floor
remains distinct from a feature's required bug-fix version. All final plugin
CI and review gates remain required before merge and release.

The final sequence is local adapter verification and documentation, a
reviewable PR #52 update, public core installation and sidecar integration,
then the full plugin CI and ecosystem review. The sidecar uses ordinary
prebuilt musl; it does not establish graph-runtime executable support.

Public reports contain fixed assertions and synthetic fixtures. Raw user logs,
usernames, private paths and private project names are not published.
85 changes: 85 additions & 0 deletions .github/scripts/check-cmake-cache-identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Exercise CMake cache selection through the real mcpp adapter."""
import argparse
import json
from pathlib import Path
import shutil
import subprocess
import tempfile


def checked(command, project, log):
result = subprocess.run(command, cwd=project, stdout=log, stderr=subprocess.STDOUT)
if result.returncode:
log.flush()
details = Path(log.name).read_text(errors="replace")[-8000:]
details = details.replace(str(project), "<fixture>").replace(str(Path.home()), "<home>")
raise AssertionError(f"mcpp exited {result.returncode}\n{details}")


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mcpp")
parser.add_argument("--target", default="x86_64-linux-musl")
args = parser.parse_args()
binary = str(Path(shutil.which(args.mcpp) or args.mcpp).resolve())
repository = Path(__file__).resolve().parents[2]
fixture = repository / "tests/cmake-cross"
with tempfile.TemporaryDirectory(prefix="mcpp-cmake-cache-") as temporary:
project = Path(temporary) / "project"
shutil.copytree(fixture, project, ignore=shutil.ignore_patterns("target", ".mcpp", ".xlings"))
manifest = project / "mcpp.toml"
text = manifest.read_text().replace('path = "../.."', "path = " + json.dumps(str(repository)))
assert text != manifest.read_text(), "fixture dependency path was not replaced"
manifest.write_text(text)
cmake = project / "answer/CMakeLists.txt"
cmake.write_text(cmake.read_text() + r'''
file(WRITE "${CMAKE_BINARY_DIR}/mcpp-cache-probe.json"
"{\"epoch\":${MCPP_CACHE_PROBE_EPOCH},\"generator\":\"${CMAKE_GENERATOR}\",\"system\":\"${CMAKE_SYSTEM_NAME}\"}")
''')
selections = [
("Ninja", "Linux"), ("Unix Makefiles", "Linux"), ("Unix Makefiles", "Linux"),
("Ninja", "Linux"), ("Ninja", "Linux"),
("Ninja", "Generic"), ("Ninja", "Generic"), ("Ninja", "Linux"),
("Ninja Multi-Config", "Linux"), ("Ninja Multi-Config", "Linux")]
seen = {}
for epoch, (generator, system) in enumerate(selections, 1):
objects = {p: p.stat().st_mtime_ns for p in project.glob("target/.build-mcpp/out/*/cmake-cross/deps-cmake/answer/build-*/**/*.o")}
options = ["-G", generator, f"-DMCPP_CACHE_PROBE_EPOCH={epoch}"]
words = ", ".join(json.dumps(value) for value in options)
(project / "build.mcpp").write_text('''import std;
import mcpp;
import mcpp.deps.cmake;
int main() {
mcpp::deps::cmake::options o;
o.source = "answer";
o.libraries = {"answer"};
''' + f' o.cache_args = {{{words}}};\n o.system_name = "{system}";\n' + ''' return mcpp::deps::cmake::use(o) ? 0 : 1;
}
''')
with (project / "private.log").open("w") as log:
checked([binary, "build", "--target", args.target, "--profile", "dev"], project, log)
current = []
for path in project.glob("target/.build-mcpp/out/*/cmake-cross/deps-cmake/answer/build-*/mcpp-cache-probe.json"):
record = json.loads(path.read_text())
if record["epoch"] == epoch:
current.append((path.parent, record))
assert len(current) == 1, "the active CMake configure did not write its current epoch"
build, record = current[0]
assert record["generator"] == generator and record["system"] == system, record
selection = (generator, system)
if selection in seen:
assert build == seen[selection], "returning to a selection lost its CMake cache"
assert all(p.stat().st_mtime_ns == stamp for p, stamp in objects.items() if p.is_relative_to(build)), "unchanged compilation objects rebuilt"
else:
assert build not in seen.values(), "incompatible selections share a CMake cache"
seen[selection] = build
applications = list((project / "target" / args.target).glob("*/bin/cmake-cross"))
assert len(applications) == 1
output = subprocess.check_output([str(applications[0])], text=True).strip()
assert output == "cmake-cross: the subproject was compiled against not glibc", output
print(f"PASS: selection {epoch}: {generator}/{system}; actual target program and cache identity")


if __name__ == "__main__":
main()
Loading
Loading