Skip to content

2026.9.26.2: one compile database per configuration, emit plans every member, prepare actions and runtime search directories, DLLs beside a Windows program, link flags as words - #702

Merged
Sunrisepeak merged 30 commits into
mainfrom
feat/runtime-library-dir
Sep 26, 2026

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements the design record .agents/docs/2026-09-26-compile-database-and-issue-699-design.md (decisions D1 to D12) as mcpp 2026.9.26.2. The pull request carries the whole round: the compile database, emit build-database for workspaces, the build-program surfaces #701 asked for in the form SPEC-007 states, the placement of a Windows program's DLLs, and #703.

The compile database (design §3)

  • One database per configuration, target/<triple>/<fingerprint>/compile_commands.json, merged only within that configuration; the root compile_commands.json is a copy, replaced whole and never merged, restored by the next build (fast path included) when deleted, with one warning when it held entries mcpp did not write.
  • directory and S1 work-directory name the output directory, where the compiler runs (JSON Compilation Database, S1-8-2); GCC entries replay from directory without writing gcm.cache/ into the source tree.
  • A module interface's entry carries its language flag before -c (GCC and Clang dialects; MSVC waits for a Windows measurement).
  • The standard-library units are listed whenever the build imports std (S1-12-1); S1 provides of those units names the std cache BMI, and toolchains carry build-id.
  • mcpp new ignores compile_commands.json.

