Tags: mcpp-community/mcpp
Tags
One reading of a compile-flag element, the databases list what the co… …mpiler receives (#655); macOS 27 (#657) * One reading of a compile-flag element; the databases list what the compiler receives (#655) An element of `cflags`, `cxxflags` and `asmflags` now has one meaning on every host: the POSIX shell's word syntax without expansions, where a backslash escapes only a blank, a quote or a backslash (mcpp.manifest.flag_words, SPEC-004 §8). The ninja writer quotes each word for its host, and compile_commands.json and `mcpp emit build-database` list the words, so the databases equal the build by construction instead of re-reading command lines. Before, an element meant whatever the host's command-line reader made of it (measured on Linux and Windows runners, #656: five of fourteen spellings gave different macros), and the databases listed the text with a third reading, which is how libarchive's `-DPLATFORM_CONFIG_H=\"...\"` reached consumers with its backslashes. - A `defines` entry is one value: `N="x"` reaches the compiler as `-DN="x"` (it arrived as `-DN=x`). Features, globs, targets and `mcpp:cfg=` alike. - Link units keep their rendered text; GAS units' database entries list their edge's list; the `-std` guard, dialect promotion and include normalisation read words. - Windows `shell_quote_arg` doubles backslashes before a quote and the closing quote. - The first plan after an upgrade names an element whose words changed (`build/flag-words`); a repeated plan says nothing. No flag-list element of the published index changes. Tests: FlagWords.* (syntax table and round trip), CompileCommandsArgs.* (edge equals database, host quoting round trip, /bin/sh as the reference), e2e 736 (same assertions on every host; fails on 2026.9.16.2). Records: .agents/docs/2026-09-17-655-*. Version 2026.9.17.1. * 655: the ecosystem verification script * CI on macOS 27: ci-macos, ci-macos-e2e and both fresh-install channels GitHub serves macOS 27 under the preview label xcode-27 (macOS 27.0 with Xcode 27, actions/runner-images#14404); there is no macos-27 label. Measured on #658: the label delivers macOS 27.0 (26A5406e), arm64, ImageOS macos27. - ci-macos and ci-macos-e2e run on macos-15 and xcode-27. - ci-fresh-install runs the xlings and Homebrew channels on macos-14 (the floor) and xcode-27 (the newest release). - Every macOS 27 leg asserts sw_vers major 27, because the label names an Xcode and its base OS has changed once. - setup-macos-llvm takes the image as an input and puts it in the xlings cache key, which named macos15 for every image. * macOS 27: the std module builds; the -D compatibility rule; host-shaped test fixes macOS 27 (measured on the xcode-27 image, #659): the 27.0 SDK's math.h leaves INFINITY and NAN to <float.h> once modules are on, and clang 22's header does not supply them under -std=c++23, so libc++'s std module stopped building. apple_float_macro_words (hostflags.cppm) states both with the SDK's own GNU-mode spellings for clang on an Apple target; its readers quote the words their own way (ninja text, the std module command, graph std flags, build program argv). gnu++23, -U__STRICT_ANSI__ and -D__need_infinity_nan were rejected for their effect beyond the defect. The raw clang steps of ci-macos state the same words. Compatibility: an element beginning with -D or /D that contains a space is one word, verbatim, as every release since #234 passed it (SPEC-004 §8 rule 8). Windows: flag_element no longer quotes a backslash the syntax does not read as an escape, normalize_include_flags keeps the spelling of an unchanged element, and the split_flags quoting tests follow the host's reader. * CI macOS 27: wrap a bootstrap mcpp that predates the float macro fix The raw clang steps now build import std on macOS 27 (run 35142520288). The self-host step failed because the bootstrap, 2026.9.16.1, predates the fix. setup-macos-llvm wraps a bootstrap older than 2026.9.17.1 on macOS 27 or later and passes the same two words through CCC_OVERRIDE_OPTIONS; the freshly built binary runs unwrapped, and the wrapper retires with the bootstrap pin. * Review: the diagnostic block sits above the defines comment; correct the 252 claim; index reading with the -D exception - report_flag_words_changes was inserted between fold_build_defines_into_flags and its comment; it now precedes the comment. - The record claimed e2e 252 keeps its real old-client leg on macOS through the wrapper. It does not, on any macOS leg including main: MCPP_BOOT is the xlings shim. Corrected and listed as a residual, with dialect promotion reading words. - The index reading script applies the -D exception: 285 literals, 0 differ. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
chore(xlings): pin 2026.9.16.1; version 2026.9.16.2 (#654) kXlingsVersion and every pin check_version_pins.sh names move to xlings 2026.9.16.1 (openxlings/xlings#601). The region object mcpp writes for an index artifact is now a preference chain (CN, then GLOBAL, then git) rather than a single base, and one index refresh is bounded. The constant's comment records it as the fourth floor. No mcpp code changes: list_repos gains an additive artifact_bases field that mcpp does not read. mcpp.toml and MCPP_VERSION move to 2026.9.16.2; the bootstrap pin stays at 2026.9.16.1. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Two spellings of one version constraint are one source (#647 E4.2 fol… …low-up) (#651) Self-review of #650 before releasing 2026.9.16.1 found that the gate E4.2 added refuses a manifest the previous release builds. `[feature-deps.<f>]` must restate its dependency's source, and a restatement naming another source is refused rather than silently dropped. The comparison was made on the bytes of the declaration. A path is normalised first, so a path dependency is judged on what it means; a version constraint was not, so two spellings of one constraint were read as two sources. Measured, with the fixture in the leg below: [dependencies] fmtlib.fmt = ">=11.0.0" [feature-deps.extra] fmtlib.fmt = { version = ">= 11.0.0", reexport = true } 2026.9.15.2 (released): builds, no error and no warning 2026.9.16.1 (f4529b2): error: ... restates the dependency 'fmtlib.fmt' with version = ">= 11.0.0", while the declaration in effect on this row names version = ">=11.0.0". One space turns a build that worked yesterday into a refusal. The judgement is now made on what the declarations mean: the constraint is compared with its whitespace removed, while the message still shows each declaration as it was written, so a genuine disagreement reads as the manifest spells it. A gate refuses a restatement that names another source, and nothing else. The same review asked why the first custom-index sync does not go through `decide_for_miss`/`apply` when the record says the refresh sites do. It should not: that pair's debounce and one-sync-per-process guard are about the index that resolves a dependency, while this sync creates a local copy of a different set of repositories that nothing else will create, so taking the guard would let an earlier refresh of the builtin index suppress a clone the build cannot proceed without. Only the opt-outs are shared. The comment said "the policy's answer is taken" and now states which half is taken, and why the other is not. Criterion: e2e 711 gains a leg. A restatement differing from the declaration only in whitespace is not refused, and one naming `>=9.9.8` against `>=9.9.9` still is. On f4529b2 the first half fails with the refusal quoted above. Readings: unit 121 passed, 0 failed; e2e 187, 710 to 714 pass. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Link forms, standard levels and path lengths: #641 and #642 (2026.9.1… …5.2) (#644) * A C++-layer provider compiles its implementation units at the standard it states (#641 item 2) A package that provides the C++ layer and states [package] standard now compiles each C++ unit that neither provides nor imports a module at exactly that level; module units, the std module included, stay at the graph's level. The level is appended to the unit's own flag vector in make_plan, so the compile edge, the scan edge, compile_commands.json and the build database read one value. The provider predicate is factored into the manifest module and used by the std module adoption. The dependency cache key and the fingerprint metadata carry the stated level only when it exists; the higher-standard warning no longer reports a provider whose level is applied. Tests: unit CxxLayerStandard.* and CacheKey.ACxxLayerProvidersStatedStandardChangesTheKey; e2e 696 (requires llvm). Docs: 07-workspace 4.2 and 22-target-side with mirrors. * docs(design): #641 and #642 triage record, probes, and implementation plan * mcpp pack --features reaches every build pass of the pack, and run --format passes its features to the pack (#641 item 4) * A required compiler family takes the pins of its own payload (#641 item 1); a dependency's C++ shared library over a graph C++ runtime is refused or carries a stated private copy (#641 item 5) * The refusal offers the static edge only where a request or a default made the library shared; e2e 690 reads four legs * docs: M1-M3 in 20-toolchains, 10-pack-and-release and 50-machine-output with mirrors; CI runs the graph C++ runtime e2e scripts on the hermetic llvm job * Bounded tool-build paths: owner-relative object addresses, a short sub-build scratch, extended-length opens on Windows (#641 item 3; M7, M8, M9) M7. A source outside the package that declares it (a build program's mcpp::source under a dependency's root) is addressed as obj/<declaring>/__pkg/<owning package>/<path inside it>, or obj/<declaring>/__ext/<8-hex hash of its directory>/ when no package contains it. The mirror of the declaring-package-relative path, with one __up per level, is gone for these sources; sources inside their declaring package, root-owned flat addresses, the #233 census and backstop, and the cache address (packageRel) are unchanged. In the root census branch the escaping form applies only where the mirrored prefix was used (a basename collision), so the flat obj/<name> of a generated OUT_DIR source (#239) is unchanged. M8. The host-tool sub-build scratch moves from <entry>/build-<hash> to <cache>/tool/.build/<hash of entry and consuming work root> (tool_store::scratch_dir), keeping per-consumer isolation, re-run reuse and removal after publish; the entry layout is unchanged. M9. mcpp::platform::fs::extended_length (identity outside Windows; absolute, normal, backslash, \\?\ or \\?\UNC\ form on Windows, idempotent) with the pure spelling rule windows_extended_length_spelling. Call sites, enumerated from the $mcpp rules of ninja_backend.cppm and the __action-stamp wrapper: src/cli/cmd_build.cppm cmd_dyndep: --output; the --expect-* ddi read; emit_dyndep_single input; emit_dyndep_from_files inputs src/cli/cmd_build.cppm cmd_stage: source and --output (stage_file) src/cli/cmd_build.cppm cmd_bmi_equal: both paths (bmi_equivalent) src/cli/cmd_build.cppm cmd_coff_def: each object read; the .def write src/cli/cmd_build.cppm cmd_bmi_compile: --bmi --slot --sem --command-file --dep-from --dep-to (not --self, an executable) src/cli/cmd_build.cppm cmd_bmi_supervise: --slot --token --command-file src/cli/cmd_build.cppm cmd_bmi_await: --slot --object src/cli.cppm __action-stamp: each stamp Downstream opens receive the converted paths. Two follow-ons: the detached compile's log opens with CreateFileW (CreateFileA does not honour the prefix past MAX_PATH), and CompileRequest::bmiTarget keeps ninja's spelling of the BMI for the copied depfile's target, which must not change. Tests: unit ObjectAddress.ASourceUnderAnotherPackageIsAddressedByThatPackage, ObjectAddress.AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory (both fail on the previous object_for), ToolStoreScratch.IsShortPerConsumerAndStable, PlatformFs.ExtendedLength* (four); e2e 698 (leg A fails on 2026.9.15.1 with obj/installer/__up/__up/dep/platform/impl/helper.o). Docs: 30-build-mcpp and its Chinese mirror. * feat: a library states its default link form, and the root build program reads each dependency's form (#642 E1, E2) - [targets.<n>] linkage and its row form state a library's default form; kind = "shared" stays the constraint. Precedence: the root edge, a written dependency_linkage, the package default, static. An explicit override is honoured with an information line and no degraded record. - The link-form resolution is computed once before the root build program (hasSources from the scanner's own package_source_files) and applied after the scan as before. - The root build program receives MCPP_DEP_<NAME>_LINKAGE under the names of MCPP_DEP_<NAME>_DIR; mcpp::dep_linkage() reads it. Dependency programs do not. - Form B descriptors read linkage the same way. - Unit tests (linkage_form, manifest, xpkg), e2e 692 and 693, docs 04, 22, 30 with their Chinese mirrors, SPEC-004 1.4. * The M3 refusal reads the resolution's declaredShared fact for the package that constrains its form * Version 2026.9.15.2; CHANGELOG; the plan's ledger and refinements * dep_dir and dep_linkage answer under the qualified namespace.name a consumer writes; e2e 693 leg F * e2e 696 and 698 write fixture paths through host_path * docs: a shared library over a graph C++ runtime on Mach-O exports what its sources mark with default visibility * docs(design): refinements measured on macos-15 and for qualified dependency names * e2e 698: the consumer's tool command survives cmd.exe's quote stripping on Windows * e2e 698 sources the host path helper before it changes directory --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
mcpp emit build-database, program probes as argument vectors, and xli… …ngs 2026.9.14.1 (2026.9.15.1, #636) (#639) * ci: workspace mcpp bootstrap pin -> 2026.9.14.3 (released + indexed); #634 ledger closed Version group 2 moves to 2026.9.14.3, released from 9b6a118 and named `latest` by the published xim index artifact `xim-index-ed04041`. The #634 implementation plan and the triage record are marked landed. The ledger closes every row with a reading (E1-E24 in mcpp#637, E25 in mcpp#638, P1-P9 in mcpp-plugins#24, I1 in mcpplibs/mcpp-index#426, R1-R4, V1-V4), and §9 records what landed, the sandbox readings on 2026.9.14.2, 2026.9.14.3 and the 2026.9.14.1 control, the ecosystem review, and two observations this work did not change. * docs(design): the build database of #636 and the two defects on the way to the latest xlings The record of the analysis, the plan and its self-review: xlings stages a hookless package from the shared download directory; a POSIX redirect in the vendored-xlings probe reaches cmd.exe on Windows; and mcpp emits an S1 build database from the plan without writing into the project. * fix(platform,xlings): a program probe is an argument vector, not a /bin/sh string The vendored-xlings version probe ran `<xlings> --version 2>/dev/null` as a command string. On Windows every command string reaches cmd.exe, which cannot open /dev/null: it printed "The system cannot find the path specified." in every command after the first, did not run xlings, and returned an empty version, so a Windows home never replaced a vendored xlings older than the pin. mcpp.platform.process gains capture_stdout: the program runs directly, its standard output is captured, its standard error is discarded and its standard input is empty; on Windows the redirect names cmd.exe's own null device. The xlings probe and the four other program probes that carried POSIX grammar on a path Windows reaches use it: the clean link specs, the libstdc++ fallback probe, the freestanding size report and the publish digest. Tests: capture_stdout unit tests (stdout only, exit code, empty stdin, environment, empty output for a missing program, the Windows command line); a version-probe unit test that runs a .bat through the real launcher on Windows; e2e 687 (no path error on the second command; an older vendored binary is replaced). * feat(emit): mcpp emit build-database, the plan as a build database written into nothing (#636) `mcpp emit build-database` plans as `mcpp build --configure-only` plans, with the same selectors, and prints the plan as an S1 build database (C++ Build Database: IDE Profile 0.2.0, a profile of WG21 P2977R2) without writing into the project. `--spec compile-commands` prints compile_commands.json entries instead; `--format json` wraps the document in the mcpp.build-database envelope; `-o <file>` writes it atomically. docs/specs/build-database.md (SPEC-005) states the rules. The document is S1 level 2: one set per package plus <package>:test and mcpp:std; visible-sets lists every other set, because the engine resolves imports over one module graph; each unit's arguments come from the record the compile database renders; ide.role is the declaration form the scanner read. Five engine changes make the no-write planning exact: - BuildOverrides::work_dir had two writes that ignored it (the multi-version mangling stage, and the root package's generated_files, which are now compared rather than written under plan_only); - the std module is described without being compiled (describe_std_module, the derivation ensure_built builds from); - the std units' commands are recovered from the commands mcpp runs, so the std cache identity does not change; - compile_commands and the build database render one UnitInvocation record; - the scanner records the module declaration form (ModuleDeclaration). mcpp.lock is read from the project and never written back; a difference is the warning MCPP_LOCK_WOULD_CHANGE. Planning narration goes to stderr through a descriptor-level redirect. A failure envelope omits data. Tests: BuildDatabase.* unit tests drive the real gcc and clang std command builders; Scanner.DeclarationFormIsRecordedAsRead; CompileCommandsEmit.UnitInvocationsAreTheCompileDatabaseArguments; WireGolden.NullDataIsOmitted; e2e 688 validates the output against the vendored S1 schema and uses --configure-only as the control leg for the no-write criterion. * fix(emit): a store package's build program inputs are not watched A store package's build program may declare inputs, and they were listed in `watch`; its files are fixed by the version its manifest and lock name, so only an editable package's declared inputs are inputs that can change. Found by running the command over lsp-mcpp, whose openkal-musl dependency declares its own mcpp.toml. * test(emit): the MSVC std units are recovered from their builder Drives msvc::std_module_build_commands and std_compat_build_commands, the command shape a Windows host with msvc@system runs (cd /d, a quoted cl.exe, /ifcOutput, /Fo:, 2>&1), through recover_invocation. * fix(emit): the selector digest separates values with newlines A \x1f separator followed by `f`, `c` or `a` (`\x1ffeatures`, `\x1fcap`, `\x1faccel`) is one longer hex escape; clang refuses it as out of range and every clang host failed to build the branch. GCC accepted it, which is why the Linux build did not see it. The sandbox verification script for the release is added alongside. * fix(scan): an entry source outside the globs is read by the scanner A target's entry source that no `sources` glob matched (a discovered test, a `main` outside the globs) had its imports read from line-leading `import` alone, in make_plan and again in the standard library check before planning. An import inside a comment or a raw string was therefore planned as one. Validating the lsp-mcpp repository's own build database against S1 found it: its scanner test was planned as importing three modules no source provides. scan_entry_file reads such a file with scan_file. The scanner's refusals (an import inside `#if`, a header unit, an extension without a role) were never applied on this path and are not applied now: a file it refuses yields the line-leading imports of its code, comments and raw strings removed, with the declaration form Unknown. Both readers now call it, so they cannot disagree about `import std`. Refs #636 * feat(emit): baseline and local arguments, private, and config-files The S1 SHOULD fields the lsp-mcpp validator reads, with values the plan determines: - `baseline-arguments` and `local-arguments`. A unit's arguments are its driver, the set's baseline, its local arguments and its own trailing `-c <source> -o <object>`, whose operands name the unit's source and object from its work directory. The baseline is the longest prefix every unit of the set shares; a prefix keeps argument order, which decides include search and macro definitions. - `private` is false: every module is visible to every set. - `config-files` lists what the driver reads without being named: the clang `<driver>.cfg` of resolve_clang_driver unless the units pass `--no-default-config`, and GCC's `lib/gcc/<triple>/<version>/specs` (or the major version) beside the driver. e2e 688 gains J (the decomposition, private, config-files) and K (the discovered test's imports inside a comment and a raw string are not `requires`); its F mapping compares paths with one separator, and the fixture's main.cpp is well-formed. SPEC-005 R3.2a, R3.6, R3.8 and R3.9a state the rules. Refs #636 * test(e2e): 687 plans in a home with its own configuration The macOS runner's inherited configuration names its ~/.xlings shim as the xlings binary, so nothing was vendored and the precondition failed before either criterion ran. The home now uses its own configuration, whose binary is the vendored one. Refs #636 * chore: bump version to 2026.9.15.1 mcpp.toml and MCPP_VERSION move together; the bootstrap pin stays at the released 2026.9.14.3. The changelog names the release on the three entries that land in it, and records the S1 fields and the entry-source scan. Refs #636 * chore(xlings): pin 2026.9.14.1 kXlingsVersion and every pin check_version_pins.sh names move to the xlings release in which a package without an install hook receives its own archive (openxlings/xlings#596). The constant's comment records it as the third reason for the floor. Refs #636 * docs(design): the execution record of #636 What landed in each repository, the five findings made while implementing, and the measurements taken so far, including lsp-mcpp's validator and its conformance runner against this mcpp. Refs #636 * docs(verify): a doctor control, the lock file named, and a dependency project Section D reads a clean store first, then requires the finding's label and the seeded lock file's name, and after --fix requires the reinstalled payload: no seeded file and */mcpp.toml present. Section C2 emits the build database of section B's project: the dependency's set, no watched store path, the S1 SHOULD fields, and an unchanged tree with target/ present. Sections that need python3 say so when it is missing. Refs #636 * docs(specs): SPEC-005's index row carries its last modification date Refs #636 * fix(emit): a plan that builds nothing writes no link specs prepare_build ran `g++ -dumpspecs` to write mcpp-clean-link.specs under plan_only. The file is read only by the link line, so the planning pass neither needs it nor should run the driver for it, and the compile arguments are the same without it (e2e 688 F still compares them with configure-only's). Found by an independent review of #639. e2e 688 criterion L measures what SPEC-005 R2.2 states: the fresh work directory holds the resolution record and no object, BMI or link specs, and a home whose build cache is empty stays without objects or BMIs while its document lists the std unit. Its control leg runs configure-only on a copy in the same home and sees the std module compiled. R2.2 now says what is true: nothing is compiled, and the toolchain is still queried as for `mcpp build`. Refs #636 --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
What a framework and its ecosystem library still hit in the engine: the #630 batch (#631) * The C++ layer answers for its own headers, and the iOS rows take libc++ and the builtins from the graph (#630, item 4) * manifest: min_api_level is a known [target.<triple>] scalar key (#610, #630 item 5) The unknown-key sweep's kKnownTargetScalars list drifted from the parser: it read min_api_level (toml.cppm) but never listed it, so a correctly spelled key was reported as unsupported and --strict turned that report into a build failure. Adds the key to the list and to the message, extends 641 with the manifest-only regression (a manifest that declares min_api_level on one row must not trip --strict when building a different row), and adds a unit test whose denominator is read from toml.cppm's own body.find(...) parse sites rather than a second hand-written list, in both directions, so a sixth key added without a matching list entry fails the test instead of shipping. * pm: an OS-only selector is a platform for `mcpp emit xpkg` (#630 item 7) A [target.<selector>] tool declaration whose predicate names only an operating system (cfg(linux), cfg(os = "linux"), the windows/macos/unix equivalents, and cfg(unix) naming both linux and macosx) answers the exact question a descriptor's per-platform block already asks, so emit_xpkg now folds it into that block instead of only raising the publish/target-axis-tools advisory. Any predicate mentioning an architecture, an environment, a layer, or wrapped in any combinator (any/all/not, even one built entirely from OS terms) keeps the warning, whose text now says so. cfgpred::os_only_platforms is built on the existing cfg() Parser (two more optional taps: seenKV and combinator) rather than a second reading of the predicate text, per the "one grammar, two readers" rule this repository already follows elsewhere. Unit tests cover both directions for the classifier and for emit_xpkg's rendered output. * e2e 663: the baseline is read from the link line's archives, not from a report line that is absent by design * One identity, two declarations, said aloud (T3, #630) A second `git`/`path` declaration of an already-resolved dependency identity carried no comparable reference: `ResolvedRecord` held no path and no `gitRev`, so a `git`/`git` or `path`/`path` disagreement fell through silently and the winner was whichever request the FIFO worklist happened to dequeue first. A `path`/`git` KIND clash refused outright even when the root itself was a party. `ResolvedRecord` gains `sourceRef` (the declared git ref, the canonical path, or the SemVer constraint) and `fromRoot`, so the resolve hit can compare a second declaration's REFERENCE rather than only its kind, and can tell whether the root is one of the two requesters. The root's declaration now wins a kind clash or a same-kind reference conflict it is a party to, reported through `mcpp::diag::warning` ("dependency/source-override") naming both requesters and both references; a losing `version` requirement is checked against the winning checkout's own `[package] version` and refused if violated, in the same Holds/Violated shape `addrset::unify` already uses for a tool pin. A kind clash between two non-root requesters keeps the existing refusal, with one added hint sentence. The "root arrives after a dependency" ordering is provably unreachable under FIFO worklist seeding (root deps are seeded before the loop starts); rather than depend on that silently, it is refused by name so the invariant cannot degrade into today's accident of queue order. docs/05 gains the decision table under a heading that finally answers the "what happens when two of them disagree" its own preamble promises, mirrored in docs/zh/05. tests/e2e/661 builds a local git repository (three commits, then an uncommitted edit) with no network, and covers all six rows of the decision table plus the negative direction (the same reference declared twice must stay silent, and `--strict` must stay green). * The tool store's key holds the source: a git tool by its commit, a path tool by a stamp of its tree (#630, item 6) * docs: the iOS rows take libc++ and the builtins from the graph; the #630 design record * An Apple cross target without a graph libc++: the SDK's headers when std is not imported, and a reported degradation when it is * pack: stage what is declared before walking what is discovered (#630 §3) `pack::run` used to refuse a Mach-O program, and a non-PE artifact on a Windows host, before any staging ran at all -- so a dispatched format that names one program and never reads the dependency closure (an `.app`/`.msi` bundler) had no tree to work from either, on a host where the built-in closure walk cannot run. The tree is now staged unconditionally -- the program, then the runtime files `mcpp::deploy`/`[runtime] deploy` declared -- and the closure walk is one step within it whose outcome is recorded rather than a precondition for reaching that step. `pack::run` returns a `ClosureResult` (`walked` plus a `reason` when not) instead of `void`; `closure_unavailable_outcome(Format)` is the pure decision of what an unavailable closure means per format: `--format tar`/`--format dir` still fail the command (the archive IS the closure, unchanged), a dispatched format receives the tree regardless. The stage manifest gains a `closure = walked | not-walked` header line, with `reason = <why>` when not walked, so a provider can read it instead of inferring a gap from an empty `lib/`. The ELF product of `--format dir` is unaffected: the reorder only moves where the Mach-O/Windows-host refusals are decided, not what the ELF path does once it is reached. Tests: unit coverage for `closure_unavailable_outcome` and for the stage manifest writer/reader with both `ClosureStatus` values (the "not-walked" case cannot be produced on Linux through `pack::run` itself -- it needs a Mach-O program or a Windows host packing a non-PE artifact, covered end-to-end by 266_pack_refuses_a_macho_program.sh on macOS CI); a new e2e, 662_pack_stages_declared_files_before_the_closure.sh, asserting the staged tree's exact structure for `--format dir` and for a dispatched format's action reading `${mcpp.stage_dir}`. Docs: docs/30-build-mcpp.md and the zh mirror gain the `closure` manifest field in the `${mcpp.stage_dir}` section. * pack.binfmt: a Mach-O reader for needed_names (#630 §4) `needed_names` returned "not implemented" for Mach-O; `elf_needed` and `pe_needed` already read a dependency list without executing anything. Mach-O gets the same treatment: - `detail::macho_thin_needed` walks a thin object's load commands: `LC_LOAD_DYLIB`, `LC_LOAD_WEAK_DYLIB`, `LC_REEXPORT_DYLIB` and `LC_LOAD_UPWARD_DYLIB` contribute names, `LC_RPATH` contributes search entries, in load-command order. Both widths (`mach_header`/ `mach_header_64`) and both endiannesses (the CIGAM magics) are handled. - `detail::macho_needed` / the exported `macho_needed(path, arch)` add the FAT (universal) case: a slice is selected by `cputype` against the caller's `arch` (mcpp's canonical spelling), defaulting to the first slice when none is given -- which is what `needed_names` does, since it has no triple to pass. `FAT_MAGIC` and `FAT_MAGIC_64` are both handled; FAT headers are always big-endian on disk regardless of a slice's own endianness. - `needed_names` completes for Mach-O by delegating to `macho_needed`, and `is_system_lib` gains a Mach-O row: `/usr/lib/` and `/System/Library/` are the operating system's, case-sensitively (`@rpath/...` is a search, not a root, so it is never system). - `resolve_macho_names` is a pure resolver: `@executable_path`, `@loader_path` and `@rpath/<x>` (tried against every rpath entry, in order, with the same two substitutions) resolve to the first path that exists; a name that resolves nowhere is reported as `unresolved`, not dropped. NOT wired into `pack::run`'s Mach-O closure step. Bundling a resolved dylib beside the program and rewriting its `LC_RPATH` needs a load-command editor -- a load command has no free space to grow a longer path into -- and the record (§4.2) designs that editor after resolution is measured on a real macOS build, not before. `pack::run`'s Mach-O branch still reports "not-walked" (see the sibling #630 §3 commit); this reader and resolver are exposed for that measurement and for whoever wires them in next. Tests: unit fixtures built in-process (a tiny Mach-O/fat-Mach-O byte writer, no external tools, same shape the ELF/PE fixtures already use) -- a thin arm64 object with two `LC_LOAD_DYLIB`, one `LC_LOAD_WEAK_DYLIB` and two `LC_RPATH`; a fat x86_64+arm64 object whose slices carry different names, asserting the wrong slice is never read; a big-endian-magic thin object; the wrong-arch request on a thin file (ignored, as documented); `is_system_lib`'s Mach-O row; and the resolver's rpath-order and unresolved-reporting behavior. Docs: docs/10-pack-and-release.md and the zh mirror note that the closure is read now, and narrow the "planned" gap to bundling + the `LC_RPATH` rewrite. * docs: the no-package iOS path is reported, not refused * e2e 666: on macOS a Mach-O program is staged without its closure and handed to a dispatched format * record: what landed for the Mach-O reader * The package std module on an Apple cross target keeps the SDK on its command * record: progress of the batch * T9 (#630 A9): the universal APK is the library route applied to an app The route mcpp pack takes is a function of the artifact's FORM, not of the target's kind. A `kind = "app"` target whose resolved link form is a shared object on every requested row (every Android row) now accepts more than one `--target`, the way a library target already does: the other legs are built first (`build_extra_android_legs`, the same leg-loop shape `build_and_pack_library` uses) and staged as `lib/<abi>/lib<name>.so` beside the primary leg's own `lib/<abi>/`, into one tree, behind one dispatch pass. A `bin` target, or any row whose form is an executable, keeps today's refusal for a second `--target`. `toolchain::triple::android_abi` derives the ABI name from the triple's architecture (aarch64 -> arm64-v8a, x86_64 -> x86_64, per the 2026-09-12 design record). `PackRoute::isApplication` and the pure predicate `accepts_several_targets` let `cmd_pack` decide the route before building anything. `Plan::extraSharedLegs`, empty for every pre-existing caller, keeps a single-`--target` pack's staged tree byte-identical. Tests: unit coverage for `android_abi` and `accepts_several_targets` (8 cases), and e2e 664 exercising the staged tree for one and two `--target` flags and both refusal directions on the sandbox's android-ndk capability. docs/10 and its zh mirror gain the shape and layout. * The tool store's rule in docs/30, the examples/12 README and the CI probe: the key holds the source * 2026.9.13.2: the #630 batch * e2e 304, 661, 666 on macOS; the builtins package is 22.1.8.4 * ci-macos-ios: the report names 22.1.8.4 * record: what landed for #630 and where each claim was measured * ci-macos-ios: a degradation is read by its sentence, not its domain * the builtins package is 22.1.8.5 --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
The action lists have no size limit, and ${mcpp.self} names the engin…
…e (2026.9.13.1) (#629)
* build.mcpp: an action's lists have no declared size limit
The bundled mcpp module held an action's six list fields in fixed arrays
(8192 bytes of serialised JSON for inputs and outputs, 16384 for command)
and refused a declaration that did not fit. The bound was in bytes, so a
consumer's checkout depth decided whether a list of 44 resource files was
accepted (HuxerUI#130 measured the margin at 45 bytes), and outputs is the
one list an author cannot shorten: an output the program does not name
cannot be built, and there is no depfile for outputs.
The arrays were fixed because one constraint was read as two. The module
must not import std, and its exported interface must name no std type;
neither forbids the heap, and <cstdlib> was already in the global module
fragment. The six arrays become one growable std-free buffer over realloc.
The exported surface and the protocol version are unchanged, and the
payload of every action that fit before is byte-identical, so the cache key
is untouched. The overflow marker keeps its wire form and now means
allocation failure; the engine's message says so.
e2e 659 declares 200 inputs and 200 outputs and a 19200-byte command and
reads the action's edge out of build.ninja; under 2026.9.12.4 the same
fixture is refused.
* build.mcpp: ${mcpp.self} names the engine, and mcpp stage is the portable copy
An action's command is an argv with no shell, so a build program had no
portable way to copy a file: cp is absent on Windows, cmd /c copy is a
shell and an 8191-character limit, and a copier carried by a package is a
host-tool sub-build for one copy. The engine is the one program present
wherever a build runs, and mcpp stage --verify content --output <dst> <src>
is the copy every stage_file edge already performs. ${mcpp.self} joins the
argv substitution family and is replaced by the engine's absolute path, as
the check wrapper already bakes it in.
mcpp stage's argument shape is a contract from here on; its help text now
states the real default (content). e2e 660 copies the linked program
through the token on every shard; under 2026.9.12.4 the token stays
literal and the edge fails.
* docs: the action lists' limit, the ${mcpp.self} row, and the design record for the four upstream asks
* chore: bump version to 2026.9.13.1
* ninja: the command-length guard measures a literal command, not its edge line
check_inline_command_lengths read each build line as a proxy for the
command. That is right for a rule that expands $in and $out and wrong for
an action rule, whose command is a literal argv and whose inputs and
outputs exist only so that ninja can order and re-run it. e2e 659's first
run on the Windows shard refused an action with 200 outputs and 200 inputs
with the whole edge line counted as argv.
The guard now measures a literal command's own text. The refusal names the
edge by its first output and the count of the rest, instead of printing
every output. e2e 659 declares 600 inputs and 600 outputs, so the edge
line crosses the POSIX 128 KiB limit as well as Windows's 32767 and the
guard is measured on every shard; its negative leg declares a 140800-byte
literal command and expects the refusal to name the edge.
* ci(openkal-cross): say what the Windows host provides for the mingw target
The Windows job of this workflow was green with one sandbox lineage and
red with the next on the same sources and the same runner image (PR #629:
lld could not find -lntdll and its neighbours). On a Windows host
openkal-windows's build program generates no import libraries, so where
lld finds them is a property of the host, the sandbox or the payload, and
nothing in the job said which. This step prints it.
---------
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
PreviousNext