mcpp 2026.9.27.1: #704-#716, feature tools, artifacts, payload revision, prepare.cppm decomposition - #719
Merged
Conversation
#712 An `[xlings]` address resolves the payload xlings selected. A new module, mcpp.xpkg_version, ports xlings' version grammar (N segments, alpha segments, written-prefix equality for three or more segments, prefix ranges for one or two, operator ranges), and xpkg_payload_at selects among installed directories with it. `libglvnd@1.7` finds 1.7.0.1. #716 A provisioning stamp counts only while every address still resolves to a payload. A removed payload is re-provisioned online and refused offline, naming it. The build cache records the payload directories a build read, so the fast path declines when one is gone. xlings reports what each request resolved to (install_targets, protocol 1.1); mcpp records that per address and answers from the record first. #705 A host tool's tree stamp skips directories that hold their own mcpp.toml, and hashes paths as UTF-8. #710 The requesting build decides a host tool's toolchain once -- `--toolchain`, else the tool package's own (its workspace's, for a member), else the requesting build's host toolchain -- passes it to the sub-build as an override, and records it in the store key. A member built as a tool inherits the workspace root's root-position keys. #713 A member inherits the workspace root's `[xlings.workspace]` entries, conditional rows included; its own declaration of a package wins. #714 An unresolved `x.workspace = true` is refused at every position, naming the entry. `[build] sources = []` infers no library target. A rooted workspace resolves its own `workspace = true` entries. #715 A rule reaches a package's synthesised build program only through a device source whose extension it declared. #704 A host build applies its own `[target.<host-triple>]` row (toolchain, linkage, cxx_runtime), as `--target <host>` did. #707 `emit build-database` builds no host tool; one missing from the store is deferred with the note MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED (SPEC-005 v1.4 R2.5). #708 `mcpp::action::env(name, value)` and `cwd(dir)` (protocol 13), applied by the engine's action wrapper in its named form `__action`. An action with neither keeps the `__action-stamp` command line byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MajuX4J8ewFeWvZJjzRJt5
#709 `[features.<f>] tools = ["<bin>"]` names the package's own programs a feature needs on the build machine. On every edge into the package, the active features' tools are requested as if the edge had written `tools = [...]`: built once for the host, reachable through `mcpp::dep_bin`. A name that is not a `kind = "bin"` target of the package is refused at load, naming the bin targets. #711 `x = { ..., artifacts = ["<bin>"] }` asks for the dependency's program built for the consumer's target and profile, as a link unit of the consumer's own plan, written to `bin/` beside its programs. The unit links the dependency's objects and those of every package it reaches; a package reached only through artifact edges is kept out of the consumer's images, since the edge takes the program and not the code. A package asked for artifacts is scanned and configured here even when all its targets are programs. `${mcpp.artifact:<package>/<target>}` names the output in actions, `mcpp pack` stages it beside the program, and `mcpp run` and the program-selection sites ignore it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MajuX4J8ewFeWvZJjzRJt5
The packaging revision xlings records for a payload (`.xpkg-install.json`, openxlings/xlings#620) is read in one place, `mcpp::xlings::paths::installed_revision`, and used twice. The runtime contract (fingerprint field 11) includes the revision of each runtime payload whose revision is 1 or more, so a C library reinstalled under the same version with changed content selects a new output directory. Revision 0 contributes nothing, so no existing key changes. An index dependency whose recorded revision differs from its descriptor's entry is not installed (#524 A): mcpp's descriptor reader now reads `revision` the way libxpkg does, and a stale payload goes back through xlings, which reinstalls it. A payload with no xlings record is judged by the marker, as before. `mcpp pack --mode self-contained` copies the bundled C library's `lib/locale` and `lib/gconv` from its payload when present and exports LOCPATH and GCONV_PATH in the launcher, keeping a value the user set (openxlings/xlings#621). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MajuX4J8ewFeWvZJjzRJt5
…ors; faster ELF reads Tests. e2e 802 (#704: a host build applies its host row, under either spelling) and 803 (#716: a recorded payload that is gone is refused offline, naming the address and the record). Unit tests for #713 (a member inherits the root's [xlings.workspace] entries and rows; its own wins), #714 (an unresolved `workspace = true` is named in every dependency table) and #710 (a member tool reads its workspace's toolchain; its own wins), for which prepare exports host_tool_declared_toolchain. Version selection. mcpp.xpkg_version gains select_installed, the one rule `mcpp::xpkg_dir` answers with (a literal name, then the xlings grammar, then the highest), and runs the conformance vectors xlings publishes, vendored as modules/versioning/tests/data/semver-vectors.tsv. ELF inspection reads a file with one sized read; filling the vector through an istreambuf_iterator made the post-link check of `mcpp test` take tens of minutes. Specs: SPEC-001 v1.5 (version answers follow xlings; packaging revision), SPEC-004 v1.8 (host row, workspace clauses 8-10, a dependency's programs), SPEC-007 v0.3 (action env/cwd, planning defers host tools, feature tools, artifacts). Docs 04, 05, 06, 07 and 30 in English and Chinese. CHANGELOG and version 2026.9.27.1. Not built or run in this session; the handover lists the verification that remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MajuX4J8ewFeWvZJjzRJt5
… pins gcc; compatibility notes e2e 804 builds a host tool reached through a plain path dependency, which belongs to no workspace: without a [toolchain] of its own the tool is built by the consumer's build-program compiler (llvm 22.1.8 in the test, while the global default on Linux is gcc), and with one it is built by that compiler. The released 2026.9.26.2 fails the first criterion; this branch passes both. e2e 802 read libstdc++'s NEEDED entry under whatever toolchain the machine defaults to, so it failed where the user's default is llvm; the manifest now names gcc@16.1.0. CHANGELOG: the changes an existing project can observe -- an unresolved `workspace = true` is refused (#714), `sources = []` no longer infers a library, and the build-database note code was renamed without an alias. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wrapper entered the action's directory in `extended_length`'s `\\?\` spelling. Windows accepts that as the current directory, but a child started there does not recognise it: on the Windows e2e job the MSYS shell of e2e 799 ran in C:\Windows while the variable reached it. The wrapper now enters the absolute, lexically normal path; POSIX is unaffected, since extended_length returns its argument unchanged there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With clang 20.1.7 and the MSVC STL, `std::optional<std::string>("gcc@...")`
failed to instantiate in this translation unit, which includes gtest's
headers and imports std; the Windows unit-test job reported the only failure
of its 129 tests as that compile error. The test now checks has_value() and
compares the string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kXlingsVersion and every .github pin check_version_pins.sh reads move to xlings 2026.9.27.1 (openxlings/xlings#622), released on GitHub and GitCode and published in the xim index. It is the release that emits install_targets and records a payload's revision, which this mcpp reads first. The vendored semver-vectors.tsv already matches that release's file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First step of the prepare.cppm decomposition (behaviour-preserving): PrepareState holds print_fingerprint, includeDevDeps, extraTargets, overrides and the derived toolPurpose. prepare_build now constructs one and reads these five through it instead of as bare locals/parameters. Everything else is still one function, unchanged. Golden fixture diff against the pre-refactor baseline is empty; mcpp test: 129/129.
P0 (manifest and workspace resolution, the first ~490 lines of prepare_build) produces root, m, effective, wsManifest, runtimeWorkspaceRoot, runtimeSelection, workRoot, planNotes, gitLockAnchors, packageIdentityLockAnchors, cacheMode and a dozen diagnosis-carrying strings/bools that later phases read. All of these move to PrepareState; every read/write throughout the rest of prepare_build now goes through state.<name>. Three classes of false positive from the mechanical rename were found and reverted: unrelated struct members/namespaced functions sharing the same short name (GitClone::root, mcpp::home::root()), a genuinely shadowing local (iface.lexically_normal() bound to a block-scoped root), and the word appearing inside string literals and prose comments. Still one function; still behaviour-preserving. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…nction Cuts P1 -- toolchain spec resolution (docs/21), the get_cfg/runtime- binding/fixup-reporting helpers, the L1 cfg() context, and the target axis/platform derivation -- out of prepare_build into phase1_toolchain_spec_and_axes(PrepareState&), called in sequence from prepare_build. static: PrepareState is TU-local, so a non-static namespace-scope function taking it by reference would otherwise expose a TU-local type through a reachable declaration (the same class of GCC 16.1 constraint as the anonymous-namespace fix for PrepareState itself). Several of P1's locals are closures called by name from much later phases (get_cfg from as far as P13's line ~15300): get_cfg, provide_runtime_payload, report_fixup, msvc_usable_either_origin, native_first_run_spec, tcSpecSource, resolvedAccel, cfgCtx and add_once. Each captured its enclosing locals by reference before this change, which is safe only because prepare_build was one stack frame for its whole call; extracted verbatim they would dangle the moment a later phase called them after P1's frame returned. Fixed by promoting every local such a closure touches into PrepareState (including two, bootstrap_checked and runtimePayloadProvided, that no later code referenced by name and so were not caught by the compiler -- only by reading each closure's body) so the default by-reference capture now closes over the state reference itself, which outlives every phase. report_fixup's capture of fixupNoticed is by value (a shared_ptr) and stays as-is: copying it is already safe regardless of lifetime. get_cfg's default argument does not survive type-erasure into std::function; its 31 call sites that relied on the default now pass it explicitly. targetPlatform has no default constructor (PlatformKey's is protected) and is held as optional<TargetPlatform>, dereferenced at each of its 7 use sites. kCurrentPlatform is a pure constant with no state dependency; hoisted to file scope instead of PrepareState. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…unction P2 is two statements: declare resolve_target_toolchain and assign its ~1000-line closure body (defined here, called from P5 -- and once, recursively, from inside itself on the Windows first-run pass). Cut into phase2_define_toolchain_resolver(PrepareState&). The closure itself becomes a PrepareState member for the same reason as P1's helpers: it is called by name from P5, long after P2's own stack frame is gone. Its internal references were already state.<name> from the P1 promotion pass (that rename covered this whole range); the only new member is the closure itself. No new compiler errors and no manual capture audit needed beyond that: every local this closure declares is declared INSIDE its own body (a fresh allocation on each call, never captured from an enclosing scope), so nothing else here has the P1-style escaping-capture hazard. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
… function P3 resolves the host toolchain build.mcpp itself compiles/runs with (host_spec_for_build_program, host_tc_for_build_program -- both called from as far as P9/P11 -- and their hostTcCache), the target sysroot, and the project's .mcpp/ directory (custom indices, [xlings] deps for the root). Cut into phase3_xlings_before_graph(PrepareState&). Same closure-capture treatment as P1/P2: host_tc_for_build_program's only new capture beyond already-promoted state is hostTcCache, which no later CODE names directly (only the closure's own body touches it) and so needed a manual read of its body rather than a compiler error to find. runtimeOwnerManifest was `const auto&` aliasing wsManifest or m; PrepareState members cannot be references, so it is now a raw pointer (both referents already outlive every phase), dereferenced at its call sites. targetSysroot and materializeRootRuntime are read only within the same block that computes them and stay ordinary locals. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
The very first rename (print_fingerprint/includeDevDeps/extraTargets/
overrides/toolPurpose -> state.<name>) was a plain sed word-boundary
substitution with no protection for string literals or comments,
unlike every rename since. A second, targeted pass found and fixed
what it corrupted: "overrides" used as a plain English verb inside
seven user-facing error/diagnostic strings and comments (e.g. "a
per-target C++ runtime contract state.overrides the project default"),
and "root" meaning "the root package/manifest" -- a different word
from the state.root PATH -- in ten more diagnostic strings ("declare
'{}' in the state.root to settle it"). None of these are exercised by
the golden fixture corpus, so this was found by scanning every string
literal in the file for a state.<name> token and reading each hit, not
by the diff. Rebuilt clean; golden fixture diff against the baseline
is empty; mcpp test: 129/129.
The largest phase (~3400 lines): packages[], the worklist walk over [dependencies]/[dev-dependencies]/[build-dependencies] with its cycle detection, xlings address unification, feature-dep forwarding, provisioning (fillXpkgDirs/fillDepDirs), and directive-tail folding. Cut into phase4_dependency_graph(PrepareState&). Escaping-closure audit (the design's flagged risk for this phase): of roughly 50 lambdas P4 declares, 8 are called again from P6/P7/P13 (graph_xlings_split, computeUsageRequirements, adoptActionOutputs, fillXpkgDirs, fillDepDirs, markDirectiveTail, bareBindingsFor, compilesHere) and needed promoting. Six more (isProgramOnlyPackage, appendUniqueFlags, appendUniquePaths, appendUniquePath, isArtifactPackage, publishedNamesFor) never escape by name themselves but are captured by reference from inside one of the eight -- exactly the hazard the compiler cannot see, found by reading each escaping closure's body rather than by a build error. The other ~35 lambdas (loadVersionDep and the rest of the per-dependency resolution machinery) are declared and fully consumed inside this one phase and stay local. Five local struct types backing promoted members (DepCacheIdentity, GitLockIdentity, DependencyEdge, GraphRequest, DependencyLinkForm, ResolvedKey, ResolvedRecord) moved to file scope, before PrepareState, since a struct defined inside a phase function cannot be named by a member declared earlier in the file. Two namespace aliases (prov, dg) used again in P7/P9 moved the same way, for the same reason. Three real local shadows of promoted names, found only by re-checking every occurrence rather than a sample (`resolved` in particular has 114 matches, nearly all the English past participle in comments/messages): `root`-style path-DFS locals literally named `state` in the cycle- detection block (renamed to visitState -- shadowing the PrepareState parameter is legal C++ but this block also reads state.packages/ state.dependencyEdges, which resolved to the wrong `state` and produced "vector<int> has no member packages"), and two/three independent `resolved` locals from resolve_semver/resolve_package_name whose tiny scopes were reverted to plain `resolved`. get_cfg-style default-argument loss recurred for fillDepDirs (one call site relied on its third parameter's default); fixed by passing nullptr explicitly. A generic lambda (foldDirectiveTailIntoPrivateBuild, `auto& pkg`) cannot be a std::function member; concretized to PackageRoot&, its only call-site type. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…ase function P5 decides whether the graph supplies a target row's system, folds a dependency's compiler requirement into tcSpec, and refuses the convention/capability conflicts (#704's target matrix). No new cross-phase state: it only reads members P0-P4 already promoted and calls state.resolve_target_toolchain(). Zero compile errors on first extraction. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…rst rename The recursive prepare_build(...) call for a host tool sub-build named its own arguments in /*param=*/value comments; the original word- boundary rename touched these too (block comments were never excluded, only // comments and later hardened against). A file-wide scan for /*...state.<name>...*/ found exactly these three and nothing else. Golden fixture diff against the baseline is empty.
tools) as one phase function Cuts feature activation (Cargo-style, additive), capability/ABI requirement accumulation and binding, the target side resolved once against the graph, and host-tool provisioning (including the recursive prepare_build(...) sub-build for a dependency's tool) into phase6_features_and_host_tools(PrepareState&). Combined into one phase function rather than three, matching the design's own file grouping for this range (features.cpp covers P6-P8); the exact phase count is not load-bearing, only that each cut is behaviour-preserving and every resulting file stays under the line cap. The recursive call needed a genuine forward declaration of prepare_build itself: every phase function is textually inserted before prepare_build's own definition, so a call to it (not through a PrepareState closure, unlike resolve_target_toolchain) has nothing to resolve against without one. First attempt put the declaration inside PrepareState's anonymous (TU-local) namespace, which cannot hold an `export` declaration; moved it to right after that namespace closes. Twelve more members promoted, all newly exposed because P6-P8 crossing into its own function separates them from P9-P13, which still read them inline: activeRootFeatures, capProviders/capRequires/capExclusive, abiRequires/abiRequiresExceptions, deviceSourcesByPackage, checkVersionFloors (a closure, but a non-escaping one -- promoted only because P13 calls it by name), resolvedTargetSide, targetSideResolved, targetSideUsage, cxxLayerProviderIndex. None had a further escaping- closure capture beyond what P0-P5 already promoted. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…print) as one phase function Cuts target-side resolution (the layer providers publish once, applied to every package), #519 per-dependency linkage form, the root and dependency build.mcpp runs that happen after resolution, the modgraph scan and validate() pass, and the C++ standard module description into phase9_target_side_and_scan(PrepareState&). One combined function for the same reason as P6-P8: the design groups this range into two files (target_side.cpp, scan.cpp) but the phase-function count itself is not load-bearing. Ten more members promoted (fp, stdBmiPath/stdObjectPath/ stdCompatBmiPath/stdCompatObjectPath, describedStdModule, stdFlagAndDialect, graph_package_entry, scan, report): P13, still inline in prepare_build's tail, reads the fingerprint and the compiled/scanned module graph this phase produces. graph_package_entry is a closure (used for resolution.json's package entries) called again from deep inside P13. Found and fixed, by a systematic diff against the last commit rather than by the compiler alone (these five did not surface as build errors on the first pass because the corrupted call sites still type- checked or only failed transitively): the declaration-stripping script used to promote a member from `Type name = init;` to `state.name = init;` matched some continuation lines of unrelated multi-argument calls that happen to read `<identifier>, state.<name>` -- indistin- guishable from a real `Type name` prefix by the same regex -- and dropped the leading identifier. Five call sites lost an argument this way: mcpp::platform::macos::deployment_target's targetIsMacos boolean (four sites: cAbiTargetIsMacos once, stdTargetIsMacos three times) and predefines::define_tokens's targetOs. All five were caught either by a genuine compile error (wrong arity/type) or by re-reading the format string argument counts; restored from the pre-rename text. A full diff-based rescan against both the last commit and this step's own starting point found no further instances of the pattern. One cosmetic note: this phase function landed textually before phase1 rather than after phase6, because the extraction script's marker search matched the (identical) line in prepare_build's own forward declaration rather than the one immediately before its real definition. Harmless -- nothing calls a phase function before prepare_build itself does, in order -- but out of the intended reading order; left for the file-separation step, where each phase becomes its own translation unit and source order stops being a question. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
P13 -- filling BuildContext (plan, assembly, Windows resources, the global cache, mcpp.lock, resolution.json) -- returns BuildContext directly, so phase13_finish(PrepareState&) returns std::expected<BuildContext, std::string> rather than the void every other phase returns; its call site is prepare_build's own final `return phase13_finish(state);`. No new state members: everything it needs was already promoted for P9-P12's sake. P0 (manifest and workspace resolution) was promoted to PrepareState in an early commit but never actually cut into its own phase function -- an omission from that commit, caught here by checking prepare_build's remaining size after every other phase was cut. Extracted into phase0_manifest_and_workspace(PrepareState&), called first. Zero new compile errors: every local P0 declares was already state.<name> from that earlier pass. prepare_build is now its intended shape: construct PrepareState, clear the global flag-words buffer, call nine phase functions in order, return the last one's result. 21 lines. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…/graph split Preparation for splitting phase4_dependency_graph across two files (graph_load.cpp, graph.cpp), matching the design's own grouping: loadVersionDep -- the git/path/version dependency loader, ~500 lines, recursive -- is defined once and called from the worklist engine that follows it, so it and everything it closes over (the same escaping- closure pattern as every other promoted member) must survive past its own definition point once that split happens. Promoted: worklist, identityBySource, declaringManifest, adoptionsReported, gitCloneBySource, selectorMigrationWarnings, preinstallStack/Done, and the closures cache_index_name, gitMemberDeclaring, qualifiedKey, stateAdoptedIdentity, reportAdoption, index_route,findIndexForNs, resolveSemver, readStrictLuaForCandidate, xpkgLuaMatchesCandidate, dependencyCoordinates, selectDependencyCandidate, loadVersionDep itself. Three more local struct types (WorkItem, DeclaringManifest, GitClone) moved to file scope for the same reason as P4's first five. index_route's default argument (cfg = nullptr) does not survive into std::function; its three no-arg call sites now pass nullptr. Still one phase4_dependency_graph function -- the actual file split follows. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…rklist Required by the file-size cap: phase4_dependency_graph was ~3300 lines, over the 2500 hard cap for whatever file its definition lands in. Split at loadVersionDep's own boundary (matches the design's graph_load.cpp / graph.cpp grouping): phase4a holds the git/path/ version dependency loader; phase4b holds the worklist engine and cycle detection that call it (already state.loadVersionDep from the previous commit, so the split itself needed only kMainConsumer -- a pure constant used by both halves -- hoisted to file scope. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
Required by the file-size cap (phase9_target_side_and_scan was ~2600 lines): split right before the modgraph scan begins, matching the design's target_side.cpp / scan.cpp grouping. No new cross-boundary state needed -- everything on both sides already reads and writes through state.<name>, so this split (unlike P4's) needed no further promotion. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
The preamble's internal helpers (warn_unknown_xpkg_keys through with_index_cause, and the ToolPurpose enum among them) sat in an anonymous namespace, giving them TU-local (internal) linkage. That was fine while every phase using them lived in the same file; once the phases move to their own implementation units, an internal-linkage name is invisible outside the file that declares it (anonymous- namespace scoping is per translation unit, modules or not). Dropped the wrapper: these are now plain module-linkage declarations in prepare.cppm, which -- like pending_flag_words_notes(), already declared that way -- every implementation unit of the module can see without needing to be exported. No behaviour change: nothing outside the module could name these either way. Golden fixture diff against the baseline is empty; mcpp test: 129/129.
…ation files
The physical split (design step 4-5 combined, given the time this
decomposition has already taken): src/build/prepare.cppm keeps only
the module preamble, the exported helper functions (with bodies --
splitting each into a bare declaration plus a definition in a separate
exports.cpp was not done; at 2414 lines the file is under the 2500
hard cap without it, so this was deferred rather than risk a
mechanical error for no required outcome), and prepare_build's own
exported declaration with its default arguments. Everything else moved
under src/build/prepare/:
state.cppm implementation partition :state -- PrepareState,
the five structs P4 needed hoisted, and every
phase function's declaration.
driver.cpp prepare_build's definition.
manifest.cpp P0.
toolchain.cpp P1 + P2.
toolchain_decision.cpp P5 (split out of toolchain.cpp: P1+P2+P5
together were 2526 lines, over the hard cap).
xlings.cpp P3.
graph_load.cpp P4a (loadVersionDep).
graph.cpp P4b (the worklist engine).
features.cpp P6-P8.
target_side.cpp P9.
scan.cpp P11.
plan.cpp P13.
Two visibility problems, both from the same cause -- an implementation
partition does not implicitly see its module's primary interface the
way an implementation unit does, so it must import it explicitly:
- The preamble helpers moved out of their anonymous namespace in the
previous commit for exactly this reason, but ToolPurpose (a type,
not a module-linkage function) still needed an explicit `export`:
:state names it in PrepareState's own member list, and a partition
only sees what its imports export.
- state.cppm added `import mcpp.build.prepare;` for BuildOverrides,
BuildContext, PlanNote, CacheMode and TcOrigin, all exported types
PrepareState's declaration itself names.
Every other file compiled against the split on the first attempt: the
extensive state.<name> threading done in every prior commit was
already correct, this just gave it its own files.
All twelve source files are at or under the 2500-line hard cap; nine
are at or under the 2000-line target. prepare.cppm (2414) and three
implementation files (target_side.cpp 1968, features.cpp 2052,
toolchain.cpp 2136, graph.cpp 2345, plan.cpp 2392) sit between the two
because splitting them further was not required to satisfy the cap
and each remaining cut point would have needed its own escaping-
closure audit.
GCC 16.1 builds this cleanly: the primary interface imports no
partition, :state is an implementation partition imported only by
implementation units, satisfying the constraint verified in
.agents/docs/2026-09-27-mcpp-2026.9.27.1-ecosystem-plan.md §4.3.
Golden fixture diff against the baseline is empty; mcpp test:
129/129 (root) plus every member with tests (versioning, platform,
manifest, toolchain-model, buildmcpp) individually green.
check_file_lengths.sh fails the build if src/build/prepare.cppm or any file directly under src/build/prepare/ exceeds 2,500 lines, wired into ci-linux.yml next to the other pure-text, no-toolchain check_*.sh steps (check_version_pins.sh, check_modules_wiring.sh). Currently passes: 13 files, all at or under the cap (the largest, plan.cpp, is 2,392). CHANGELOG.md: one entry under 2026.9.27.1 describing the internal decomposition -- no behavior change, so it sits in the existing '其他' (other) section alongside the ELF-read-once note, not as its own dated change.
The comment originally attached to the PrepareState struct definition stayed behind in prepare.cppm when the struct itself moved to state.cppm (it was written above the struct's enclosing namespace, not the struct itself), leaving state.cppm's copy undocumented. Restored, updated for the current file layout. Also fixed a comment that had gone stale during the P9/P11 split: fp/stdBmiPath/scan/report and their siblings are computed inside P9 (target_side.cpp) and P11 (scan.cpp) themselves, not inside P2's resolve_target_toolchain closure as an earlier commit's comment claimed. No code change; golden fixture diff against the baseline is empty.
The primary interface still carried the former preamble's definitions, about 1,700 lines of helpers. They move into four implementation units by concern -- config.cpp (conditional merges, build flags and defines, workspace inheritance, feature requests, std-module detection), options.cpp (the MSVC guidance, the cache mode, the profile), toolchain_env.cpp (target rows, sysroots, the MSVC binding, build-program environments, the toolchain a host tool chose) and fetch.cpp (git remotes, network retries, xlings addresses and their provisioning). Exported functions keep their documented declaration, default arguments included, in prepare.cppm; the other helpers are declared in `:state`, grouped by the file that defines them, and keep their design notes beside their definitions. prepare.cppm goes from 2,414 to 777 lines: exported types, exported inline functions and declarations. An edit to a helper now recompiles its own unit and relinks, instead of changing the interface every importer depends on. Moved verbatim; the normalised resolution.json, build.ninja and emit build-database outputs of the seven golden fixtures are byte-identical to the pre-decomposition binary, and the 129 unit tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feature_request_tokens, parse_feature_request and feature_forward_request_tokens (the --features grammar, #649 E8), define_name, previous_release_words (the previous release's reading of a flag element) and is_local_git_remote (including a Windows drive letter's colon) were reachable only through whole builds while they were internal to one unit. They are exported from prepare.cppm for this file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t note - Every phase file opens with what it holds (P0 to P13) and the helper files with their concern, as the rest of the tree does. - `:state` declares the phase functions in the order the driver runs them. - prepare.cppm's layout note lists all sixteen files as they are, states that the interface defines nothing beyond its exported inline functions, and cites #721 for the GCC 16.1 constraint. - check_file_lengths.sh no longer points at a document outside this repository or counts the units. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decomposition gave every implementation unit a copy of the former file's 87 imports. Each unit, the primary interface and `:state` now import only the modules whose namespaces they name, plus every module that exports into mcpp::build itself (usable unqualified from here) or only re-exports others; 835 import lines go. A unit's import list is again a statement of what it depends on. Verified: GCC 16.1 and LLVM 22.1.8 builds, 130 unit tests, and the golden fixtures byte-identical to the pre-decomposition binary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entry states the result rather than the plan: a primary interface of exported types, inline functions and declarations; implementation units and the `:state` partition, each importing what it uses and capped at 2,500 lines by CI; byte-identical planning output over the seven golden fixtures; an edit in P13 rebuilding in 12 s instead of 71 s; and #721 for the GCC 16.1 constraint. test_prepare_helpers joins the list of tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hat produced it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
speak-agent
marked this pull request as ready for review
September 27, 2026 09:17
This was referenced Sep 27, 2026
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
mcpp 2026.9.27.1: the defect reports #704 to #716 and the feature requests #707 to #711, the mcpp side of openxlings/xlings#620 and #621, and the decomposition of
src/build/prepare.cppm. Commits:85c9037aac072ctools, #711artifactsdependency edgec136000install_targetsrecord and reciperevision;LOCPATH/GCONV_PATHin bundle-all packsbba733a6b6cfbdaf4c96fcwdis a plain absolute path, not the\\?\form290bb46test_workspace_inheritancecompiles under clang and the MSVC STL16c9b6716c9b67prepare.cppmdecomposition and its review, see belowDefects
--targetapplies[target.<host-triple>], as--target <host>does (SPEC-004 §4.6).mcpp::xpkg_diranswers the payload xlings installed.install_targetsrecord first, then selects with the xlings version grammar, solibglvnd@1.7answers1.7.0.1.--toolchain, then the tool package's own declaration (a member first inherits the workspace root's[toolchain],[target.*]and[indices]), then the requesting build's host toolchain. The tool tree digest skips nested packages and hashes paths as UTF-8..workspace = truebecomes a version dependency with an empty version #714:[xlings.workspace]entries and conditional rows; its own declaration of a package wins.workspace = trueis refused at every position, naming the table and the entry.[build] sources = []infers no library target.Features
emit build-databasebuilds no host tool. A tool missing from the store is deferred with the noteMCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED(SPEC-005 v1.4 R2.5).mcpp::action::env(name, value)andcwd(dir), protocol 13. An action with neither keeps its protocol 12 command line byte for byte.[features.<f>] tools = ["<bin>"].x = { ..., artifacts = ["<bin>"] }builds the dependency's program for the consumer's target and puts it in the consumer'sbin/. It links none of the dependency's code;${mcpp.artifact:<x>/<bin>}names it in actions;mcpp packstages it.Payload revision and self-contained locale (openxlings/xlings#620, #621)
revision=<n>). With revision 0 the contract text is unchanged.lib/localeandlib/gconvand exportLOCPATHandGCONV_PATH, keeping values the user already set.Specs and docs
Tests
test_xpkg_version, including every vector of xlings'semver-vectors.tsvwith no active version;test_freestanding(payload selection, revision record);test_manifest(feature tools,artifacts,sources = [], per-entry revision);test_tool_store(nested package);test_build_directives(protocol 13, env and cwd);test_workspace_inheritance(A workspace root's xlings declaration is not visible to its members' build programs, so each member repeats it #713, A non-member path package's.workspace = truebecomes a version dependency with an empty version #714, A host tool's sub-build resolves its own toolchain, not the one its key records, so a workspace pinned to one LLVM installs a second #710).788_emit_defers_an_unbuilt_host_tool798_a_rule_applies_through_a_source_it_claims799_an_action_runs_with_its_env_and_cwd800_a_feature_provides_its_host_tools801_a_dependency_program_is_shipped_with_the_consumer802_a_host_build_applies_its_host_row803_a_recorded_payload_that_is_gone_is_refused_offline804_a_path_host_tool_builds_with_its_chosen_toolchain(fails on 2026.9.26.2, passes here)prepare.cppm decomposition
prepare_buildwas one function of about 13,740 lines, 85% of a 16,105-line file, with ~180 top-level locals and dozens of by-reference closures sharing one frame.src/build/prepare.cppmprepare/state.cppm:state:PrepareState, phase and helper declarationsprepare/driver.cppprepare_build: runs the phases in ordermanifest.cpp,toolchain.cpp,xlings.cpp,graph_load.cpp,graph.cpp,toolchain_decision.cpp,features.cpp,target_side.cpp,scan.cpp,plan.cppconfig.cpp,options.cpp,toolchain_env.cpp,fetch.cpp:stateimports the interface.resolution.json,build.ninjaandemit build-databaseoutputs of seven fixtures (single package, workspace, git dependency, host tool, build program, musl cross target, emit) are byte-identical to the pre-decomposition binary.PrepareStateand called by a later phase were checked for captures of a finished phase's locals, statically and under AddressSanitizer withdetect_stack_use_after_return=1(golden fixtures and 12 e2e scripts): none.plan.oand relinks, 12 s; the same edit in the monolith rebuiltprepareand 12 downstream interface units, 71 s. Each unit imports only what it uses (835 import lines removed).emit build-databaseon the workspace fixture: 0.47 s per run for both binaries.test_prepare_helperscovers the pure helpers the phases share, now exported for it..github/tools/check_file_lengths.shkeeps every file at or below 2,500 lines.Verification status
mcpp buildpasses;mcpp test129 passed, 0 failed (non-root); per-member tests pass; e2e 788, 798 to 804 pass.Known red lane
macOS ARM64 — xlings LLVM end-to-end (xcode-27)ande2e suite (macOS ARM64, self-host, xcode-27)fail on main as well since 2026-09-23: ld64.lld cannot read the Xcode 27 SDK's TAPI files (#669, upstream LLVM).Issues addressed: #704, #705, #707, #708, #709, #710, #711, #712, #713, #714, #715, #716. They are commented on and closed after the release is verified in an xlings subos sandbox.