emit build-database (#699, design §4)

Build programs (#701, design §5.4, SPEC-007)

  • mcpp::runtime_search_dir(dir) (mcpp:runtime-search-dir=, protocol 12) joins LinkIntent::runtimeSearchDirs, the field [runtime] runtime_search_dirs feeds; runtime_library_dir and the legacy library_dirs field are not extended.
  • The prepare role, with a.output_dir(dir): construction whose file names are unknown when the program runs. The edge fails when the directory holds no file after success; compile edges of the declaring package and every link edge wait for it; progress lines say PREPARE; a rerun input inside a prepare directory is warned.
  • Role constants mcpp::roles::{source, check, object, artifact, prepare}; unknown role strings are refused with the list (they were read as source).
  • After a check or prepare succeeds, every stamp is newer than every input.

Windows DLL placement (design §5.4 W, SPEC-007 R4.3)

  • A PE program whose plan has runtime search directories gets an edge after its link, mcpp place-dlls, which places the DLLs it imports from those directories beside it, with mcpp pack's closure walk and system rule. A replaced DLL is placed again in the same build; deployed DLLs are order-only inputs of the link.

Link flags as words (#703)

  • SPEC-004 §8's reading extends to ldflags and the link directives: $ORIGIN reaches the run path instead of /../lib, dependency flags propagate word by word, a search directory with a space is one argument, and the first plan names an element whose words changed.

Specifications and documents

  • SPEC-004 v1.7, SPEC-005 v1.3, SPEC-007 v0.2 (new: docs/specs/build-plugins.md); docs/01, 04, 30, 31, 50 and their zh pairs; CHANGELOG. Build-program cache epoch 3.

Closes #699
Closes #701
Closes #703

Test plan

  • mcpp test: 129 test binaries pass; mcpp test -p buildmcpp, -p manifest pass.
  • New e2e 779 to 797 pass locally on the integrated binary; 780, 781, 784, 785, 787, 788, 789, 795 fail on 2026.9.26.1 as their criteria require (design §12.2).
  • mingw-cross + wine legs (796, 797) run locally; CI runs them in the mingw-cross job with asserted lines.
  • llvm rows (783 to 786) are asserted in the hermetic llvm job.
  • docs structure/style, modules wiring, narrowing, version pins checks pass.
  • CI on every platform (xcode-27 legs are ci-macos xcode-27: ld64.lld cannot parse arm64e.x1 in either available SDK (upstream, tracked) #669 and unrelated).

speak-agent and others added 3 commits September 26, 2026 06:03
…ory, joining `[runtime] library_dirs`

`mcpp::runtime_library_dir(dir)` (`mcpp:runtime-library-dir=`, protocol 12)
is the build-program form of `[runtime] library_dirs`: a launch-time search
directory for a dependency a build.mcpp discovers (a vcpkg prefix's `bin/`,
a Qt SDK's `bin/`) rather than one an author can write into a fixed TOML
array.

It joins the SAME field the manifest key populates
(`RuntimeConfig::libraryDirs`), so every existing consumer of that field
sees a directive-declared entry without any of them changing: `mcpp run`'s
loader path, `mcpp pack`'s closure search, and the ELF/Mach-O
`-Wl,-rpath` rendering. Persisted like `deploy`/`warning`/`pack-format`
(non-empty cache tag, `kCacheEpoch` not bumped, same reasoning), and the
root package's own build.mcpp needed the same manifest-snapshot mirror
`deploy` already has in prepare.cppm, or `resolve_runtime_contract` never
sees a directive-sourced entry.

Adds unit coverage in test_build_directives.cpp and an e2e script
(779) that links an executable against a prebuilt shared library only the
directive's RUNPATH makes loadable, checks the build.mcpp cache-hit replay,
and checks `mcpp pack --format dir`'s closure staging.

Docs: docs/30-build-mcpp.md and docs/zh/30-build-mcpp.md gain the directive,
the typed API entry and a worked section; docs/04-mcpp-toml.md and its zh
counterpart point `[runtime] library_dirs` at the build-program form.
SPEC-004 §8's element reading extends to ldflags and the link directives: each
element is read into words, and each word reaches the linker quoted for the host
and escaped for ninja. A $ORIGIN in [build] ldflags, in mcpp::link_flag or in a
dependency's ldflags reaches the program's run path instead of /../lib; a search
directory whose path holds a space is one argument. Dependency flags propagate
word by word, the link directives spell engine-built paths as one word, the
build-program cache epoch moves to 3, and the first plan names an element whose
words differ from what 2026.9.26.1 passed.
A PE image has no run path, so a DLL in a runtime search directory served
mcpp run and mcpp pack and not a program started by hand. A PE program whose
plan has runtime search directories now gets an edge after its link, mcpp
place-dlls, which reads the program's import closure with mcpp pack's
read_closure and system rule and publishes each DLL it resolves in those
directories beside the program. Its depfile names the DLLs it placed, so a DLL
replaced in its directory is placed again. Deployed DLLs become order-only
inputs of the link, since the linker reads the import library.
… is a warning

#699 item 1 (E1). `emit build-database --workspace` stopped its whole member
loop at the first planning failure, discarding the sets of every member that
had already planned; `mcpp build --workspace` never had this defect
(continue-on-failure). The loop now plans every selected member on its own: a
member whose planning fails contributes no set and one `error` diagnostic
(`MCPP_BUILD_DATABASE_PLAN_FAILED` or `MCPP_OFFLINE_DOWNLOAD_REQUIRED`, the
refusal code read right where that member's planning ends so an earlier
member's recovery cannot relabel a later one's reason) whose `path` names its
`mcpp.toml`, relative to the workspace root. `data` is present when at least
one member planned; a workspace in which every member fails still omits it,
since S2 has no partial outcome. The exit status is 1 whenever any diagnostic
is an error, independent of `--format`.

#699 item 2 (E2). Under `emit` (`plan_only`), a host tool that fails to build
no longer costs the requesting member's plan: it is a warning,
`MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`, naming the tool, its package and the
first line of the failure. The tool build itself and its `check` actions are
unchanged, and the requesting build program still receives the path the tool
would have been published at (the store key already fixes it before the
build runs). `mcpp build` is unaffected: the same failure still ends the
build.

#699 item 2 (E3). Under `emit`, a package whose build program fails (does not
compile, exits non-zero, times out, or is refused) is described without that
program's directives instead of costing its member: the manifest's own
configuration, the toolchain, the module graph and the standard-library units
are described as usual, with one error, `MCPP_BUILD_DATABASE_PROGRAM_FAILED`,
whose `path` names its `build.mcpp`. Nothing from a failed run was ever
applied (`run_build_program` returns before parsing directives on every
failure path); a later failure that follows from the gap fails the member
under E1's rule. Both `run_build_program` sites (a dependency's and the
root's) carry the same branch, gated on `overrides.plan_only`.

`PlanNote` gains a severity (default warning, so every existing note is
unchanged) and a `path` (absolute when recorded, rewritten to the
workspace-relative form `render()` uses for everything else). `emit`'s
per-member diagnostics and `build_database::render`'s note handling and
`watch` assembly (a failed member's `mcpp.toml` and `build.mcpp`, when
present, join `watch` exactly as a planned member's do) change together.

Design: .agents/docs/2026-09-26-compile-database-and-issue-699-design.md §4.
docs/50 §8 and its zh counterpart now describe the shape #699's fix gives
`mcpp.build-database`: `emit` plans every selected member on its own, a
failed member's diagnostic carries `path`, `data` is present whenever one
member planned and absent only when none did, and the exit status is 1
whenever any diagnostic is an error even though `data` is present. The two
new codes join the table: `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT` (warning)
and `MCPP_BUILD_DATABASE_PROGRAM_FAILED` (error).

SPEC-005 R2.5 states the host-tool warning and that its `check` actions and
`mcpp build`'s own failure are unchanged; R5.2 states member containment, the
per-member `path`, the program-failure description and the exit-code rule.
Header, version line and R3.7/R3.8/R4.1 are untouched (T1's).
787: a workspace member whose planning fails (an unresolvable dependency, not
a build program) does not cost its sibling's set; the failed member's `path`
and its containment in `watch`; a workspace in which every member fails
omits `data`; the exit code is 1 with and without `--format`.

788: a host tool whose build carries a failing blocking `check` (the e2e 315
fixture) is a warning under `emit`, not a lost plan; `mcpp build` still fails
on the same check.

789: a build program that exits 1, and one that does not compile, are each
described without their directives, `path` naming `build.mcpp`, and nothing
a failed run printed before exiting reaches the described unit (the exit-code
check precedes directive parsing). A third case, a library target with no
sources of its own relying entirely on directives, shows the member failing
under E1 when the missing directives leave nothing to link.

All three fail on the released 2026.9.26.1 (measured) and pass on this
branch's build.
… standard-library units

Implements T1 (W1, W2, W3) of the 2026-09-26 compile-database design record
(.agents/docs/2026-09-26-compile-database-and-issue-699-design.md), against
issues #397, #677, #699 and the xmake-comparison report it triages.

W1 (design §3.2). compile_commands.json is now two files with one rule each.
The CONFIGURATION's database, target/<triple>/<fingerprint>/compile_commands.json,
holds the fresh plan's entries merged with the entries it already had whose
`file`, resolved against `directory`, the fresh plan lacks and which still
exist -- unchanged in spirit from before, but now scoped to one output
directory instead of the project root, and keyed by the resolved path rather
than the raw string. The ROOT file is a copy of that database: replaced
whole, never merged, left untouched when byte-identical. When the replaced
root file held entries mcpp did not write (another tool's, or a stale mix of
toolchains), one warning states how many; an entry is mcpp's own when its
`output`, resolved against `directory`, lies under this project's `target/`
tree or under the mcpp home (the standard-library units' shared cache). The
fast path (execute.cppm, try_fast_build and try_fast_run) publishes the root
file through the same function the full path uses, reading the configuration
database already on disk, so a deleted root file returns on the next build
without a plan (C1). `emit build-database` still writes neither file. The
scaffold's .gitignore lists compile_commands.json.

W2 (design §3.3, §3.4). `directory` is the output directory the compiler
actually runs in, for every project unit and every toolchain, matching the
JSON Compilation Database format, S1-8-2 and what CMake, ninja and xmake
already write (C3); before this, GCC's importers failed when replayed from
`directory` and wrote gcm.cache/ into the project root. A unit that provides
a module also carries its dialect's language flag (BmiTraits::
moduleInterfaceLangFlag) immediately before `-c`, so a reader does not infer
it from the extension the way clang's driver cannot for `.ixx` (C4); the
MSVC form stays out until a Windows measurement of clang-cl-mode clangd
answers whether it accepts `/interface`.

W3 (design §3.5). The standard-library units a build compiles are recovered
once, in prepare.cppm, from the same derivation `ensure_built` and
`describe_std_module` both read, and carried on the plan
(BuildPlan::stdModuleUnits) rather than recovered again by each renderer:
compile_commands.json, `emit --spec compile-commands` and the S1 document
(mcpp.build.build_database) now render the identical record for these units,
which follows S1-12-1 and could not previously disagree because it did not
exist in the JSON-format documents at all (D5a). The S1 document's `provides`
for `std`/`std.compat` now names the BMI path in the shared std cache instead
of an empty string (D5b), and `ide.toolchains.<id>.build-id` carries the
compiler's build identity -- the same value already computed as one field of
the toolchain fingerprint (the driver's normalized --version banner, or a
hash of the driver binary) -- stable across two runs of one toolchain.
Project modules keep "" in `provides` under `emit`, unaffected.

`mcpp.build.plan` gains `recover_invocation`/`split_command_words`, moved
down from `mcpp.build.build_database` (which re-exports them under their
established names for its own tests) so both `mcpp.build.compile_commands`
and `mcpp.build.build_database` -- neither of which may import the other --
can render from the same recovered record without a dependency cycle.

Tests: unit (test_compile_commands.cpp: the within-configuration merge
resolves `file` against `directory`; a project unit's `directory` is the
output directory; the language flag before `-c` for an interface unit and
its absence for an implementation unit and for MSVC); e2e 781-786 (new,
against 2026.9.26.1: 781, 784 and 785 fail as designed); e2e 211 and 47
updated for `directory` now naming the output directory; e2e 688 updated for
the standard-library units now appearing in both the S1 document's compile-
commands export and the real build's compile_commands.json.
…rary entries

docs/01 (and its zh counterpart) now state what C6 left undocumented: the
root compile_commands.json is a copy of the current configuration's own
database under target/<triple>/<fingerprint>/, replaced whole rather than
merged, a symlink at the root is followed, a replace that discarded another
writer's entries is reported, and a build that imports `std` lists the
standard-library units alongside the project's own.

SPEC-005 (docs/specs/build-database.md): R3.7 states that `work-directory` is
the directory the compiler runs in (not the project root) and that a unit
providing a module carries its interface language flag before `-c`; R3.8
states that the standard-library units' `provides` names the BMI path in the
shared std cache and that `ide.toolchains.<id>.build-id` carries the
compiler's build identity; R4.1 states that the compile-commands document
includes the standard-library units, per S1-12-1. The header, version line
and change log are left for the pull request's integration commit, which
also carries R2.5 and R5.2 (T2's).
…rface flag and the standard-library units (#699 report)
…put (mcpp#701, mcpp#702)

Re-implements #701/#702's two prior commits to design
.agents/docs/2026-09-26-compile-database-and-issue-699-design.md §5.4, and
adds P, the `prepare` role the design's O1 asks for. One protocol bump (12)
and one cache-format epoch bump (3) cover all three.

R1'. `mcpp::runtime_search_dir(dir)` (`mcpp:runtime-search-dir=`, protocol
12) replaces the branch's `runtime_library_dir`: a launch-time search
directory a build.mcpp discovers (a vcpkg prefix's `bin/`, a Qt SDK's
`bin/`, a directory a `prepare` action populates) rather than one an author
writes into a fixed TOML array. It lands DIRECTLY on
`LinkIntent::runtimeSearchDirs` -- the field `runtime_search_dirs` itself
populates -- not the retiring `RuntimeConfig::libraryDirs` the branch had
joined by mistake, so every existing reader (the plan merge, RUNPATH/rpath,
`mcpp run`'s loader path, `mcpp pack`'s closure search) sees a
directive-declared directory exactly as it sees the manifest key's, and a
dependency's declaration reaches its consumer's executable through the same
merge. The directory need not exist when the program runs.

R2. After a `check` or `prepare` command succeeds, the engine creates every
declared stamp that is missing and sets the modification time of every
existing one to the present, whether or not the command wrote it; on
failure it writes nothing. This replaces the branch's before/after time
comparison with R2's simple form: a stamp feeds no compile or link edge, so
touching one the command already wrote changes no build.

P. The `prepare` role (`mcpp::roles::prepare`, alongside `source`, `check`,
`object`, `artifact`) is for construction whose file names are unknown when
the build program runs -- installing a vcpkg manifest or a CMake subproject
into a prefix, unpacking an SDK. Outputs: one or more stamps, written by the
engine through the same wrapper as `check`, and one declared directory
(`mcpp::action::output_dir`) the command populates. When the command
succeeds and that directory does not exist, the engine writes no stamp and
fails the edge, naming the directory. Every compile edge of the declaring
package waits for it, as a blocking `check` does today; so does every LINK
edge in the plan, because `plan.linkIntent` -- and therefore the `$ldflags`
every link rule interpolates -- is one value merged from every package's
LinkGlobal directives, read alike by the declaring package's own link and
by every package that depends on it. The #534 self-check
(`check_action_ordering`) is extended to assert this for links, the same
way it already does for compiles. Progress lines label it `PREPARE`. An
unknown role string is refused, naming the value and the five roles, where
it used to be read as `source` silently -- the cache-format epoch moves to
3 so a stale entry from before this guard existed is re-validated rather
than replayed two different ways by two code paths. A build program whose
`rerun_if_changed`/`rerun_if_changed_glob` names a path inside a `prepare`
directory (of any package in the graph) is warned, naming both paths: that
directory is populated at build time, after configuration has already run.
…1, mcpp#702)

779: rewritten for runtime_search_dir (renamed from ...runtime_library_dir):
RUNPATH in build.ninja, mcpp run, a build.mcpp cache-hit replay, mcpp pack,
and a dependency's declaration reaching a consumer's executable through the
same LinkGlobal merge.

790: a prepare action's command writes a header and a shared library into a
directory whose contents build.mcpp never names; a unit that includes the
header compiles and a program linked against the library runs through
runtime_search_dir on the first build; the second build runs nothing; after
an input of the action changes, it runs once and then not again.

791: a prepare action whose command creates nothing fails naming its
declared directory. The stamp is declared beside that directory, not
inside it -- ninja creates a declared output's own parent directory before
running any edge, so a stamp nested inside the directory would make it
"exist" regardless of what the command did.

792: a build program whose rerun_if_changed or rerun_if_changed_glob names a
path inside a prepare directory is warned, naming both paths; a re-run input
outside any such directory is the silent control.

793: an unknown action role is refused, naming the value and the five roles;
each real role, including a real object action, is the control.

794/795: the R1' Windows leg -- a DLL in a declared runtime search directory
is found by `mcpp run` through PATH and placed beside the packed program by
`mcpp pack`. 794 requires windows and runs on Windows CI; this host cannot
run it. 795 exercises the identical property through mingw-cross + wine,
which this host can and does run, giving this round a passing reading
before release.
…e prepare role and the stamp rule (mcpp#701, mcpp#702)

docs/30, and zh: a new section states the three kinds of work a build.mcpp
or the rules it imports do (SPEC-007 §1) -- configuration depends only on
declared inputs, construction is never done while build.mcpp runs, an
incomplete environment is a warning, and one behaviour serves planning and
building alike. `dep_bin` gains the paragraph on a host tool that fails to
build under `emit build-database`: naming it still configures correctly,
calling it while configuring does not. The directive tables and the typed
API table rename runtime-library-dir/runtime_library_dir to
runtime-search-dir/runtime_search_dir and correct what it joins; the worked
section is rewritten to match, with the Qt example fixed to pass `lib/` to
link_search and `bin/` to runtime_search_dir. The action role table gains
`prepare`, spelled with the `mcpp::roles` constants; a new section states
its contract (output_dir, the post-condition, the ordering, the R1.3
warning). The stamp-rule paragraph is rewritten for R2's simple form.

docs/31, and zh: the role table gains `prepare` and points at docs/30's
fuller contract and at SPEC-007.

docs/04, and zh §2.11: `runtime_search_dirs` (not the retiring
`library_dirs`) is named as the field the build-program form joins.
…le constants (#701, #702)

Conflicts resolved: the epoch-3 comment carries both reasons (#703, #702);
a link edge's one order-only list carries the deployed DLLs and the prepare
phonies; the runtime-search-dir residue mirror lands in T2's restructured root
block. The prepare post-condition asks for a populated directory rather than
an existing one, because ninja creates a stamp's parent before the command
runs; e2e 791 checks a stamp beside and inside the directory.
…t's copies resolve again from their directories

A prepare action rewrites files ninja does not know as outputs, so the DLL it
replaced was placed one build late; the placement edge now takes the graph's
prepare stamps as implicit inputs. A copy placed beside the program was then
resolved in the program's directory before the runtime search directories and
shadowed the newer version; the stamp records the names placed, and read_closure
resolves those names outside the program's directory (notInFirstDir).
@speak-agent speak-agent changed the title 2026.9.27.1: a build program declares a runtime library directory, and a passing check moves its stamp 2026.9.26.2: one compile database per configuration, emit plans every member, prepare actions and runtime search directories, DLLs beside a Windows program, link flags as words Sep 26, 2026
…atform, python3 in the hermetic llvm step

The unit tests compare a native-spelled directory as a path and JSON-escape a
Windows path; e2e 781 and 782 build with the machine's default toolchain, and
781 asks for no plan only where the fast path exists (ELF); e2e 211 derives the
sibling fixture from the entry's own file, which mcpp spells; the hermetic
container gains python3 for the compile-database scripts.
…spellings of a Windows temporary directory share
@Sunrisepeak
Sunrisepeak merged commit c109fdd into main Sep 26, 2026
40 of 42 checks passed
Sunrisepeak pushed a commit that referenced this pull request Sep 26, 2026
…adings (#706)

The design record gains §12.3 and §12.4:
- mcpp 2026.9.26.2 (#702, c109fdd), released and mirrored to GitCode from a
  CN host, and indexed through xim-pkgindex#880;
- the fresh-install matrix, green except the xcode-27 legs of #669;
- the sandbox check of the published artefacts (20 ok) and the same script
  against 2026.9.26.1, which fails exactly the 16 checks the release changes;
- mcpp-index#470 and its full sweep, every workspace shard green;
- S2 0.3.0 in mcppls 0.0.5 (#26);
- what remains open.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment