From ca6646f7fdfdc203ce687ba2f9cc9c1129aa5c8c Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:51:53 +0800 Subject: [PATCH 1/8] 2026.10.1.2: a pack states its build, every drive takes the job count, and a unit's compile does not depend on the member selection (#751, #753) Implements .agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md. - Every ninja drive takes the job count and the stale-token reclaim from the plan, and is reported whenever the command opened the report, unless its caller states that it writes its own lines (mcpp test's per-test drives). - mcpp pack states its build: package lines, the status row, and Finished before the first Packing line; one Finished for several configurations; the dispatch pass is reported; a pack fills the global dependency cache. - Every package's BMIs lie below its own directory except the root's, as object files do since #233; a unit reads one module map of what it reaches through its imports (a GCC mapper, or a clang/MSVC argument file). - A file a member lists from outside its directory belongs to that member, not to the workspace's virtual root. - Position independence follows the target: every unit of an ELF target that is not freestanding is compiled with -fPIC. --- ...nd-selection-independent-compile-design.md | 559 ++++++++++++++++++ .agents/docs/README.md | 6 +- CHANGELOG.md | 66 +++ docs/04-mcpp-toml.md | 4 +- docs/05-dependencies.md | 24 +- docs/07-workspace.md | 14 +- docs/10-pack-and-release.md | 10 + docs/91-toolchain-internals.md | 2 +- docs/specs/build-database.md | 18 +- docs/zh/04-mcpp-toml.md | 4 +- docs/zh/05-dependencies.md | 15 +- docs/zh/07-workspace.md | 12 +- docs/zh/10-pack-and-release.md | 6 + docs/zh/91-toolchain-internals.md | 2 +- mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/bmi_cache.cppm | 9 +- src/build/backend.cppm | 19 +- src/build/execute.cppm | 57 +- src/build/ninja_backend.cppm | 193 ++++-- src/build/plan.cppm | 293 +++++---- src/build/prepare.cppm | 9 + src/build/prepare/plan.cpp | 24 +- src/build/progress.cppm | 9 + src/cli/cmd_build.cppm | 9 +- src/pack/library_pipeline.cppm | 23 +- src/pack/pipeline.cppm | 75 ++- tests/e2e/09_path_dependency.sh | 4 +- ..._module_name_is_unique_within_a_program.sh | 18 +- ...ed_bmi_waits_for_a_module_compiled_here.sh | 10 +- ...ild_and_every_drive_takes_the_job_count.sh | 114 ++++ ...er_compiles_once_whatever_the_selection.sh | 292 +++++++++ tests/unit/test_module_address.cpp | 309 ++++++++++ tests/unit/test_object_address.cpp | 44 ++ 34 files changed, 1983 insertions(+), 274 deletions(-) create mode 100644 .agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md create mode 100644 tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh create mode 100644 tests/e2e/872_a_member_compiles_once_whatever_the_selection.sh create mode 100644 tests/unit/test_module_address.cpp diff --git a/.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md b/.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md new file mode 100644 index 000000000..be29dfb32 --- /dev/null +++ b/.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md @@ -0,0 +1,559 @@ +--- +subject: design +status: active +--- + +# A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751) + +- Status: revision 2, being implemented as 2026.10.1.2. + - Revision 1 was reviewed on 2026-10-01. D1 to D4 were accepted as + recommended: one placement rule for every plan, uniform PIC on ELF + targets subject to M3, the module map as an argument file, and every + group's build before one `Finished`. D5: one pull request. + - Revision 2 adds the results of M1 and M2 (section 8), a review from + several angles (section 12), the tasks and their dependencies (section + 13), and the work in other repositories (section 14). +- Date: 2026-10-01. +- Origin: + 1. mcpp#753: `mcpp pack` reports its build as `Planning` for as long as the + build runs (release job of v2026.10.1.1: six minutes). + 2. mcpp#751: when two workspace members each compile a module of one name, + a shared member's compile commands differ between `-p` and + `--workspace`, so each switch recompiles it (GalTranslPP 3.1.3: 3m22s + and 4m30s on Windows). + 3. The member selection plan of 2026.10.1.1 + (`2026-09-30-member-selection-and-build-program-cost-plan.md`, section + 8), which recorded #751 as outside that release and named its two + possible remedies without choosing one. +- Task: separate what is mcpp's from what is not, and what is a defect from + what is mcpp's design; then state one design for each defect at the level + of its class, not of the reported instance. + +## 0. Summary + +| # | Finding | Source | Item | +|---|---|---|---| +| F1 | A cold `mcpp pack` prints no `Compiling` line and no `Finished`; the status row stays at `Planning` | measured, 2026.10.1.1 | A2, A3 | +| F2 | Every ninja drive assembles its own `BuildOptions`. Of ten construction sites, one sets the job count and one other attaches a report | code | A1, A2 | +| F3 | `[build] jobs = 3` reaches ninja as `-j3` from `mcpp build`, and not at all from `mcpp test` or `mcpp pack` | measured, 2026.10.1.1 | A1 | +| F4 | The reclaim of stale `detach-codegen` tokens runs only on the `mcpp build` path | code | A1 | +| F5 | A pack does not populate the global BMI cache for the dependencies it compiled | code | A4 | +| F6 | #751 reproduces: each switch between `--workspace` and `-p app` recompiles `core` | measured, 2026.10.1.1, llvm 22.1.8 | B1 | +| F7 | Two disjoint selections, `-p app` and `-p tool`, each with its own module `m`, place it at the same flat path and evict each other: each switch recompiles `core`. Not reported in #751 | measured | B1 | +| F8 | A workspace with a member that builds a shared library: `--workspace` adds `-fPIC` to every unit's flags, `-p app` does not, and each switch recompiles every member. No module is involved | measured | B2 | +| F9 | F6 to F8 share one cause: a fact computed over the plan's graph decides a unit's command line, and since 2026.9.29.1 a `-p` plan holds the selected closure while the build directory is shared by every selection of the configuration | code | B0 | + +Two designs follow. + +- **A. Every ninja drive of a plan is configured from the plan** (#753 and + F2 to F5). The backend reads the job count and the token reclaim from the + plan. A drive is reported unless its caller states that it reports the + drive itself. `mcpp pack` writes `Finished` before `Packing`. +- **B. A unit's compile edge does not depend on the member selection** + (#751 and F7, F8). The BMIs of every package other than the root are placed + below that package's directory, as object files have been since #233. Each + importer receives one module map derived from its own closure. Position + independence becomes a property of the target and stops being a census of + the graph. + +## 1. Triage + +### 1.1 #753 + +The whole report is mcpp's, and its expectation is mcpp's own stated +contract. `cmd_pack` opens the progress region with the comment "The build is +reported as `mcpp build` reports it; `Finished` closes the report before the +pack's own lines" (`src/cli/cmd_publish.cppm:204`). No build in the pack +pipeline attaches a report (`src/pack/pipeline.cppm:167`, `:1152`, `:1317`; +`src/pack/library_pipeline.cppm:189`). The comment states a contract that +the code does not implement. + +The reported instance is one symptom of F2. The same omission drops the +declared job count (F3). A declared value that is silently dropped is a +defect, whatever feature it belongs to. + +### 1.2 #751 + +The defect is real and general: it affects any workspace in which two +members provide one module name, which 2026.9.30.2 (W10, #732) made legal. +Four parts of the report are corrected or excluded. + +| Claim in #751 | Verdict | Reason | +|---|---|---| +| "A member's compile commands depend on the member and its own dependencies, not on unrelated members of the graph" | **Too broad** | mcpp makes two inputs selection-dependent by design. Features are unified over the selection, Cargo-style (docs/06), and e2e 851 C asserts that `-p app23b` compiles `lib` without the feature only `app23` asks for. A member selected with `-p` holds the root's declaration privileges (e2e 850). The defect is a command line that changes while its meaning does not. Section 4.0 states the invariant in that form | +| "Then `-p` shares what `--workspace` built, as e2e 851 states" | **Incorrect citation** | 851 asserts one build directory per configuration and, in C, a recompile for a different feature union. It asserts nothing about recompiles | +| Remedy 2: "decide the per-provider mapping by what one program links" | **Insufficient alone** | With the placement still decided per graph, the `--workspace` graph would hold two BMIs at `pcm.cache/m.pcm`. A placement that does not depend on the graph is required, and that is remedy 1 | +| The comment's "CLI pack's tail" (8m40s at `Running 3/3`) | **Not designed for** | The comment states that it is not attributed. It was measured on 2026.9.30.2, whose per-member pack path 2026.10.1.1 replaced (#749). It is re-measured in the cross-verification (section 9) after B | + +One part of the GalTranslPP case is the project's own: its updater and its +core each compile `3rdParty/3rdModule/boost.ixx`. W10 allows this and notes +that a package both depend on would compile the file once. Whether to move +the file into such a package is a usage choice, not an mcpp item. + +## 2. Measurements + +All runs use the released 2026.10.1.1 +(`~/.xlings/data/xpkgs/xim-x-mcpp/2026.10.1.1/bin/mcpp`) on Linux x86_64 with +the machine's default toolchain, llvm 22.1.8. The fixtures are in the +session's scratchpad and are restated in the e2e criteria of section 10. + +**F1.** A workspace `{util, app -> util, dso}`, `rm -rf target`, then +`mcpp pack -p app --format tar`. The output goes from `Target ...` directly +to `Packing app v0.1.0`; no `Compiling` and no `Finished` line appears. The +same tree under `mcpp build -p app` prints `Compiling util`, +`Compiling app` and `Finished`. + +**F3.** A package with `[build] jobs = 3`, ninja argv traced with +`strace -f -e trace=execve`: + +| command | ninja invocations | `-j` | +|---|---|---| +| `mcpp build` | 2 | `-j3`, `-j3` | +| `mcpp pack --format tar` | 2 | none (`--quiet`) | +| `mcpp test` | 3 | none | + +Without `-j`, ninja runs `nproc + 2` compiles. docs/04 defines `jobs` as "how +many compiles run at once" and resolves `"auto"` against free memory, +because "a single module interface compile peaks at 0.5–1.0 GB". `test` and +`pack` exceed the bound that the key exists to enforce. + +**F6.** The fixture of #751 (module renamed `.cppm`, `core`'s module named +`corelib`, since `core` is a reserved top-level name): + +| step | compiled | +|---|---| +| `build --workspace` | core, tool, app | +| `build -p app` | core, app | +| `build -p app` | nothing | +| `build --workspace` | core, app | +| `build -p app` | core, app | + +**F7.** The same workspace with `tool` given its own `m` (returns 7, `core`'s +returns 42): `-p app`, `-p tool`, `-p app` compiles `core` on the third step. +Every program prints its own value, so the results are correct; the cost is +the eviction. The build directory then holds `pcm.cache/m.pcm`, +`pcm.cache/core/m.pcm` and `pcm.cache/tool/m.pcm`. + +**F8.** Workspace `{util (lib), app -> util (bin), dso (shared)}`, no module +name shared. Each switch between `--workspace` and `-p app` compiles `util` +and `app`. The `--workspace` build.ninja carries `-fPIC` in its global +`cxxflags`; the `-p app` build.ninja carries none. The flag comes from +`plan.needsPic` (`src/build/flags.cppm:733`), which `make_plan` sets when +any link unit of the graph is a shared library (`src/build/plan.cppm:3201`). +The same value enters the global BMI cache key (`src/build/cache_key.cppm:422`), +so a cached dependency is also served from two entries. + +**Payload facts used below.** GCC 16.1.0 from xlings defines neither +`__PIC__` nor `__PIE__` by default. clang 22.1.8 for `x86_64-linux-gnu` +defines both as 2. xlings' build directory holds 144 BMIs. + +## 3. Design A: every ninja drive of a plan is configured from the plan + +### 3.1 The inventory + +| site | report | `-j` from plan | token reclaim | cache populate | +|---|---|---|---|---| +| `mcpp build` / `run` (`execute.cppm:986`) | yes | yes | yes | yes | +| `mcpp test` phase A (`:2876`) | yes | no | no | yes | +| `mcpp test` bulk, per test, per member (`:2932`, `:3280`, `:3800`) | own lines, by design | no | no | — | +| `mcpp pack` group build (`pipeline.cppm:1317`) | no | no | no | no | +| `mcpp pack` dispatch pass (`:1152`) | no | no | no | — | +| `mcpp pack` Android legs (`:167`) | no | no | no | no | +| `mcpp pack` library (`library_pipeline.cppm:189`) | no | no | no | no | +| configure (`configure.cppm:92`, dry run) | — | — | — | — | + +The comment above `scheduleNinjaJobs` (`execute.cppm:750`) states that +concurrency is resolved once and stamped on the plan "so this reads one +value instead of re-deriving it". One site reads that value. + +### 3.2 Items + +- **A1. The backend reads what the plan determines.** + - The ninja argv takes `-j` from `plan.scheduleNinjaJobs` in + `NinjaBackend::build`. `BuildOptions::parallelJobs` is removed: its only + setter passes the same value. + - The stale-token reclaim of a `detach-codegen` plan runs in the backend, + once per build directory per command, before that directory's first + drive. It cannot run before every drive, because a detached code + generation may outlive the ninja that started it, and its token would + then be live. + - No caller can omit either setting, because none sets it. +- **A2. A drive is reported unless its caller reports it.** + - `BuildOptions` gains `report`, with two values. `Region`, the default, + means the backend attaches a `progress::Build` for the plan's directory + whenever the command opened the region. `Caller` means the caller + writes its own lines. + - `mcpp test` passes `Caller` for its bulk, per-test and per-member + drives, so their output does not change. + - The default is the reported form. A new call site that says nothing is + therefore reported, which is the opposite of today. + - Machine output does not open the region, so `--message-format json` is + unchanged. +- **A3. `mcpp pack` states its build as `mcpp build` does.** + - It calls `programs_done()` before its first drive and + `configurations(n)` for `n` configuration groups. + - It writes one `Finished`, after the builds of all groups and before any + staging. The pipeline therefore builds every group first, then stages + and dispatches each group (D4). A group whose build fails is still + reported alone (#749, P3). + - The dispatch pass is a reported drive. Its steps are the format + provider's actions and are attributed to that package. It writes no + second `Finished`; `Distributing` and `Packed` follow it as today. +- **A4. One completion after a drive.** The population of the global BMI + cache (`execute.cppm:1003`, `:2895`) moves into one function that `build`, + `test` and every pack build call. `diag::flush` stays with each command, + which owns its `--strict`. + +### 3.3 What does not change + +- `mcpp build`'s output, and `mcpp test`'s per-test lines. +- The JSON envelope of `mcpp pack --message-format json`. +- The build graph: no edge and no `build.ninja` byte changes. + +## 4. Design B: a unit's compile edge does not depend on the selection + +### 4.0 The invariant + +A unit of package `P`, compiled in configuration `C`, has a compile edge +(command, outputs, inputs) that is a function of: + +- `C`; +- `P` and `P`'s closure; +- the feature union the selection activates for `P` (docs/06, e2e 851 C); +- the declarations a selected member holds as the root (e2e 850). + +No other fact about the plan's graph may reach a compile edge. The last two +inputs change what is compiled. Every other dependence on the selection +changes only how the same compile is spelled, and costs a recompile for +nothing. The invariant is stated in `docs/07-workspace.md` beside the +selection rules, and section 10 gives the test that enforces it. + +The cause of F6 to F8 is a census: a value computed over the whole graph and +written into every unit. W10 states its condition over "the configuration" +(`2026-09-30-build-wall-time-progress-count-and-hang-plan.md`, W10: +"`//` when it has more than one"). +The implementation evaluates the condition over `graph.providersOf` +(`src/build/plan.cppm:3216`). Since 2026.9.29.1 (#738), a `-p` plan holds +only the selected closure, and the build directory is keyed by the +configuration and shared by every selection of it (e2e 851 C). A plan +therefore cannot observe the configuration that W10's condition names. + +The codebase has met this machine before. Object paths were once decided by +a census over every unit. The comment at `src/build/plan.cppm:1755` records +three defects from it (#233, #240, #344) and its conclusion: "A conditional +layout is exactly the state that generated this bug family, and all it buys +is shorter paths." Dependencies' object paths have been unconditional since. +B1 applies the same rule to BMIs, and B2 to position independence. + +### 4.1 B1: BMI placement by provider, and lookup by closure + +- **Placement.** The root package's BMIs stay at `/`. + Every other package's BMIs go to `//`, + unconditionally. This is the object rule of #233. + - A workspace plan's root is virtual and provides nothing. Every member is + therefore qualified, in every selection. + - `std` and `std.compat` belong to the configuration and stay flat. + - The staging of a cached BMI uses the same function. The global cache's + entries do not change, only the path to which an entry is staged. +- **Lookup.** + - Each package whose closure provides a module outside the flat directory + gets one module map. The map lists every named module its units can + import (the closure's providers, `std`, `std.compat`, and names placed + by other means). This is the map that W10 writes for GCC today, with + the same content-hashed name (`modmap/-.map`), and it is + an input of every edge that reads it. + - Each compile takes one argument: + - GCC: `-fmodule-mapper=`. + - clang: `@.rsp`, holding one `-fmodule-file==` line per + name. + - MSVC: `@.rsp`, holding `/reference =` lines. + - Collation (`mcpp dyndep --module-map`) reads the same map, as it does + for W10. + - The resolver of W10 (`resolve_provider` over the importer's closure) is + unchanged. Only the predicate that decides whether to use it changes. +- **What is removed.** The `collided` set, and the split between bound and + unbound names: one path for every plan. +- **What changes, and for whom.** + - Every project that imports a module from a dependency gets new BMI + paths and one map argument per compile. It rebuilds once after the + upgrade. The fingerprint does not contain the engine version, so the + rebuild comes from ninja's command comparison, not from a new directory. + - A project whose modules are all its own (and `std`) is byte-identical. + - e2e 847 G changes from "no map without a collision" to "no map without + a dependency module". +- **What it also removes.** F7: a qualified placement cannot be evicted by + another selection. W10's clangd limitation, which concerns two providers + of one name in one database, stays as documented. + +### 4.2 B2: position independence is a property of the target + +- **P1 (recommended).** Every unit is compiled with `-fPIC` on a target + that has ELF shared objects: hosted Linux, the BSDs, and Android. Where + the compiler already defaults to PIC, as on Android, the flag is + redundant. + - On PE/MSVC there is no flag, as today. + - On Mach-O the compiler defaults to PIC, so nothing changes. + - A freestanding target has no shared objects and gets no flag, as today. + - `plan.needsPic` stops being a census. The `pic` field of the cache key + becomes a function of the target, so a cached dependency is served from + one entry whatever the selection. + - This is rustc's default relocation model on these targets, chosen for + the same reason: one artifact serves a program and a shared object. +- **P2.** PIC only for the units in the closure of a shared link unit of + the plan. This is narrower, but still a census over consumers: a package + that both a shared library and a program use differs between selections. + It removes F8's instance and keeps its class. +- **The cost of P1 is measured before it is decided** (M3). On GCC 16, whose + default is neither PIC nor PIE, P1 changes the code of every hosted ELF + build. On clang 22 it changes `-fPIE` to `-fPIC`. + +### 4.3 What B does not change + +- The feature union and the root's declaration privileges: these stay + selection-dependent by design (section 4.0). +- One build directory per configuration (#747, e2e 851). +- W10's rules: resolution in the importer's closure, refusal of two + providers in one closure, and refusal of one file reached twice in one + closure (e2e 847 A–F, 848). + +## 5. Alternatives rejected + +| Alternative | Why not | +|---|---| +| Take the census over the workspace's universe (every member's closure) at every plan | A `-p` plan would resolve and scan the closures of members it does not build. #738 plans `-p` on the selected closure because mcpp-index has 172 members and its CI runs `mcpp test -p` per member | +| Record the census in the build directory and read it back | Depends on order: a `-p` before the first `--workspace` still spells the commands differently | +| Put the census into the directory key | Each selection keeps its own warm directory. But `build --workspace` followed by `run -p X`, the measured case, then compiles in a cold directory | +| #751's remedy 2 alone | Two BMIs of one name at one flat path in the `--workspace` graph (section 1.2) | +| Per-package search paths (`-fprebuilt-module-path` and `/ifcSearchDir` for each closure package) | A stale BMI in one package's directory can answer for a name that another package of the closure now provides: a wrong answer instead of an error | +| Per-name flags on the command line | About 19 KB per clang compile for xlings' 144 BMIs. This approaches Windows' 32,767-character command line once paths are absolute | +| A: only attach a report at the four pack sites | Leaves F3 to F5. The next call site repeats the omission, because the default stays unreported | + +## 6. Relation to W10 + +W10 rejected "explicit maps for every unit" because "it changes every +`build.ninja`, every database entry and every BMI path, with no gain for the +projects that have no collision". B1 is narrower: only projects with +dependency modules change. Its gain was not visible when W10 was written. A +collision is a property of the configuration, and no single plan can observe +the configuration since #738 and #747. This record therefore supersedes +W10's placement rule and keeps W10's resolution rule. + +## 7. Decisions for review + +- **D1. The scope of B1.** + - Option (a), recommended: one rule for every plan, the #233 rule. + - Option (b): the rule for workspace plans only. A single package's plan + keeps W10's census, which is stable there because that package's graph + is its configuration. + - (b) changes no build of a project outside a workspace. (a) has one rule + and no census, at the cost of one rebuild and new database entries for + every project with dependency modules. +- **D2. Position independence.** + - Option (a), recommended: P1, subject to M3. + - Option (b): P2. + - Option (c): take F8 out of this design into its own issue, and ship B1 + alone. +- **D3. The lookup spelling for clang and MSVC.** + - Recommended: the map as a response file, one argument per compile. The + compile database writes the flags expanded if M2 shows that a reader + does not expand `@file`. + - Alternative: per-name flags. +- **D4. The order of a pack over several configurations.** + - Recommended: every group's build, then one `Finished`, then staging and + dispatch. + - Alternative: per group, with `Finished` deferred to the end, which then + follows the first `Packing` line. +- **D5. Delivery.** A and B as one pull request and one release, or A + first. A is small, and B changes every project with dependency modules. + +## 8. Measurements before the implementation + +| # | Question | Decides | +|---|---|---| +| M1 | On GCC 16, clang 22 and MSVC: a BMI compiled in one directory and staged from the cache into `//` imports a second staged BMI. Does it load in a consumer whose map names both, when the paths recorded in the BMI differ from the paths of use? | B1 at all | +| M2 | Do clangd 22 and mcppls read a database entry that carries `@.rsp` (clang) or `-fmodule-mapper=` (GCC)? | D3 | +| M3 | GCC 16 and clang 22, mcpp's own build and unit tests, with and without uniform `-fPIC`: object and binary size, build wall time, test run time | D2 | +| M4 | The length of the longest compile command of xlings on Windows, before and after B1 | D3 (confirmation) | + +**Results (revision 2).** + +- **M1, GCC 16.1.0 and clang 22.1.8: holds.** Module `A` imports `B`. Both + were compiled in one directory, copied to two other directories + (`pa/A`, `pb/B`), and the original directory was deleted. A consumer that + imports both compiles, links and runs, given a GCC mapper file or a clang + argument file that lists both. The consumer fails when the map lists only + `A`: clang reports `failed to find module file for module 'B'` and GCC + `B: error: failed to read compiled module`. The map must therefore hold the + importer's whole closure, which section 4.1 already requires. MSVC is + covered by e2e 848 on the Windows leg. +- **M2, clangd 22.1.8: holds.** clangd expands an `@file` argument relative + to the database's `directory` and applies the `-fmodule-file=` lines it + holds, with and without `--experimental-modules-support`, and reports no + error. +- **M2, mcppls: holds.** mcppls expands response files relative to the + entry's directory (`src/project/compdb.cpp`, `expand_response_files`), + drops `-fmodule-file=` and `-fmodule-mapper=` as module mechanics + (`src/spec/options.cpp`, S1-9-4), and drops an unexpanded + `@.modmap`, which is CMake's spelling of the same mechanism. The + argument file is therefore named with the `.modmap` suffix. mcppls then + reads it as module mechanics whether or not the file exists yet. +- M3 is measured on the implementation (section 13, T5). M4 reduces to one + argument per compile under D3, and the Windows legs of CI confirm it. + +## 9. Delivery and verification + +- **mcpp.** One or two pull requests (D5), with the e2e criteria of section + 10 on the CI matrix (GCC and clang on Linux, clang on macOS, clang and + MSVC on Windows). +- **Ecosystem.** mcppls is a release canary and reads the database (M2). + xlings is built from the pull request branch: its build directory changes + under B1 and must build, test and pack. +- **The validation project.** GalTranslPP is built from the mcpp pull + request branch on a temporary branch, and both runs are green before the + merge. Four checks: + - `run -p GPPCLI` after `build --workspace` compiles nothing; + - the pack shows `Compiling` and `Finished`; + - the CLI pack's tail from #751's comment is re-measured; + - the result is reported on #751. +- **mcpp's own release job.** Its `pack --target x86_64-linux-musl --mode + static` shows the build's progress (#753's measurement). + +## 10. Criteria + +**A** + +- (CHANGE) A cold `mcpp pack --format tar` prints a `Compiling` line for + each package that compiles, and `Finished` before `Packing`. Under + `--message-format json`, stdout is one JSON document. Fails on 2026.10.1.1. +- (CHANGE) `[build] jobs = 1`, with four translation units of about 0.3 s + each. In `.ninja_log`, no two compile steps overlap under `build`, `test` + or `pack`. Fails on 2026.10.1.1 for `test` and `pack`. +- (unit) With default `BuildOptions`, the ninja argv carries + `-j`. +- (KEEP) `mcpp build`'s output, and `mcpp test`'s per-test lines. + +**B.** One fixture workspace, run on GCC and clang (and on MSVC in Windows +CI), holds: + +- a member reached by two members; +- two members each providing `m`: one from a distinct file, and one from + the same file through `..`; +- a member with a shared target; +- a dependency from the index that provides a module and is served from the + cache. + +Its criteria: + +- (CHANGE) For every member `M`, compare `mcpp emit build-database -p M` + with `--workspace`. For every source of `M`'s closure, the entries' argument + lists are equal. The entries are compared as parsed JSON, per source, not + by substring. This fails on 2026.10.1.1 for F6 and F8. +- (CHANGE) `build --workspace`, then `build -p M` for every `M`: `.ninja_log` + gains no compile entry. `-p app; -p tool; -p app`: the third gains none + (F7). +- (KEEP) A variant with a feature difference: the two argument lists differ + exactly by that feature's `-D`, which is e2e 851 C restated per entry. +- (KEEP) e2e 847 A–F, 848, 849, 850 and 851. (CHANGE) e2e 847 G, as in + section 4.1. +- (KEEP) The byte comparison of `build.ninja` and `compile_commands.json` + for every fixture whose modules are all root-owned. + +## 11. Risks + +- B1 moves the BMI paths of most real projects. Eleven e2e scripts read + `pcm.cache/` or `gcm.cache/` paths, and each is reviewed for a flat-path + assumption about a dependency's BMI. +- Under P1, every hosted ELF entry of the global cache is missed once, + because its key changes. +- A third census may exist outside the inventory of section 4. The + criterion of section 10 B is written to find one: it compares whole + argument lists, not the two flags this design knows about. + +## 12. Review from several angles (revision 2) + +- **Architecture.** + - A: every value a drive takes from the plan is read by the backend, the + one component every command reaches. A caller states only what is its + own: goals, keep-going, a timeout, and whether it writes its own lines. + - B: one placement rule for objects and BMIs, decided by the provider + alone. One resolver (W10's) answers every import. Position independence + is read from the target, as the object format already is. + - Removed: the `collided` census, `BuildOptions::parallelJobs`, the cache's + exclusion of qualified BMIs, and the graph scan behind `needsPic`. +- **Stability.** + - B1 moves the BMIs of every dependency. M1 establishes that a moved BMI + loads through a complete map on GCC and clang. The cache-served case is + an e2e on every leg, MSVC included. + - The token reclaim runs once per build directory per command. A detached + code generation of an earlier drive in the same command therefore keeps + its token. + - A pack builds every group before staging any of them. A group's failure + stays its own, as #749 requires. +- **Simplicity.** B1 deletes a branch rather than adding one: a plan without + dependency modules and a plan with a collision take the same path. A adds + one enumerator to `BuildOptions` and removes one field. +- **User experience.** + - `mcpp pack` shows the build it performs: the package lines, the status + row and `Finished`. + - `[build] jobs`, `--jobs` and `MCPP_JOBS` bound every command that + compiles. + - Switching between `-p` and `--workspace` recompiles only what the + feature union changes. +- **Compatibility.** + - No manifest key, no command-line option and no JSON field is added or + changed. + - The global cache keeps its entry layout. Under P1 the `pic` field of + hosted ELF keys changes, so those entries are missed once and then + refilled. + - The build database gains one argument per compile for a package that + imports a dependency's module: `-fmodule-mapper=` on GCC, `@.modmap` + elsewhere. Both readers of the ecosystem accept it (M2). +- **Platforms.** + - GCC reads the mapper file. clang reads the argument file, which is + tokenised by the host's rules, so a path is quoted when it holds a space + or a quote. MSVC reads `/reference` lines from a UTF-8 file with a byte + order mark, as every response file mcpp writes for an MSVC tool. + - P1 applies to ELF only. Mach-O already defaults to PIC, PE has no such + flag, and WebAssembly and freestanding targets have no shared objects of + the kind the flag serves. +- **Consistency.** `build`, `test` and `pack` drive ninja through one + backend with the same job count and the same report. The rule of #233 now + covers both kinds of build output that a package contributes. +- **Upgrade without action.** Nothing is to be done by a user. The first + build after the upgrade recompiles a project that imports dependency + modules (its command lines changed) and, on hosted ELF, every project + (`-fPIC`). The build directory, whose key does not change, is reused. +- **Test coverage.** Every item has a criterion that fails on 2026.10.1.1 + (section 10). The property test of section 10 B compares whole argument + lists, so it also detects a census that this design has not listed. + +## 13. Tasks and dependencies + +| Task | Content | Depends on | +|---|---|---| +| T1 | A1: `-j` and the token reclaim in the backend; `parallelJobs` removed | — | +| T2 | A2: `BuildOptions::report`; the backend attaches the report; `mcpp test`'s own drives pass `Caller` | T1 (same function) | +| T3 | A3: `mcpp pack` and the library pack state their build; builds before staging; the dispatch pass is reported | T2 | +| T4 | A4: one function populates the global cache after a drive; called by `build`, `test` and `pack` | — | +| T5 | B2: position independence from the target; M3 measured on mcpp's own build | — | +| T6 | B1: placement by provider; the map for every importer of a qualified module; the `.modmap` argument file; the cache's BMI artifacts carry their build path | — | +| T7 | Unit tests and e2e for T1 to T6 (section 10); existing e2e that read flat BMI paths reviewed | T1–T6 | +| T8 | Documentation (docs/04, 05, 07, 10, 91, the build-database specification, their Chinese counterparts), CHANGELOG, version 2026.10.1.2 | T1–T6 | +| T9 | Pull request; CI on every leg; GalTranslPP built from the branch | T7, T8 | +| T10 | Review of the pull request and of the ecosystem | T9 | +| T11 | Merge, release, GitCode assets, xim-pkgindex, mcpp-index | T10 | +| T12 | Sandbox verification from the index; comments on and closure of #751 and #753 | T11 | + +T1, T4, T5 and T6 touch disjoint code and may be done in any order. They are +done in one branch, by one author, because T6 and T5 both change the +command lines that T7's criteria compare. + +## 14. Work in other repositories + +| Repository | Work | When | +|---|---|---| +| Sunrisepeak/GalTranslPP, PR 3 | A temporary branch builds mcpp from the pull request and runs the workflow; both runs green is a merge condition. After the release, PR 3 runs on the released version | T9, T12 | +| mcpp-community/mcpp-language-server | Release canary; no change expected (M2) | T11 | +| openxlings/xlings | Release canary; built and tested with the release in the sandbox; no change expected | T11, T12 | +| openxlings/xim-pkgindex | The release workflow opens the version bump; merged by a maintainer | T11 | +| mcpp-community/mcpp-index | The `latest_mcpp` pin moves to the release, and the full sweep runs | T11 | diff --git a/.agents/docs/README.md b/.agents/docs/README.md index c357062c8..f3c3cde5e 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -321 records. +322 records. ## By subject @@ -30,6 +30,7 @@ Records that declare one. Everything else is listed by date below. ### design +- [A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751)](2026-10-01-pack-drive-and-selection-independent-compile-design.md) — active - [Member selection, build programs prepared once, a pack over several members, and the output streams of `mcpp run`: the plan for the release after 2026.9.30.2 (#748, #749, #750)](2026-09-30-member-selection-and-build-program-cost-plan.md) — landed - [The build's wall time, its progress count, a hang after the build, and #732 and #744: measurements and a remediation plan](2026-09-30-build-wall-time-progress-count-and-hang-plan.md) — landed - [Build output, revision 3: every package that does work is named, the live display is one line drawn in one write, and a repeated warning is stated once per file](2026-09-30-build-output-refinement-design.md) — landed @@ -110,6 +111,9 @@ Records that declare one. Everything else is listed by date below. ## By date +### 2026-10 + +- [A pack's build reported as a build, and a unit's compile independent of the member selection: triage and design (#753, #751)](2026-10-01-pack-drive-and-selection-independent-compile-design.md) — active ### 2026-09 - [Member selection, build programs prepared once, a pack over several members, and the output streams of `mcpp run`: the plan for the release after 2026.9.30.2 (#748, #749, #750)](2026-09-30-member-selection-and-build-program-cost-plan.md) — landed diff --git a/CHANGELOG.md b/CHANGELOG.md index 87425db74..ed61d083b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,72 @@ > Each `## []` section is that release's notes. Entries are written in English > from 2026.9.28.3 on; earlier entries remain as written. +## [2026.10.1.2] - 2026-10-01 + +This release implements the design for a pack's build and a compile that does +not depend on the member selection +(`.agents/docs/2026-10-01-pack-drive-and-selection-independent-compile-design.md`), +and resolves mcpp#751 and mcpp#753. A project that imports a module of a +dependency, and every project on an ELF target, is compiled once more after the +upgrade, because its commands change (both under **Changed**). + +### Fixed + +- **A unit is compiled by the same command in every selection of a + workspace.** The selections of one configuration share a build directory, + and three facts about the whole graph reached the commands of members they + did not concern, so each switch between `--workspace` and `-p` recompiled + them (mcpp#751; GalTranslPP 3.1.3 recompiled its core for 3m22s): + - a module name that two members provide moved every BMI of that name below + its provider's directory, and told the importers so, only when the graph + held both providers; + - a file that a member lists from outside its own directory was owned by the + workspace's virtual root, whose object census depended on how many members + listed it; + - a member that builds a shared library put `-fPIC` on every unit of the + graph. + A command now depends on the unit's package, the packages it reaches, the + features the selection activates for it and the declarations a selected + member holds as the root, and on nothing else in the graph. `mcpp build + --workspace` followed by `mcpp build -p ` compiles nothing, and so + does any alternation of selections (e2e 872). +- **`mcpp pack` states the build it performs.** Its build wrote no package + line, no status row and no `Finished`, so a release job whose pack + recompiled mcpp showed `Planning` for six minutes (mcpp#753). A pack is now + stated as `mcpp build` states one, with `Finished` before the first `Packing` + line and one `Finished` for a pack over several configurations; the second + pass of a dispatched `--format` is stated in the same way (e2e 871). +- **`[build] jobs` bounds every command that compiles.** `mcpp test` and + `mcpp pack` ran ninja's default number of jobs whatever `[build] jobs`, + `--jobs` or `MCPP_JOBS` said, which on a machine with little memory exceeded + the bound the key exists to enforce. The backend reads the job count from + the plan, and the reclaim of stale `bmi_schedule = "on"` tokens, which ran + only under `mcpp build`, runs before the first drive of every build + directory (e2e 871). +- **A pack fills the global dependency cache** with the dependencies its + build compiled, as `mcpp build` and `mcpp test` do. + +### Changed + +- **Every package's BMIs lie below the package's directory, except the root + package's.** The rule is the one object files have followed since mcpp#233: + `gcm.cache//.gcm` (`pcm.cache` with clang). A unit that + imports a module of another package reads one module map of the modules it + reaches through its imports: a mapper file with GCC, and an argument file + `@/modmap/-.modmap` of `-fmodule-file=` or + `/reference` lines with clang or MSVC. A project whose modules are all its + own is laid out, and every command spelled, as before. A package whose + module name another package of the graph provides is served from the global + dependency cache again; 2026.9.30.2 compiled it in the project. +- **Every unit of an ELF target that is not freestanding is compiled with + `-fPIC`,** whether or not the graph links a shared library, as rustc's + default relocation model does on these targets: an object compiled once + serves a program and a shared object. Mach-O compilers produce + position-independent code by default, PE has no such flag, and nothing + changes on those targets or on freestanding ones. The global dependency + cache keys these entries by the flag, so each is compiled once more and + then served again. + ## [2026.10.1.1] - 2026-10-01 This release implements the plan for member selection, build programs prepared diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md index ab90baa69..b79af2dc5 100644 --- a/docs/04-mcpp-toml.md +++ b/docs/04-mcpp-toml.md @@ -819,7 +819,9 @@ jobs = "auto" # or a positive number; --jobs / MCPP_JOBS override it bmi_schedule = "off" # auto (default, = off) | on | off ``` -`jobs` is how many compiles run at once. `"auto"` is resolved **against the +`jobs` is how many compiles run at once, in every command that compiles: +`build`, `run`, `test` and `pack` (2026.10.1.2+; before it, `test` and `pack` +ran ninja's default number of jobs whatever the key said). `"auto"` is resolved **against the machine doing the build**, never frozen into the manifest: it takes the physical core count on a heterogeneous CPU (a 13900K is 8 P-cores + 16 E-cores, so its 32 threads are not 32 equal workers) and clamps that by free memory, because a diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md index 202db560b..a3bdfb548 100644 --- a/docs/05-dependencies.md +++ b/docs/05-dependencies.md @@ -551,14 +551,22 @@ each of them may have its own module of one name. are built separately. - Two packages that one program links may not provide the same name. The build is refused, and the message names the package whose closure holds both. -- When two packages of one build provide a name, each BMI lies below its - package's directory in the build directory, and every compile that may import - the name is told which one it means: through a module map with GCC, through - `-fmodule-file=` with Clang, and through `/reference` with MSVC. When every - name has one provider, the build directory and every command are as they - were before. -- A package that provides such a name is compiled in the project, not served - from the global dependency cache. +- Every package's BMIs lie below the package's directory in the build + directory, `gcm.cache//` (`pcm.cache` with Clang), except the root + package's, which lie at their names, as object files do (2026.10.1.2+). A + compile that imports a module of another package is told where its BMI is, + and where the BMIs of the modules that one imports are, through one module + map: a mapper file with GCC (`-fmodule-mapper=`), and an argument file of + `-fmodule-file=` lines with Clang or `/reference` lines with MSVC + (`@/modmap/-.modmap`). The map lists what the + unit reaches through its imports and nothing else, so the command does not + change with the rest of the graph. A project whose modules are all its own + is laid out, and every command spelled, as before. +- Until 2026.10.1.2 a BMI moved below its package's directory only when two + packages of the graph provided its name, and such a package was not served + from the global dependency cache. Both depended on which packages a command + selected, so a workspace member was compiled again at each switch between + `-p` and `--workspace` (mcpp#751). - clangd finds a module by its name in the compilation database, so for a name two packages provide it may show the other program's module. The build is not affected. diff --git a/docs/07-workspace.md b/docs/07-workspace.md index 1626bb292..1dc8df773 100644 --- a/docs/07-workspace.md +++ b/docs/07-workspace.md @@ -486,9 +486,17 @@ member that several members use is compiled once. - **Selection.** `--workspace`, and a virtual root without `-p`, select every member. `-p X`, and a command run in X's directory, plan X and what X reaches; `-p X -p Y` plans both together, as one selection (§5.3). The - selections share the build directory, so `mcpp build --workspace` + selections share the build directory, and a unit is compiled by the same + command in every selection that holds it: the command depends on the unit's + package, the packages that package reaches, the features the selection + activates for it and the declarations a selected member holds as the root, + and on nothing else in the graph (2026.10.1.2+). So `mcpp build --workspace` followed by `mcpp build -p X` compiles nothing, and a package is compiled - again only when its active features differ between the two commands. + again only when its active features, or the root declarations of the + selected members, differ between the two commands. Before 2026.10.1.2, + three facts about the whole graph also reached other members' commands: a + module name that two members provide, a file that two members list from + outside their own directories, and a member that builds a shared library. - **Flags.** A member's `cflags`, `cxxflags`, `ldflags` and defines apply to that member's commands. Editing them recompiles that member and what imports it; the build directory stays the same. @@ -570,6 +578,8 @@ myproject/ └── target/// ├── build.ninja, compile_commands.json # one graph and one database per configuration ├── obj// # intermediate objects of every package + ├── gcm.cache// # each package's BMIs (pcm.cache with clang) + ├── modmap/ # the module maps of the units that import them └── bin/ ├── server/ # a member's products: bin// │ ├── server diff --git a/docs/10-pack-and-release.md b/docs/10-pack-and-release.md index 18d7c0cd0..ebb9053fe 100644 --- a/docs/10-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -170,6 +170,16 @@ relative `-o` keeps meaning the directory the command was typed in. take, with the same precedence: `--profile` wins over either, on all three commands. +A pack states the build it performs as `mcpp build` states one (mcpp +2026.10.1.2+): a line for each package that does work, the status row while it +runs, and `Finished` before the first `Packing` line. A pack over several +configurations builds every configuration first and writes one `Finished`. The +second pass of a dispatched `--format` is stated in the same way, and its +`Distributing` and `Packed` lines follow it. The build runs `[build] jobs` +compiles at once, as every command that compiles does. Before 2026.10.1.2 the +build of a pack wrote none of this, and a pack that recompiled for minutes +showed only `Planning`. + ### Packing several members (mcpp 2026.10.1.1+) `-p` may be repeated, and `--workspace` and `--exclude` select members as they do diff --git a/docs/91-toolchain-internals.md b/docs/91-toolchain-internals.md index d1037b996..b00dd0ace 100644 --- a/docs/91-toolchain-internals.md +++ b/docs/91-toolchain-internals.md @@ -645,7 +645,7 @@ reads this flag**. | axis | the question | examples | the form of the query | |---|---|---|---| -| **target format** | what kind of image is produced | `-fPIC` (PE code is position independent by design; clang refuses the flag outright) | `triple::parse(...)->is_pe()`, host fallback | +| **target format** | what kind of image is produced | `-fPIC`: on every unit of an ELF target that is not freestanding, whether or not the graph links a shared library (2026.10.1.2+), and never on PE, whose code is position independent by design and where clang refuses the flag outright | `triple::parse(...)->object_format()`, `is_pe()`, host fallback | | **target ABI** | which linker will consume this | `--out-implib` vs `/IMPLIB:`, `/DEF:`, the SONAME / install-name form | `is_msvc_target(tc)`, `triple->is_msvc_env()` | | **dialect** | which program mcpp is invoking | `-L` vs `/LIBPATH:`, `-I` vs `/I`, the archive command | `dialect_for(tc)`, `LinkStyle::SeparateLinker` | diff --git a/docs/specs/build-database.md b/docs/specs/build-database.md index dc8568fe6..ee7ef0878 100644 --- a/docs/specs/build-database.md +++ b/docs/specs/build-database.md @@ -126,13 +126,17 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3 真的编译就能得到(§3.4)。`ide.toolchains..build-id` 给出编译器的构建标识, 取自 mcpp 已经算出的驱动身份(工具链指纹的同一个字段),同一工具链的两次运行 之间保持稳定。**已实现** -- **R3.8a** 一个模块名在一个程序之内标识一个模块,而一个配置可以包含多个程序。同一 - 配置中两个包提供同一个模块名时(两者不在同一个包的闭包中,#732),两个单元的 - `provides` 都列出这个名字;可能导入它的每个单元,其 `arguments` 带有构建所用的 - 绑定:GCC 为 `-fmodule-mapper=<映射文件>`(相对 `work-directory`),Clang 为 - `-fmodule-file=<名字>=<路径>`,MSVC 为 `/reference <名字>=<路径>`。只按名字在文档中 - 查找提供方的消费方,因此可能取到另一个程序的模块;构建本身不受影响。每个名字只有 - 一个提供方时,文档与此前逐字相同。**已实现** +- **R3.8a** 一个模块名在一个程序之内标识一个模块,而一个配置可以包含多个程序 + (#732)。除根包之外,每个包的 BMI 都位于其所属包的子目录下(2026.10.1.2+,#751)。 + 导入其他包模块的单元,以及提供这样一个模块的单元,其 `arguments` 带有构建所用的 + 一份模块映射:GCC 为 `-fmodule-mapper=<映射文件>`(相对 `work-directory`),Clang + 与 MSVC 为一个参数文件 `@<绝对路径>/modmap/<包>-<哈希>.modmap`,其中每行一个 + `-fmodule-file=<名字>=<路径>` 或 `/reference <名字>=<路径>`。映射只列出该单元经其 + 导入所能到达的模块,因此同一单元在一个配置的每一种成员选择下,`arguments` 逐字相同。 + `.modmap` 后缀与 CMake 模块映射相同,读者可据此把未展开的参数文件识别为模块机制。 + 同一个名字由两个包提供时,两个单元的 `provides` 都列出它;只按名字在文档中查找提供方 + 的消费方,因此可能取到另一个程序的模块,构建本身不受影响。模块全部属于根包的工程, + 文档与此前逐字相同。**已实现** - **R3.9** `ide.role` 取自扫描器读到的模块声明形式: | 声明 | `ide.role` | diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md index 17b7c2027..d3b4048d1 100644 --- a/docs/zh/04-mcpp-toml.md +++ b/docs/zh/04-mcpp-toml.md @@ -778,7 +778,9 @@ jobs = "auto" # or a positive number; --jobs / MCPP_JOBS override it bmi_schedule = "off" # auto (default, = off) | on | off ``` -`jobs` 是同时运行多少个编译。`"auto"` 是**相对正在执行构建的这台机器** +`jobs` 是同时运行多少个编译,适用于每一条会编译的命令:`build`、`run`、`test` 与 `pack` +(2026.10.1.2+;此前 `test` 与 `pack` 无论这个键怎么写都按 ninja 的默认并行数运行)。 +`"auto"` 是**相对正在执行构建的这台机器** 解析的,绝不会被冻结进 manifest:它取一颗异构 CPU 的物理核心数(一颗 13900K 是 8 个 P-core + 16 个 E-core,所以它的 32 个线程不是 32 个 等价的工作者),并按空闲内存夹紧这个数字,因为单次模块接口编译峰值 diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md index 73510a027..0954003b9 100644 --- a/docs/zh/05-dependencies.md +++ b/docs/zh/05-dependencies.md @@ -495,11 +495,16 @@ updater = { path = "../updater", artifacts = ["updater"] } - import 在导入方所在包的闭包内解析:该包,以及它经依赖到达的每个包。闭包不沿 `artifacts`、`tools` 与 `[build-dependencies]` 边延伸,这些边对应的程序另行构建。 - 被同一个程序链接的两个包不得提供同一个名字。此时构建被拒绝,消息点名其闭包同时包含两者的包。 -- 同一次构建中有两个包提供同一个名字时,各自的 BMI 位于构建目录中其所属包的子目录下, - 每一个可能导入该名字的编译都会被告知它指哪一个:GCC 经模块映射文件,Clang 经 - `-fmodule-file=`,MSVC 经 `/reference`。每个名字只有一个提供方时,构建目录与每条命令都与 - 以前相同。 -- 提供这种名字的包在项目内编译,不从全局依赖缓存取用。 +- 除根包之外,每个包的 BMI 都位于构建目录中该包的子目录下,即 `gcm.cache//` + (Clang 为 `pcm.cache`);根包的 BMI 位于其名字处,与目标文件的规则相同(2026.10.1.2+)。 + 导入其他包模块的编译,经一份模块映射得知该 BMI 以及它所导入模块的 BMI 位于何处:GCC 用映射 + 文件(`-fmodule-mapper=`),Clang 用由 `-fmodule-file=` 行组成的参数文件,MSVC 用由 + `/reference` 行组成的参数文件(`@<构建目录>/modmap/-.modmap`)。映射只列出该 + 单元经其导入所能到达的模块,不含其他内容,因此命令不随图的其余部分变化。模块全部属于自身的 + 项目,其构建目录布局与每条命令都与以前相同。 +- 2026.10.1.2 之前,只有当图中两个包提供同一个名字时 BMI 才移到所属包的子目录下,且这样的包 + 不从全局依赖缓存取用。两者都取决于命令选中了哪些包,因此在 `-p` 与 `--workspace` 之间每切换 + 一次,工作区成员就被重新编译一次(mcpp#751)。 - clangd 在编译数据库中按名字查找模块,因此对两个包提供的同一个名字,编辑器可能显示另一个 程序的模块。构建不受影响。 diff --git a/docs/zh/07-workspace.md b/docs/zh/07-workspace.md index 172e9c035..c15eb004c 100644 --- a/docs/zh/07-workspace.md +++ b/docs/zh/07-workspace.md @@ -441,9 +441,13 @@ mcpp test --workspace --workspace-timeout 1800 # whole fan-out (default 0 = no `[indices]` 路径)按该成员的目录解析。 - **选择。** `--workspace`,以及虚拟工作空间根下不带 `-p` 的命令,选中全体成员;`-p X` 与在 X 的目录中执行的命令规划 X 及其所依赖的一切;`-p X -p Y` 把两者放在一起规划,作为一个 - 选择(§5.3)。这些选择共用构建目录,因此先执行 - `mcpp build --workspace` 再执行 `mcpp build -p X` 不编译任何内容;只有当某个包在两条命令中 - 启用的 feature 不同时,它才会被重新编译。 + 选择(§5.3)。这些选择共用构建目录,同一个编译单元在包含它的每个选择中都由同一条命令编译: + 命令取决于单元所属的包、该包所到达的包、选择为它启用的 feature,以及被选成员作为根所持有的 + 声明,而不取决于图中的其他任何内容(2026.10.1.2+)。因此先执行 `mcpp build --workspace` + 再执行 `mcpp build -p X` 不编译任何内容;只有当某个包启用的 feature,或被选成员的根声明在 + 两条命令中不同时,它才会被重新编译。2026.10.1.2 之前,另有三个关于整张图的事实也会进入 + 其他成员的命令:两个成员提供同一个模块名、两个成员从各自目录之外列出同一个文件、某个成员 + 构建共享库。 - **编译参数。** 成员的 `cflags`、`cxxflags`、`ldflags` 与 defines 作用于该成员自己的命令。 修改它们会重新编译该成员以及导入它的单元,构建目录保持不变。 - **Feature。** `--features f` 在每个声明了 `f` 的被选成员中启用它;没有被选成员声明它时, @@ -506,6 +510,8 @@ myproject/ └── target/// ├── build.ninja, compile_commands.json # 每个配置一张图、一份数据库 ├── obj// # 各包的中间产物 + ├── gcm.cache// # 各包的 BMI(clang 为 pcm.cache) + ├── modmap/ # 导入它们的编译单元所读的模块映射 └── bin/ ├── server/ # 成员的产物:bin/<包名>/ │ ├── server diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md index 0c9294b43..488ab49a0 100644 --- a/docs/zh/10-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -156,6 +156,12 @@ feature:每一条 `--target` 腿,以及被分派格式的两遍构建。它 `--release` 与 `--dev`(mcpp 2026.9.16.1+)是 `build`、`run` 已接受的简写, 优先级相同:三条命令上都是 `--profile` 优先于它们。 +打包对它所执行的构建的陈述方式与 `mcpp build` 相同(mcpp 2026.10.1.2+):每个有工作的包 +一行,运行期间显示状态行,并在第一行 `Packing` 之前写出 `Finished`。跨多个配置的打包先构建 +全部配置,只写一次 `Finished`。由包提供的 `--format` 的第二遍以同样方式陈述,其后是 +`Distributing` 与 `Packed` 行。构建同时运行 `[build] jobs` 个编译,与每一条会编译的命令相同。 +2026.10.1.2 之前,打包中的构建不写出上述任何内容,重新编译数分钟的打包只显示 `Planning`。 + ### 打包多个成员(mcpp 2026.10.1.1+) `-p` 可以重复,`--workspace` 与 `--exclude` 选择成员的方式与 `mcpp build` 相同 diff --git a/docs/zh/91-toolchain-internals.md b/docs/zh/91-toolchain-internals.md index 1d9c37764..5657c5ef5 100644 --- a/docs/zh/91-toolchain-internals.md +++ b/docs/zh/91-toolchain-internals.md @@ -601,7 +601,7 @@ SDK 解析它的 C 世界(`CLibMode::Sysroot`),并有自己的 libc++ 链 | 轴 | 要问的问题 | 例子 | 查询方式 | |---|---|---|---| -| **目标格式** | 产出的是哪一种镜像 | `-fPIC`(PE 代码按设计就是位置无关的;clang 直接拒绝这个 flag) | `triple::parse(...)->is_pe()`,宿主兜底 | +| **目标格式** | 产出的是哪一种镜像 | `-fPIC`:ELF 且非独立环境(freestanding)的目标上每个编译单元都带,无论图中是否链接共享库(2026.10.1.2+);PE 上从不带,PE 代码按设计就是位置无关的,clang 直接拒绝这个 flag | `triple::parse(...)->object_format()`、`is_pe()`,宿主兜底 | | **目标 ABI** | 哪个链接器会消费它 | `--out-implib` 与 `/IMPLIB:`、`/DEF:`,SONAME / install-name 的形式 | `is_msvc_target(tc)`、`triple->is_msvc_env()` | | **dialect** | mcpp 调用的是哪个程序 | `-L` 与 `/LIBPATH:`、`-I` 与 `/I`,归档命令 | `dialect_for(tc)`、`LinkStyle::SeparateLinker` | diff --git a/mcpp.toml b/mcpp.toml index 9422646b9..b4e40fc06 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.10.1.1" +version = "2026.10.1.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 6ea6470ae..56694e638 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.10.1.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.10.1.2"; } // namespace mcpp diff --git a/src/bmi_cache.cppm b/src/bmi_cache.cppm index fb4001f88..6b48dd778 100644 --- a/src/bmi_cache.cppm +++ b/src/bmi_cache.cppm @@ -128,6 +128,12 @@ struct ObjArtifact { // objects above. struct DepArtifacts { std::vector bmiFiles; + // Where an entry of `bmiFiles` lies in the build's BMI directory when not + // at its name: below its package's directory, which is every package but + // the root's (pack drive and selection design 2026-10-01, B1). Read when + // the entry is populated, as `ObjArtifact::buildRel` is, and never written + // to entry.json, whose BMIs are named by module. + std::map> bmiBuildRel; std::vector objFiles; }; @@ -369,7 +375,8 @@ populate_from(const CacheKey& key, auto projectBmi = projectTargetDir / key.bmiDirName; for (auto& g : arts.bmiFiles) { - auto from = projectBmi / g; + auto rel = arts.bmiBuildRel.find(g); + auto from = projectBmi / (rel == arts.bmiBuildRel.end() ? g : rel->second); if (!std::filesystem::exists(from)) { return std::unexpected(std::format( "expected build output missing: {}", from.string())); diff --git a/src/build/backend.cppm b/src/build/backend.cppm index ad56358a9..be859fbfa 100644 --- a/src/build/backend.cppm +++ b/src/build/backend.cppm @@ -14,7 +14,6 @@ struct BuildOptions { bool verbose = false; bool dryRun = false; bool requireCompileDatabase = false; - std::size_t parallelJobs = 0; // Explicit ninja goal targets (LinkUnit::output paths, relative to the // plan's outputDir). Empty = build the full plan (default behavior). std::vector ninjaTargets; @@ -35,9 +34,23 @@ struct BuildOptions { // The report of this build directory (build progress design 2026-09-29). // Set, ninja runs without `--quiet` and is read as it runs: the step // record is written, the status lines and ninja's log feed the report, - // and a failed step's diagnostics are written when it fails. Null (under - // --quiet or machine output), ninja's output is examined after it exits. + // and a failed step's diagnostics are written when it fails. Null, the + // backend applies `report`. mcpp::build::progress::Build* progress = nullptr; + // Who states this drive when `progress` is null (pack drive and selection + // design 2026-10-01, A2). `Region`: the backend reports it whenever the + // command opened the progress region and is not quiet, which is how + // `mcpp pack` came to build for six minutes under `Planning` (#753): the + // default is the reported form, so a caller that says nothing is + // reported. `Caller`: the caller writes its own lines, as `mcpp test` + // does for each test's build. + // + // The job count is not an option. Every drive takes it from the plan + // (`BuildPlan::scheduleNinjaJobs`), where `[build] jobs`, `--jobs` and + // `MCPP_JOBS` were resolved once; it was an option set by one of ten + // callers, and `test` and `pack` ran ninja's default instead (A1). + enum class Report { Region, Caller }; + Report report = Report::Region; }; struct BuildResult { diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 808003efa..6f57e40d1 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -957,36 +957,12 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache, // wrong, where the bare word was printed for three months over full // recompiles. mcpp::build::progress::programs_done(); - mcpp::build::progress::Build report(ctx.outputDir); - - // RECLAIM THE STALE CONCURRENCY TOKENS, HERE AND NOT IN prepare. - // - // detach-codegen bounds real compiler concurrency with a semaphore of - // directories under `/.mcpp-sched`, released by the supervisor - // holding each token. A supervisor that never runs its cleanup — Ctrl-C on - // the build, the OOM killer, a reboot — leaves its directory behind, and - // nothing else deletes one. Every such event permanently lowers the cap for - // that build directory; after `cap` of them the next build waits for a token - // that can never be released and hangs with no output at all. - // - // This was first placed in prepare, beside the schedule decision, and an - // e2e that plants a full set of stale tokens showed it never running: an - // incremental build takes the project-level fast path, which replays - // build.ninja without re-deriving the plan. The reclaim has to sit on the - // path EVERY build takes, which is the line below this one. - // - // Safe here because ninja has not been spawned yet, so no token in the - // directory can have a live owner. - if (ctx.plan.scheduleTag == "detach-codegen") { - std::error_code semEc; - std::filesystem::remove_all( - std::filesystem::path(ctx.plan.outputDir) / ".mcpp-sched", semEc); - } + // The job count, the stale-token reclaim and the report are the backend's + // (pack drive and selection design 2026-10-01, A1, A2): every command's + // drive takes them from the plan and the open report. mcpp::build::BuildOptions opts; opts.verbose = verbose; - opts.parallelJobs = static_cast(ctx.plan.scheduleNinjaJobs); - if (!mcpp::ui::is_quiet()) opts.progress = &report; auto r = be->build(ctx.plan, opts); if (!r) { // A failed step was reported when it failed; what follows is the @@ -996,19 +972,8 @@ export int run_build_plan(BuildContext& ctx, bool verbose, bool no_cache, return 1; } - // Populate the global cache for deps that did NOT hit. prepare_build leaves - // depsToPopulate empty under --cache=local|off, so the mode gate is already - // enforced there; asserting it again here keeps the write side legible on - // its own terms rather than as a consequence of something in prepare. - if (ctx.cacheMode != CacheMode::Global) ctx.depsToPopulate.clear(); - for (auto& task : ctx.depsToPopulate) { - auto pr = mcpp::bmi_cache::populate_from(task.key, ctx.outputDir, task.artifacts); - if (!pr) { - mcpp::ui::warning(std::format( - "bmi cache populate failed for {}@{}: {}", - task.key.packageName, task.key.version, pr.error())); - } - } + // Populate the global cache for deps that did NOT hit. + mcpp::build::populate_dependency_cache(ctx); // P1.5: warn if fingerprint changed from last build (explains full rebuild). // Compared against the entry for the SAME profile: the profile is now a @@ -2892,14 +2857,7 @@ static std::optional test_phase_a(TestBuild& tb, const TestOptions& // M3.2: populate BMI cache for deps that did NOT hit cache — deps // are package-level artifacts, so this belongs right after Phase A. - for (auto& task : ctx->depsToPopulate) { - auto pr = mcpp::bmi_cache::populate_from(task.key, ctx->outputDir, task.artifacts); - if (!pr) { - mcpp::ui::warning(std::format( - "bmi cache populate failed for {}@{}: {}", - task.key.packageName, task.key.version, pr.error())); - } - } + mcpp::build::populate_dependency_cache(*ctx); // No "Finished test" line here: Phase A only built the shared // prerequisites. Printing a success banner right before per-test @@ -2930,6 +2888,7 @@ static void test_bulk(TestBuild& tb, const TestOptions& testOpts, Keep&& keep) { auto* ctx = &*tb.ctx; auto& backend = tb.backend; mcpp::build::BuildOptions bulk; + bulk.report = mcpp::build::BuildOptions::Report::Caller; // a test's own lines bulk.keepGoing = true; bulk.buildTimeoutSecs = static_cast(testOpts.buildTimeoutSecs); for (auto& lu : ctx->plan.linkUnits) @@ -3278,6 +3237,7 @@ static int test_run_member(TestBuild& tb, const TestOptions& testOpts, std::expected b{}; if (!bulkBuiltEverything) { mcpp::build::BuildOptions bOpts; + bOpts.report = mcpp::build::BuildOptions::Report::Caller; // a test's own lines bOpts.ninjaTargets = {lu.output.generic_string()}; bOpts.buildTimeoutSecs = static_cast(testOpts.buildTimeoutSecs); auto tBuild = std::chrono::steady_clock::now(); @@ -3798,6 +3758,7 @@ export void run_workspace_tests(std::span passthrough, const auto goals = member_package_goals(*tb.ctx, slots[i].owner); if (goals.empty()) continue; mcpp::build::BuildOptions own; + own.report = mcpp::build::BuildOptions::Report::Caller; // a test's own lines own.ninjaTargets = goals; own.buildTimeoutSecs = static_cast(testOpts.buildTimeoutSecs); const auto t0 = std::chrono::steady_clock::now(); diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index c12e46cd5..baac2e4c2 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -63,6 +63,20 @@ public: // Factory for this backend implementation. std::unique_ptr make_ninja_backend(); +// What a command writes when a drive failed. Under a report the failed step's +// package line and diagnostics were written as it failed, and `message` +// would repeat them; what remains is the advice that reads the whole output. +// Without one, both are written now. One function for `build`, `test` and +// `pack`, whose drives are reported alike (pack drive and selection design +// 2026-10-01, A2). +void report_failed_drive(const BuildError& error); + +// Writes the module maps the plan's units name (pack drive and selection +// design 2026-10-01, B1) into the plan's build directory: what the backend +// does before a drive, and what `mcpp emit build-database` does for the +// database it prints, whose argument lists name these files. +void write_module_maps(const BuildPlan& plan); + // The ninja mcpp runs for a toolchain: the sandbox-local ninja beside the // toolchain when there is one, else `ninja` from PATH. One answer for the // engine's own builds and for the build information (#734 E2). @@ -278,6 +292,39 @@ namespace { // U+FEFF in UTF-8. The response files of the MSVC tools begin with it (#693). constexpr std::string_view kUtf8ByteOrderMark = "\xEF\xBB\xBF"; +// Whether the plan's compiler names an imported module's BMI as MSVC does +// (`/reference =`), and so reads an argument file by MSVC's rules. +bool msvc_module_spelling(const BuildPlan& plan) { + const auto prefix = mcpp::toolchain::bmi_traits(plan.toolchain).moduleFileUsePrefix; + return !prefix.empty() && prefix.back() == ' '; +} + +// The text of a module map's argument file (B1): one argument per line. clang +// reads it by GNU rules on every host (#247), where a word holding a space or +// a quote is written in double quotes with `\` and `"` escaped; the paths are +// written with forward slashes, so a backslash does not occur. MSVC reads it +// by its own rules (`shell_quote_arg` on a Windows host) and needs the byte +// order mark to read a path that is not ASCII (#693). +std::string module_map_arguments(const std::vector& arguments, bool msvc) { + std::string out = msvc ? std::string(kUtf8ByteOrderMark) : std::string{}; + for (auto const& a : arguments) { + if (msvc) { + out += mcpp::build::shell_quote_arg(a); + } else if (a.find_first_of(" \t\"'\\") == std::string::npos) { + out += a; + } else { + out += '"'; + for (char c : a) { + if (c == '"' || c == '\\') out += '\\'; + out += c; + } + out += '"'; + } + out += '\n'; + } + return out; +} + std::string escape_ninja_path(const std::filesystem::path& p) { // Ninja escapes: $ → $$, : → $:, space → $ (with leading space). // For simplicity we wrap in case-by-case. @@ -1492,9 +1539,9 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // or a plain .cpp still compiles in one edge whose output is the object, // and a `--target-bmi` there would name an edge nobody declared. // - // `$module_map` (mcpp#732) names the package's module map when two packages - // of the plan provide one module name; the rule carries it only then, so - // a plan without such a name writes the file it always wrote. + // `$module_map` names the unit's module map when it reaches a BMI below its + // provider's directory (B1); the rule carries it only when some unit does, + // so a plan of the root's modules alone writes the file it always wrote. append(std::format( "rule cxx_dyndep\n" " command = $mcpp dyndep --single --bmi-dir {} --bmi-ext {} $bind $expect{} --output $out $in\n" @@ -2246,11 +2293,13 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, // exactly one symbol (`_ZGIW3std`, measured) and an importing TU references // it, so a missed unit is an undefined symbol at link time rather than a // silent miscompile. - // The module map of each package whose closure holds a name two packages - // provide (mcpp#732): module name -> BMI path, as the plan resolved it. + // The module map of each unit that reaches a BMI below its provider's + // directory (pack drive and selection design 2026-10-01, B1), by + // `CompileUnit::moduleScope`: module name -> BMI path, as the plan resolved + // it. std::map>, std::less<>> scopeBmis; - for (auto const& [pkg, scope] : plan.moduleScopes) { - auto& m = scopeBmis[pkg]; + for (auto const& [key, scope] : plan.moduleScopes) { + auto& m = scopeBmis[key]; std::istringstream lines(scope.content); for (std::string name, path; lines >> name >> path;) m[name] = path; } @@ -2258,13 +2307,13 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, for (auto& cu : plan.compileUnits) if (!cu.providesModule.empty()) byModule[cu.providesModule].push_back(&cu); // The unit `importer`'s import of `name` means: the one provider, or the - // one its package's module map names. + // one its module map names. auto provider_of = [&](const CompileUnit& importer, const std::string& name) -> const CompileUnit* { auto it = byModule.find(name); if (it == byModule.end()) return nullptr; if (it->second.size() == 1) return it->second.front(); - auto sc = scopeBmis.find(importer.packageName); + auto sc = scopeBmis.find(importer.moduleScope); if (sc == scopeBmis.end()) return nullptr; auto m = sc->second.find(name); if (m == sc->second.end()) return nullptr; @@ -2364,15 +2413,15 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, return s; }; // The BMI a unit provides, where the plan placed it: below its package's - // directory when two packages provide its module name (mcpp#732). + // directory unless the root package provides it (B1). auto unit_bmi = [&](const mcpp::build::CompileUnit& cu) { return cu.bmiFile.empty() ? bmi_path(cu.providesModule) : std::string(traits.bmiDir) + "/" + cu.bmiFile; }; - // The BMI `cu`'s import of `name` means: its package's module map when it - // has one, and the module's own name otherwise. + // The BMI `cu`'s import of `name` means: its module map when it has one, + // and the module's own name otherwise. auto import_bmi = [&](const mcpp::build::CompileUnit& cu, std::string_view name) { - if (auto sc = scopeBmis.find(cu.packageName); sc != scopeBmis.end()) + if (auto sc = scopeBmis.find(cu.moduleScope); sc != scopeBmis.end()) if (auto it = sc->second.find(name); it != sc->second.end()) return it->second; return bmi_path(name); }; @@ -2615,6 +2664,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, std::vector ddi_paths; ddi_paths.reserve(plan.compileUnits.size()); std::unordered_map ddiOwner; + std::unordered_map ddiScope; // ddi -> its unit's module map for (auto& cu : plan.compileUnits) { attribute(cu.packageName); if (cu.servedFromCache) continue; // staged, never scanned @@ -2623,6 +2673,7 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, auto ddi = (cu.object.parent_path() / cu.source.filename()).string() + ".ddi"; ddi_paths.push_back(ddi); ddiOwner[ddi] = cu.packageName; + if (!cu.moduleScope.empty()) ddiScope[ddi] = cu.moduleScope; append(std::format("build {} : cxx_scan {}{}\n", escape_ninja_path(ddi), escape_ninja_path(cu.source), order_only_for(cu))); // `-o` and `-fdeps-target` are DIFFERENT under the split shape and @@ -2726,7 +2777,8 @@ std::string emit_ninja_string(const BuildPlan& plan, std::string* placements, append(std::format("build {} : cxx_dyndep {}\n", dd, ddi)); if (two_phase_ddi.contains(ddi)) append(" bind = --split-module\n"); - if (auto sc = plan.moduleScopes.find(ddiOwner[ddi]); sc != plan.moduleScopes.end()) + if (auto at = ddiScope.find(ddi); at != ddiScope.end()) + if (auto sc = plan.moduleScopes.find(at->second); sc != plan.moduleScopes.end()) append(std::format(" module_map = --module-map {}\n", escape_ninja_path(sc->second.mapFile))); if (auto it = ddi_expect.find(ddi); it != ddi_expect.end()) @@ -3953,9 +4005,49 @@ std::optional ninja_encoding_mismatch(std::string_view reported, "it), so a build here must be ASCII.", processCodePage)); } +namespace { + +// RECLAIM THE STALE CONCURRENCY TOKENS, ONCE PER BUILD DIRECTORY PER COMMAND. +// +// detach-codegen bounds real compiler concurrency with a semaphore of +// directories under `/.mcpp-sched`, released by the supervisor +// holding each token. A supervisor that never runs its cleanup (Ctrl-C on the +// build, the OOM killer, a reboot) leaves its directory behind, and nothing +// else deletes one. Every such event permanently lowers the cap for that build +// directory; after `cap` of them the next build waits for a token that can +// never be released and hangs with no output at all. +// +// The reclaim was first placed in prepare, where an incremental build that +// replays build.ninja never reaches it, and then on the path of `mcpp build`, +// which `mcpp test` and `mcpp pack` do not take (pack drive and selection +// design 2026-10-01, A1). Here every drive reaches it. It runs before the +// first drive of a directory in this process, when no supervisor of this +// command can own a token; a later drive of the same command may still have a +// detached code generation of an earlier one running, whose token is live. +void reclaim_stale_schedule_tokens(const std::filesystem::path& outputDir) { + static std::mutex m; + static std::set reclaimed; + std::lock_guard lock(m); + if (!reclaimed.insert(outputDir.lexically_normal().generic_string()).second) return; + std::error_code ec; + std::filesystem::remove_all(outputDir / ".mcpp-sched", ec); +} + +} // namespace + std::expected NinjaBackend::build(const BuildPlan& plan, const BuildOptions& opts) { auto t0 = std::chrono::steady_clock::now(); + // THE REPORT OF THIS DRIVE (pack drive and selection design 2026-10-01, + // A2): the caller's, or, when the caller leaves it to the backend, one of + // the backend's own while the command's report is open. + std::optional ownReport; + mcpp::build::progress::Build* report = opts.progress; + if (!report && !opts.dryRun && opts.report == BuildOptions::Report::Region + && mcpp::build::progress::is_open() && !mcpp::ui::is_quiet()) { + ownReport.emplace(plan.outputDir); + report = &*ownReport; + } // Where a drive's wall clock went. `mcpp test` calls this once per test on // an already-built tree, so anything here that is not proportional to the // work done is paid N times — and that is invisible from the outside, @@ -3984,6 +4076,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan return std::unexpected(BuildError{std::format("cannot create output dir '{}': {}", plan.outputDir.string(), ec.message()), plan.outputDir}); + if (!opts.dryRun && plan.scheduleTag == "detach-codegen") + reclaim_stale_schedule_tokens(plan.outputDir); auto ninja_path = plan.outputDir / "build.ninja"; // Written beside build.ninja and not into it: the fast path replays the @@ -4004,7 +4098,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan { auto record = step_record(plan, attribution); mcpp::build::progress::write_record(plan.outputDir, record); - if (opts.progress) opts.progress->set_record(std::move(record)); + if (report) report->set_record(std::move(record)); } if (!placements.empty()) { // Written only when it changes: it is an input of the placement edge. @@ -4020,16 +4114,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan std::ofstream(listPath, std::ios::binary | std::ios::trunc) << placements; } } - // mcpp#732: the module maps the units of a package read when two packages - // provide one module name. The content's hash is in the name, so a file - // that exists is already right; a changed resolution names a new file. - for (auto const& [pkg, scope] : plan.moduleScopes) { - const auto path = plan.outputDir / scope.mapFile; - std::error_code mec; - if (std::filesystem::exists(path, mec)) continue; - std::filesystem::create_directories(path.parent_path(), mec); - std::ofstream(path, std::ios::binary | std::ios::trunc) << scope.content; - } + write_module_maps(plan); // Command-length backstop (see // .agents/docs/2026-08-06-command-length-architecture.md). The structural @@ -4273,7 +4358,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan std::vector nargv{ninjaProgram}; // With a report, ninja's status lines are read as it runs (build progress // design 2026-09-29, §6.1), so they are not suppressed. - if (!opts.verbose && !opts.progress) + if (!opts.verbose && !report) nargv.push_back("--quiet"); nargv.push_back("-C"); nargv.push_back(plan.outputDir.string()); @@ -4296,8 +4381,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan nargv.push_back("-d"); nargv.push_back(topics); } - if (opts.parallelJobs) - nargv.push_back(std::format("-j{}", opts.parallelJobs)); + if (plan.scheduleNinjaJobs > 0) + nargv.push_back(std::format("-j{}", plan.scheduleNinjaJobs)); if (opts.keepGoing) { nargv.push_back("-k"); @@ -4340,10 +4425,10 @@ std::expected NinjaBackend::build(const BuildPlan& plan if (manifest.find("\nbuild " + std::string(kStagedCacheGoal) + " : phony") != std::string::npos) { const auto preDeadline = std::chrono::milliseconds(static_cast(opts.buildTimeoutSecs) * 1000); - if (opts.progress) { + if (report) { std::vector pre{ninjaProgram, "-C", plan.outputDir.string(), std::string(kStagedCacheGoal)}; - (void)run_ninja_reporting(pre, nenv, preDeadline, *opts.progress, opts.verbose, + (void)run_ninja_reporting(pre, nenv, preDeadline, *report, opts.verbose, command_prefixes(flags, plan), mcpp::build::progress::PassKind::Placement); } else { @@ -4382,17 +4467,18 @@ std::expected NinjaBackend::build(const BuildPlan& plan std::chrono::milliseconds(static_cast(opts.buildTimeoutSecs) * 1000); // The main pass's options, and its goal last. std::vector scan{ninjaProgram}; - if (!opts.verbose && !opts.progress) scan.push_back("--quiet"); + if (!opts.verbose && !report) scan.push_back("--quiet"); scan.insert(scan.end(), {std::string("-C"), plan.outputDir.string()}); if (opts.verbose) scan.push_back("-v"); if (const char* topics = std::getenv("MCPP_NINJA_DEBUG"); topics && *topics) { scan.push_back("-d"); scan.push_back(topics); } - if (opts.parallelJobs) scan.push_back(std::format("-j{}", opts.parallelJobs)); + if (plan.scheduleNinjaJobs > 0) + scan.push_back(std::format("-j{}", plan.scheduleNinjaJobs)); scan.push_back(std::string(kScannedGoal)); - if (opts.progress) { - auto run = run_ninja_reporting(scan, nenv, scanDeadline, *opts.progress, opts.verbose, + if (report) { + auto run = run_ninja_reporting(scan, nenv, scanDeadline, *report, opts.verbose, command_prefixes(flags, plan), mcpp::build::progress::PassKind::Scan); if (run.exitCode != 0 || run.timedOut) { @@ -4422,8 +4508,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan std::chrono::milliseconds(static_cast(opts.buildTimeoutSecs) * 1000); if (scanFailed) { // `out`, `ninjaExit`, `buildTimedOut` and `reported` are the scan's. - } else if (opts.progress) { - auto run = run_ninja_reporting(nargv, nenv, deadline, *opts.progress, opts.verbose, + } else if (report) { + auto run = run_ninja_reporting(nargv, nenv, deadline, *report, opts.verbose, command_prefixes(flags, plan)); out = std::move(run.output); ninjaExit = run.exitCode; @@ -4441,8 +4527,8 @@ std::expected NinjaBackend::build(const BuildPlan& plan bool ok = (ninjaExit == 0) && !buildTimedOut; // Every package gets its final line before what the build has to say // after ninja: validations, advice, `Finished`. - if (opts.progress) { - opts.progress->finish(ok); + if (report) { + report->finish(ok); if (ok) mcpp::build::progress::checking(); } @@ -4455,7 +4541,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan std::chrono::steady_clock::now() - t0); // Under a report the failed steps were written as they failed, and // under --verbose every line was: only what is left is attached. - std::string partial = !opts.progress ? out + std::string partial = !report ? out : reported || opts.verbose ? std::string{} : filter_ninja_output(out, command_prefixes(flags, plan)); return std::unexpected(BuildError{ @@ -4583,7 +4669,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan } stage("symbol-provision"); // Under a report the lines were printed as ninja wrote them. - if (opts.verbose && !opts.progress && !out.empty()) + if (opts.verbose && !report && !out.empty()) mcpp::ui::block(out); std::set want(opts.ninjaTargets.begin(), opts.ninjaTargets.end()); for (auto& lu : plan.linkUnits) { @@ -4597,7 +4683,7 @@ std::expected NinjaBackend::build(const BuildPlan& plan auto prefixes = command_prefixes(flags, plan); // What was reported as it happened is not repeated: a failed step's // diagnostics (default output), or every line (--verbose). - auto diagnostics = reported || (opts.progress && opts.verbose) ? std::string{} + auto diagnostics = reported || (report && opts.verbose) ? std::string{} : opts.verbose ? out : filter_ninja_output(out, prefixes); // Appended here as well as on the fast path (execute.cppm): the two // paths report failure through different channels, and advice attached @@ -4640,4 +4726,29 @@ std::unique_ptr make_ninja_backend() { return std::make_unique(); } +// The module maps of the units that reach a BMI below its provider's directory +// (B1). The key in a file's name is a hash of what the file holds, so a file +// that exists is already right, and a changed resolution names a new file. +void write_module_maps(const BuildPlan& plan) { + const bool msvc = msvc_module_spelling(plan); + for (auto const& [key, scope] : plan.moduleScopes) { + std::error_code ec; + const auto path = plan.outputDir / scope.mapFile; + if (!std::filesystem::exists(path, ec)) { + std::filesystem::create_directories(path.parent_path(), ec); + std::ofstream(path, std::ios::binary | std::ios::trunc) << scope.content; + } + if (scope.argsFile.empty()) continue; + const auto args = plan.outputDir / scope.argsFile; + if (std::filesystem::exists(args, ec)) continue; + std::ofstream(args, std::ios::binary | std::ios::trunc) + << module_map_arguments(scope.arguments, msvc); + } +} + +void report_failed_drive(const BuildError& error) { + if (!error.reported) mcpp::ui::error(error.message); + mcpp::ui::block(error.diagnosticOutput); +} + } // namespace mcpp::build diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 410bbc394..1196beec4 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -57,12 +57,17 @@ struct CompileUnit { // struct from whichever unit copies a CompileUnit first. std::string providesModule; // The unit's BMI, relative to the toolchain's BMI directory: the module's - // name (`common.gcm`) when one package of the plan provides it, and below - // the provider's directory (`gpp.updater/common.gcm`) when two do - // (mcpp#732). Empty on a unit that provides nothing, and on a unit built - // by hand, which then takes the name. + // name (`common.gcm`) when the root package provides it, and below the + // provider's directory (`gpp.updater/common.gcm`) for every other package + // (pack drive and selection design 2026-10-01, B1). Empty on a unit that + // provides nothing, and on a unit built by hand, which then takes the name. std::string bmiFile; std::vector imports; // logical names imported + // The key of this unit's module map in `BuildPlan::moduleScopes`: the BMI + // of every module the unit imports, directly or through another module, + // and of the module it provides. Empty when every such BMI lies at its + // name in the BMI directory, which is where the compilers look unaided. + std::string moduleScope; // Unit came from a scan_overrides declaration — plan-vs-ddi // verification is mandatory for it (ninja_backend emits --expect-*). bool scanOverridden = false; @@ -316,18 +321,37 @@ struct PlanPackage { std::string source; }; -// mcpp#732: a package whose closure holds a module name that two packages of -// the plan provide. Its units are told which BMI each name means: through -// `mapFile` for GCC, whose mapper does not fall back for a name it does not -// list, and by one flag per such name for clang and MSVC. The dyndep step -// reads `mapFile` on every compiler. +// The module map of a set of compile units (pack drive and selection design +// 2026-10-01, B1): which BMI each module name means to them. A unit is given +// one when a module it imports, or the module it provides, lies below its +// provider's directory, which is every module of a package other than the +// root. GCC reads `mapFile` through `-fmodule-mapper=`, because its mapper +// does not fall back for a name it does not list; clang and MSVC read +// `argsFile` as an argument file of `-fmodule-file=` or `/reference` lines. +// The dyndep step reads `mapFile` on every compiler. +// +// The map lists what the UNIT reaches through its imports, and nothing that +// the rest of the graph holds. A map derived from the graph, or from the +// package's whole closure, would name other members' modules under +// `--workspace` and not under `-p`, and the dev-dependencies' modules under +// `mcpp test` and not under `mcpp build`, and a unit compiled in one build +// directory by both would be compiled again at every switch (#751). struct ModuleScope { // Relative to the build directory, with a hash of `content` in its name, // so a changed resolution changes the flag that names it. std::filesystem::path mapFile; // ` ` lines, one - // per name the package's units may import, sorted by name. + // per name, sorted by name. std::string content; + // clang and MSVC: relative to the build directory, named after `mapFile` + // with the `.modmap` suffix that CMake's module maps carry and that + // compilation-database readers recognise as module mechanics. Empty for + // GCC. + std::filesystem::path argsFile; + // The arguments `argsFile` holds, unquoted, each BMI path absolute. The + // backend writes them one per line in the host's response-file syntax, + // with the byte order mark an MSVC tool needs to read a non-ASCII path. + std::vector arguments; }; struct BuildPlan { @@ -437,9 +461,9 @@ struct BuildPlan { std::filesystem::path stdBmiPath; // absolute path to prebuilt std.gcm std::filesystem::path stdObjectPath; // absolute path to prebuilt std.o std::filesystem::path stdCompatBmiPath; // absolute path to prebuilt std.compat.pcm - // mcpp#732: by qualified package name. Empty when every module name of the - // plan has one provider, and then the build directory is laid out, and - // every command spelled, exactly as before. + // By `CompileUnit::moduleScope`. Empty when every module of the plan is + // the root package's, and then the BMI directory is flat and no command + // names a map. std::map> moduleScopes; std::filesystem::path stdCompatObjectPath; // absolute path to prebuilt std.compat.o std::filesystem::path scanDepsPath; // clang-scan-deps binary (Clang only) @@ -1825,8 +1849,24 @@ make_plan(const mcpp::manifest::Manifest& manifest, std::set scannedSources; std::map rootBasenameCount; std::vector unitOwner(graph.units.size(), 0); + // A WORKSPACE PLAN'S ROOT OWNS NO SOURCE (pack drive and selection design + // 2026-10-01, B1). Its directory is the workspace's, so a file no member + // contains (`3rdParty/boost.ixx`, listed by a member as `../3rdParty/...`) + // is inside it, and owning it would put the file in the root's basename + // census: flat under `-p` of one member that lists it, below a package + // directory under `--workspace` once a second member lists it too, and a + // recompile at every switch (#751, GalTranslPP 3.1.3). Such a unit belongs + // to the member that declares it, whose object addresses do not depend on + // the rest of the graph. + std::map> packageIndex; + if (manifest.package.virtualRoot) + for (std::size_t p = 1; p < packages.size(); ++p) + packageIndex.emplace(qualified_package_name(packages[p].manifest), p); for (auto idx : topoOrder) { unitOwner[idx] = owner_of(graph.units[idx].path); + if (unitOwner[idx] == 0 && manifest.package.virtualRoot) + if (auto it = packageIndex.find(graph.units[idx].packageName); it != packageIndex.end()) + unitOwner[idx] = it->second; scannedSources.insert(graph.units[idx].path); if (unitOwner[idx] == 0) rootBasenameCount[object_filename_for(graph.units[idx].path, objExt)]++; @@ -3193,29 +3233,49 @@ make_plan(const mcpp::manifest::Manifest& manifest, } } - // The single derivation. Deliberately at the END of make_plan, after every - // producer of a link unit has run: a dependency resolved to the shared - // form arrives as an ordinary SharedLibrary unit, so this one predicate - // covers the package's own shared targets and its dependencies' alike. - for (auto const& lu : plan.linkUnits) { - if (lu.kind == LinkUnit::SharedLibrary) { plan.needsPic = true; break; } + // POSITION INDEPENDENCE IS A PROPERTY OF THE TARGET (pack drive and + // selection design 2026-10-01, B2). It was decided by a census of the + // graph: any shared library among the link units put `-fPIC` on every + // unit. A workspace's `--workspace` plan holds the member that builds a + // shared library and a `-p` plan of another member does not, while both + // compile into one build directory, so each switch between them recompiled + // every member (measured on 2026.10.1.1). The answer must not depend on + // the selection, and an object compiled once must serve a program and a + // shared object alike, as rustc's default relocation model does on these + // targets: every unit of a target whose objects are ELF and that has a + // loader is compiled position-independent. Mach-O compilers produce PIC + // by default, PE has no such flag, and WebAssembly and freestanding + // targets have no shared objects of this kind. + { + // An empty target is the host's, which `Triple{}` does not describe. + auto t = mcpp::toolchain::triple::parse(tc.targetTriple); + const auto target = t ? *t : mcpp::toolchain::triple::host_triple(); + plan.needsPic = target.object_format() == mcpp::toolchain::triple::ObjectFormat::Elf + && !target.is_freestanding(); } - // MODULE NAMES ARE RESOLVED IN THE IMPORTER'S CLOSURE (mcpp#732). A module - // name identifies one module within one program, and a plan holds several - // programs (a package and the programs it ships through `artifacts`, a - // workspace's members), so two packages of one plan may each provide - // `common` when no closure holds both (the scanner refuses the rest). - // Every compiler finds a BMI by name in one directory, so the two BMIs go - // below their providers' directories, and the units that may import such a - // name are told which one it means. With every name provided once, none of - // this applies: no unit moves, no flag is added. At the end of make_plan, + // EVERY PACKAGE'S BMIS LIE BELOW ITS OWN DIRECTORY, EXCEPT THE ROOT'S (pack + // drive and selection design 2026-10-01, B1), as object files have since + // mcpp#233. A module name identifies one module within one program + // (mcpp#732), and a build directory serves every selection of a workspace's + // configuration, so whether two packages provide one name is a fact about + // the configuration that no single plan observes: a `-p` plan holds the + // selected closure. Decided per graph, as 2026.9.30.2 did, the placement and + // every importer's flags differed between `-p` and `--workspace`, and each + // switch recompiled the shared members (#751). Decided by the provider + // alone, they are the same in every plan that holds the unit. The root's + // modules stay at their names, where every compiler looks unaided: a + // project without dependency modules is laid out, and every command + // spelled, as before. + // + // A unit is told where the BMIs it reaches lie through one module map: the + // modules it imports, the modules those import in turn, and the module it + // provides, each resolved in the importing package's closure by the one + // resolver (`mcpp::modgraph::resolve_provider`). At the end of make_plan, // after every producer of a compile unit (a target's `main` included). { const auto traits = mcpp::toolchain::bmi_traits(tc); - std::set> collided; - for (auto const& [name, units] : graph.providersOf) - if (units.size() > 1) collided.insert(name); + const auto rootPackage = qualified_package_name(manifest); auto basename = [&](std::string_view name) { std::string out; for (char c : name) out.push_back(c == ':' ? '-' : c); @@ -3223,89 +3283,114 @@ make_plan(const mcpp::manifest::Manifest& manifest, return out; }; auto bmi_file = [&](std::string_view provider, std::string_view name) { - return collided.contains(name) ? std::string(provider) + "/" + basename(name) - : basename(name); + return provider == rootPackage ? basename(name) + : std::string(provider) + "/" + basename(name); + }; + auto bmi_path = [&](std::string_view provider, std::string_view name) { + return std::string(traits.bmiDir) + "/" + bmi_file(provider, name); }; for (auto& cu : plan.compileUnits) if (!cu.providesModule.empty()) cu.bmiFile = bmi_file(cu.packageName, cu.providesModule); - if (!collided.empty()) { - const bool gcc = traits.moduleFileUsePrefix.empty(); - std::set> packagesWithUnits; - for (auto const& cu : plan.compileUnits) packagesWithUnits.insert(cu.packageName); - std::map, std::less<>> flagsOf; - for (auto const& pkg : packagesWithUnits) { - std::vector> entries; // name -> BMI - std::vector> bound; // collided ones - for (auto const& [name, units] : graph.providersOf) { - auto provider = mcpp::modgraph::resolve_provider(graph, pkg, name); - if (!provider) continue; - const auto path = std::string(traits.bmiDir) + "/" - + bmi_file(graph.units[*provider].packageName, name); - entries.emplace_back(name, path); - if (collided.contains(name)) bound.emplace_back(name, path); - } - if (bound.empty()) continue; // sees no collided name - // GCC's mapper answers only what it lists. The standard - // library modules, and any name a unit of the package imports - // that no unit of the graph provides (a BMI placed by other - // means), are listed where GCC's own mapper puts them. - if (gcc) { - std::set> listed; - for (auto const& [name, path] : entries) listed.insert(name); - auto flat = [&](const std::string& name) { - if (listed.insert(name).second) - entries.emplace_back(name, std::string(traits.bmiDir) + "/" + basename(name)); - }; - flat("std"); - flat("std.compat"); - for (auto const& cu : plan.compileUnits) - if (cu.packageName == pkg) - for (auto const& imp : cu.imports) - if (!graph.providersOf.contains(imp)) flat(imp); + const bool gcc = traits.moduleFileUsePrefix.empty(); + std::string_view prefix = traits.moduleFileUsePrefix; + while (!prefix.empty() && prefix.front() == ' ') prefix.remove_prefix(1); + const bool separate = !prefix.empty() && prefix.back() == ' '; + if (separate) prefix.remove_suffix(1); + + // Every unit, the ones the global cache will serve included: their + // stage edges wait for the BMIs they import that this build compiles + // (e2e 849), and find them through the same map. + for (auto& cu : plan.compileUnits) { + // name -> (provider package, BMI path); a name no unit of the graph + // provides is placed by other means (`std`, a staged BMI) at its + // name, and has no provider. + std::map, std::less<>> reach; + std::vector> pending; // (importer package, name) + for (auto const& imp : cu.imports) pending.emplace_back(cu.packageName, imp); + while (!pending.empty()) { + auto [importer, name] = std::move(pending.back()); + pending.pop_back(); + if (reach.contains(name)) continue; + auto provider = mcpp::modgraph::resolve_provider(graph, importer, name); + if (!provider) { + reach.emplace(name, std::pair{std::string{}, + std::string(traits.bmiDir) + "/" + basename(name)}); + continue; } - std::sort(entries.begin(), entries.end()); - ModuleScope scope; - for (auto const& [name, path] : entries) scope.content += name + " " + path + "\n"; - scope.mapFile = std::filesystem::path("modmap") - / std::format("{}-{}.map", pkg, - mcpp::toolchain::hash_string(scope.content).substr(0, 8)); - auto& flags = flagsOf[pkg]; - if (gcc) { - flags.push_back(mcpp::manifest::flag_element( - "-fmodule-mapper=" + scope.mapFile.generic_string())); - } else { - // `-fmodule-file==` (clang) or `/reference - // =` (MSVC), with an absolute path so the - // compile databases, whose directory is the project, - // name the same file. - std::string_view prefix = traits.moduleFileUsePrefix; - while (!prefix.empty() && prefix.front() == ' ') prefix.remove_prefix(1); - const bool separate = !prefix.empty() && prefix.back() == ' '; - if (separate) prefix.remove_suffix(1); - for (auto const& [name, path] : bound) { - auto bmi = outputDir / std::filesystem::path(path); - bmi.make_preferred(); - const auto value = name + "=" + bmi.string(); - if (separate) { - flags.push_back(std::string(prefix)); - flags.push_back(mcpp::manifest::flag_element(value)); - } else { - flags.push_back(mcpp::manifest::flag_element(std::string(prefix) + value)); - } + auto const& u = graph.units[*provider]; + reach.emplace(name, std::pair{u.packageName, bmi_path(u.packageName, name)}); + for (auto const& req : u.requires_) + pending.emplace_back(u.packageName, req.logicalName); + } + const bool ownQualified = !cu.providesModule.empty() && cu.packageName != rootPackage; + const bool importsQualified = std::ranges::any_of(reach, [&](auto const& e) { + return !e.second.first.empty() && e.second.first != rootPackage; + }); + if (!ownQualified && !importsQualified) continue; // every BMI at its name + + std::map> entries; // name -> BMI path + for (auto const& [name, at] : reach) entries.emplace(name, at.second); + if (!cu.providesModule.empty()) + entries.emplace(cu.providesModule, bmi_path(cu.packageName, cu.providesModule)); + // GCC's mapper answers only what it lists: the standard library's + // modules are listed where GCC's own mapper puts them. + if (gcc) + for (auto const* std_name : {"std", "std.compat"}) + entries.emplace(std_name, std::string(traits.bmiDir) + "/" + basename(std_name)); + + ModuleScope scope; + for (auto const& [name, path] : entries) scope.content += name + " " + path + "\n"; + if (!gcc && importsQualified) { + // `-fmodule-file==` (clang) or `/reference + // =` (MSVC) for each module the unit imports that + // does not lie at its name, absolute, so that a compilation + // database read from another directory names the same files. + // The unit's own module is written through `-fmodule-output=` + // or `/ifcOutput`, and is not loaded. clang reads an argument + // file by GNU rules on every host, where a backslash escapes + // (#247), so its paths are written with forward slashes; MSVC + // reads it by its own rules and takes native paths. + for (auto const& [name, at] : reach) { + if (at.first.empty() || at.first == rootPackage) continue; + auto bmi = outputDir / std::filesystem::path(at.second); + const auto value = name + "=" + + (separate ? bmi.make_preferred().string() : bmi.generic_string()); + if (separate) { + scope.arguments.push_back(std::string(prefix)); + scope.arguments.push_back(value); + } else { + scope.arguments.push_back(std::string(prefix) + value); } } - plan.moduleScopes.emplace(pkg, std::move(scope)); } - for (auto& cu : plan.compileUnits) { - if (cu.kind != mcpp::SourceKind::ModuleInterface - && cu.kind != mcpp::SourceKind::Cxx) - continue; - if (auto f = flagsOf.find(cu.packageName); f != flagsOf.end()) - cu.packageCxxflags.insert(cu.packageCxxflags.end(), - f->second.begin(), f->second.end()); + // The key names both files. Two units may share a map and differ in + // their arguments (a module and a unit that imports it list the same + // BMIs; only the importer loads one), so the hash covers both, by + // the relative paths they hold: the key is the same in every build + // directory, as the map's own content is. + std::string identity = scope.content; + if (!scope.arguments.empty()) { + identity += "loads"; + for (auto const& [name, at] : reach) + if (!at.first.empty() && at.first != rootPackage) + identity += " " + name + "=" + at.second; + } + const auto key = std::format("{}-{}", cu.packageName, + mcpp::toolchain::hash_string(identity).substr(0, 8)); + cu.moduleScope = key; + scope.mapFile = std::filesystem::path("modmap") / (key + ".map"); + if (gcc) { + cu.packageCxxflags.push_back(mcpp::manifest::flag_element( + "-fmodule-mapper=" + scope.mapFile.generic_string())); + } else if (!scope.arguments.empty()) { + scope.argsFile = std::filesystem::path("modmap") / (key + ".modmap"); + auto args = outputDir / scope.argsFile; + args.make_preferred(); + cu.packageCxxflags.push_back(mcpp::manifest::flag_element("@" + args.string())); } + plan.moduleScopes.try_emplace(key, std::move(scope)); } } diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 8d043bd02..b1dd679ed 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -874,4 +874,13 @@ prepare_build(bool print_fingerprint, bool includeDevDeps = false, // set of notes. export std::vector take_notes_on_failure(); +// After a drive of `ctx`'s plan succeeded: the global cache receives the +// dependencies that the drive compiled because no entry served them. Called by +// every command that builds a plan (`build`, `test`, `pack`); a pack once left +// the cache unfilled, so the next build of another configuration compiled the +// same dependencies again (pack drive and selection design 2026-10-01, A4). +// prepare_build records no such dependency under `--cache=local|off`; the mode +// is checked here as well, so the write side states its own condition. +export void populate_dependency_cache(BuildContext& ctx); + } // namespace mcpp::build diff --git a/src/build/prepare/plan.cpp b/src/build/prepare/plan.cpp index cb627ba58..62b98ec90 100644 --- a/src/build/prepare/plan.cpp +++ b/src/build/prepare/plan.cpp @@ -2059,17 +2059,17 @@ static std::expected step13_dependency_cache(PrepareState& st // consumer three edges away, which is far harder to read than // one extra compile. if (cu.packageObjectRel.empty()) { addressable = false; break; } - // A BMI below its package's directory (a module name two - // packages of the plan provide, mcpp#732) has no address in - // the entry, whose BMIs are named by module: the package - // compiles here instead of being cached. - if (cu.bmiFile.find('/') != std::string::npos) { addressable = false; break; } + // The entry names a BMI by its module; the build places it + // below its package's directory (B1), and the entry is filled + // from there. if (!cu.providesModule.empty()) { std::string bmi; for (char c : cu.providesModule) bmi.push_back(c == ':' ? '-' : c); bmi += std::string(bmiT.bmiExt); + if (!cu.bmiFile.empty() && cu.bmiFile != bmi) + arts.bmiBuildRel.emplace(bmi, cu.bmiFile); arts.bmiFiles.push_back(std::move(bmi)); } arts.objFiles.push_back({cu.packageObjectRel.generic_string(), @@ -2465,4 +2465,18 @@ void focus_on_member(BuildContext& ctx) { ctx.projectRoot = m.root; } +void populate_dependency_cache(BuildContext& ctx) { + if (ctx.cacheMode != CacheMode::Global) ctx.depsToPopulate.clear(); + for (auto& task : ctx.depsToPopulate) { + auto pr = mcpp::bmi_cache::populate_from(task.key, ctx.outputDir, task.artifacts); + if (!pr) { + mcpp::ui::warning(std::format( + "bmi cache populate failed for {}@{}: {}", + task.key.packageName, task.key.version, pr.error())); + } + } + // Once: a second drive of the same plan has nothing new to add. + ctx.depsToPopulate.clear(); +} + } // namespace mcpp::build diff --git a/src/build/progress.cppm b/src/build/progress.cppm index c08557d46..4d834b0e9 100644 --- a/src/build/progress.cppm +++ b/src/build/progress.cppm @@ -190,6 +190,9 @@ enum class ProgramOutcome { Ran, Cached, Failed }; void open(bool verbose); // Closes it: the region is erased. Idempotent. void close(); +// Whether the command opened the report and has not closed it. A drive whose +// caller supplies no report is reported by the backend exactly then. +bool is_open(); // The status row as the region would draw it now. std::string status_row(); // The number of configurations the command builds: with more than one, a @@ -1233,6 +1236,12 @@ void close() { r.keys.reset(); // the terminal's mode is restored here } +bool is_open() { + auto& r = report(); + std::lock_guard lock(r.m); + return r.open; +} + void configurations(std::size_t n) { auto& r = report(); std::lock_guard lock(r.m); diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index 9fee26df3..3609b613e 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -12,6 +12,7 @@ import std; import mcpplibs.cmdline; import mcpp.build.prepare; import mcpp.build.execute; +import mcpp.build.ninja; // write_module_maps import mcpp.bmi_cache.maintenance; // parse_duration, for `clean --stale --older-than` import mcpp.build.directives; // the device-slot table import mcpp.build.configure; @@ -695,6 +696,10 @@ export int cmd_emit_build_database(const mcpplibs::cmdline::ParsedArgs& parsed) for (auto const& r : planRoots) failedMemberRoots.push_back(r); continue; } + // The argument lists name the plan's module maps; the files are + // written into the plan's work directory, so a reader that + // expands them finds them. + mcpp::build::write_module_maps(ctx->plan); contexts.push_back(std::move(*ctx)); workDirs.push_back(mo.work_dir); testDiscovery.push_back(std::move(discovery)); @@ -1239,8 +1244,8 @@ export int cmd_dyndep(const mcpplibs::cmdline::ParsedArgs& parsed) { if (!bmiExtStorage.empty()) opts.bmiExt = bmiExtStorage; opts.splitModuleEdges = parsed.is_flag_set("split-module"); - // mcpp#732: the package's module map, when two packages of the plan - // provide one module name. + // The unit's module map, when a BMI it reaches lies below its provider's + // directory (pack drive and selection design 2026-10-01, B1). std::map> moduleMap; if (auto mm = parsed.option_or_empty("module-map").value(); !mm.empty()) { std::ifstream is{mcpp::platform::fs::extended_length(std::filesystem::path{mm})}; diff --git a/src/pack/library_pipeline.cppm b/src/pack/library_pipeline.cppm index fe0c5b896..a59089d1b 100644 --- a/src/pack/library_pipeline.cppm +++ b/src/pack/library_pipeline.cppm @@ -29,6 +29,8 @@ import mcpp.build.backend; import mcpp.build.ninja; import mcpp.build.plan; import mcpp.build.prepare; +import mcpp.build.progress; +import mcpp.build.flags; // profile_descriptor import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; @@ -127,6 +129,15 @@ export int build_and_pack_library(const std::string& targetName, std::vector legs = triples; if (legs.empty()) legs.push_back({}); // one leg, this host + // EACH LEG'S BUILD IS STATED AS `mcpp build` STATES ONE (#753): its + // packages' lines and the status row as it runs, and one `Finished` for + // the legs together, after the last of them. + const bool reporting = mcpp::build::progress::is_open(); + if (reporting && legs.size() > 1) { + mcpp::build::progress::configurations(legs.size()); + mcpp::build::progress::defer_finished(); + } + LibraryPackPlan plan; plan.builtBy = std::string(mcpp::MCPP_VERSION); plan.writeArchive = opts.format == mcpp::pack::Format::Tar; @@ -186,19 +197,20 @@ export int build_and_pack_library(const std::string& targetName, // fingerprint, and picking one by name or by mtime silently selects a // stale binary. The link unit knows its own output. auto be = mcpp::build::make_ninja_backend(); + mcpp::build::progress::programs_done(); mcpp::build::BuildOptions bo; if (auto br = be->build(ctx->plan, bo); !br) { // The compiler's own output, not just "build failed". `mcpp build` // has always printed this; `mcpp pack` dropped it, so a failure // inside the packer's build arrived as three words and CI logs had // nothing to go on. - if (!br.error().diagnosticOutput.empty()) { - std::fputs(br.error().diagnosticOutput.c_str(), stderr); - if (br.error().diagnosticOutput.back() != '\n') std::fputs("\n", stderr); - } - mcpp::ui::error(br.error().message); + mcpp::build::report_failed_drive(br.error()); return 1; } + mcpp::build::populate_dependency_cache(*ctx); + if (reporting) + mcpp::build::progress::finished( + ctx->profile, mcpp::build::profile_descriptor(ctx->plan.manifest.buildConfig)); // Everything below reads the package being packed. const auto kept = *target; mcpp::build::focus_on_member(*ctx); @@ -430,6 +442,7 @@ export int build_and_pack_library(const std::string& targetName, }); mcpp::ui::status("Packed leg", std::format("{} [{}]", triple, tag.str())); } + if (reporting && legs.size() > 1) mcpp::build::progress::finish_deferred(); // ── does the package cover what it claims? ───────────────────────── // diff --git a/src/pack/pipeline.cppm b/src/pack/pipeline.cppm index 73dc263f1..7e940e141 100644 --- a/src/pack/pipeline.cppm +++ b/src/pack/pipeline.cppm @@ -18,6 +18,7 @@ import mcpp.build.distribution; import mcpp.build.flags; import mcpp.build.ninja; import mcpp.build.plan; +import mcpp.build.progress; import mcpp.config; import mcpp.fetcher.progress; import mcpp.manifest; @@ -164,15 +165,13 @@ build_extra_android_legs(const std::string& targetName, if (!ctx) { mcpp::ui::error(ctx.error()); return std::nullopt; } auto be = mcpp::build::make_ninja_backend(); + mcpp::build::progress::programs_done(); mcpp::build::BuildOptions bo; if (auto br = be->build(ctx->plan, bo); !br) { - if (!br.error().diagnosticOutput.empty()) { - std::fputs(br.error().diagnosticOutput.c_str(), stderr); - if (br.error().diagnosticOutput.back() != '\n') std::fputs("\n", stderr); - } - mcpp::ui::error(br.error().message); + mcpp::build::report_failed_drive(br.error()); return std::nullopt; } + mcpp::build::populate_dependency_cache(*ctx); // FROM THE PLAN, never a glob — see the function comment. A // dependency's own `shared` target also contributes a `SharedLibrary` @@ -349,6 +348,9 @@ struct PackRun { // The value of a dispatched `--format` is not known to be one until prepare // has run; see `prepare_group`. bool quietUntilValidated = false; + // The command reports its build (`mcpp pack` in human output): the drives + // are stated as `mcpp build` states one (#753). + bool reporting = false; std::optional cfg; }; @@ -996,6 +998,15 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba } }; stage_members(); + // THE DISPATCH PASS IS REPORTED AS A BUILD (#753). `Finished` closed the + // report of the build above; this pass reopens it for its own planning, + // its build programs and its drive, and closes it before the members' + // results. + if (run.reporting) mcpp::build::progress::open(mcpp::log::is_verbose()); + struct CloseReport { + bool on; + ~CloseReport() { if (on) mcpp::build::progress::close(); } + } closeReport{run.reporting}; auto distCtx = mcpp::build::prepare_build(false, false, {}, g.ov); // A MEMBER WITHOUT A TREE FAILS ALONE (P3). Its provider may name only a // built file and proceed, which is why the member is kept in the pass; when @@ -1149,18 +1160,16 @@ std::optional dispatch_group(PackRun& run, GroupJob& g, mcpp::build::Ba for (auto const& [name, s] : submitted) for (auto const& o : s.outputs) std::filesystem::remove_all(absolute_of(o), rmEc); } + mcpp::build::progress::programs_done(); mcpp::build::BuildOptions dbo; dbo.keepGoing = run.several; auto dr = be.build(distCtx->plan, dbo); const bool driveFailed = !dr.has_value(); if (!dr) { - if (!dr.error().diagnosticOutput.empty()) { - std::fputs(dr.error().diagnosticOutput.c_str(), stderr); - if (dr.error().diagnosticOutput.back() != '\n') std::fputs("\n", stderr); - } - if (!run.several) return Refusal{1, dr.error().message}; - if (!dr.error().reported) mcpp::ui::error(dr.error().message); + mcpp::build::report_failed_drive(dr.error()); + if (!run.several) return Refusal{1, {}}; } + if (run.reporting) mcpp::build::progress::close(); // THE CRITERION IS THE FILE, NOT THE EXIT CODE. A cached build program // replaying the first pass's answer, or a tool that writes nothing and @@ -1245,6 +1254,7 @@ std::optional check_destinations(PackRun& run, std::vector& g // in, which is one member; a single member is the same steps with one member. PackOutcome run_pack(PackRun run) { auto be = mcpp::build::make_ninja_backend(); + run.reporting = mcpp::build::progress::is_open(); // ─── Build first (pack implies a fresh build) ──────────────────── mcpp::build::BuildOverrides base; @@ -1311,23 +1321,47 @@ PackOutcome run_pack(PackRun run) { return PackOutcome{refused->rc}; } - // ─── One build per group, each member staged, one dispatch ─────── + // ─── One build per group, then one `Finished` ──────────────────── + // + // THE PACK STATES ITS BUILD AS `mcpp build` STATES ONE (#753): each + // package that does work has its line, the status row counts the build, + // and `Finished` closes the report before the first `Packing` line. Every + // group is built before any is staged, so a pack over several + // configurations writes one `Finished`, as `mcpp build` does; a group + // whose build fails still fails alone (P3). + mcpp::build::progress::programs_done(); + std::size_t building = 0; + for (auto const& g : groups) if (g.rc == 0) ++building; + if (run.reporting && building > 1) { + mcpp::build::progress::configurations(building); + mcpp::build::progress::defer_finished(); + } + bool everyGroupBuilt = true; for (auto& g : groups) { if (g.rc != 0) continue; mcpp::build::BuildOptions bo; auto br = be->build(g.ctx->plan, bo); if (!br) { - // The compiler's own output, not just "build failed" — same reason as - // in the library pipeline. - if (!br.error().diagnosticOutput.empty()) { - std::fputs(br.error().diagnosticOutput.c_str(), stderr); - if (br.error().diagnosticOutput.back() != '\n') std::fputs("\n", stderr); - } - mcpp::ui::error(br.error().message); + mcpp::build::report_failed_drive(br.error()); g.rc = 1; + everyGroupBuilt = false; if (!run.several) return PackOutcome{1}; continue; } + mcpp::build::populate_dependency_cache(*g.ctx); + if (run.reporting) + mcpp::build::progress::finished( + g.ctx->profile, mcpp::build::profile_descriptor(g.ctx->plan.manifest.buildConfig)); + } + if (run.reporting && building > 1 && everyGroupBuilt) + mcpp::build::progress::finish_deferred(); + // A report not closed by `Finished` (a group failed) is closed here, before + // the members' own lines. + mcpp::build::progress::close(); + + // ─── Each member staged, one dispatch per group ────────────────── + for (auto& g : groups) { + if (g.rc != 0) continue; // Everything below reads the package being packed: in a workspace plan, // each selected member in turn (workspace design 2026-09-29 §15). for (auto& m : g.members) { @@ -1343,7 +1377,8 @@ PackOutcome run_pack(PackRun run) { } if (run.opts.format != mcpp::pack::Format::Dispatched) continue; if (auto refused = dispatch_group(run, g, *be)) { - mcpp::ui::error(refused->message); + // A failed drive was reported where it failed. + if (!refused->message.empty()) mcpp::ui::error(refused->message); for (auto& m : g.members) if (m.result.rc == 0) m.result.rc = refused->rc; if (!run.several) return PackOutcome{refused->rc}; diff --git a/tests/e2e/09_path_dependency.sh b/tests/e2e/09_path_dependency.sh index 4994898e3..e023286c8 100755 --- a/tests/e2e/09_path_dependency.sh +++ b/tests/e2e/09_path_dependency.sh @@ -77,7 +77,9 @@ out=$("$MCPP" run 2>&1) # build.ninja should compile sources from BOTH packages ninja_file="$(find target -name build.ninja)" grep -q 'mylibA.*greet.cppm' "$ninja_file" || { echo "ninja missing dep package source"; exit 1; } -grep -q 'mcpp.cache/mylibA.greet.gcm\|gcm.cache/mylibA.greet.gcm' "$ninja_file" || { +# A dependency's BMI lies below its package's directory (pack drive and +# selection design 2026-10-01, B1); the root's would lie at its name. +grep -q 'gcm.cache/[^ /]*mylibA/mylibA.greet.gcm' "$ninja_file" || { echo "ninja missing dep BMI"; exit 1; } # Path-resolution error reporting: declared name mismatch diff --git a/tests/e2e/847_a_module_name_is_unique_within_a_program.sh b/tests/e2e/847_a_module_name_is_unique_within_a_program.sh index de5a4b136..e8a90a242 100755 --- a/tests/e2e/847_a_module_name_is_unique_within_a_program.sh +++ b/tests/e2e/847_a_module_name_is_unique_within_a_program.sh @@ -16,8 +16,9 @@ # Criteria, with the default toolchain (GCC on Linux, clang on macOS and # Windows): # A. An app and its `artifacts` updater each provide a different module `boost`: -# the build succeeds, each program prints its own module's value, and the -# two BMIs lie below their packages' directories. +# the build succeeds, each program prints its own module's value, the +# updater's BMI lies below its package's directory and the app's, the +# root's, at its name (pack drive and selection design 2026-10-01, B1). # B. Editing the updater's `boost` rebuilds the updater and not the app. # C. Two independent members of one workspace, each with its own `boost`: # `--workspace` builds both, each with its own value. @@ -27,7 +28,7 @@ # naming the program's package. # F. One file reached twice within one closure: refused, and named as one # file reached as two packages. -# G. A build whose module names each have one provider writes no module map +# G. A build whose modules are all the root package's writes no module map # and no binding flag: its build directory is laid out as before. set -e @@ -81,8 +82,9 @@ app=$(bin_of a/app/target app); upd=$(bin_of a/app/target updater) [ -n "$app" ] && [ -n "$upd" ] || fail "A: a program is missing" a.log [ "$("$app" | tr -d '\r')" = 1 ] || fail "A: the app does not print its own module's value" [ "$("$upd" | tr -d '\r')" = 2 ] || fail "A: the updater does not print its own module's value" -nb=$(find a/app/target -path '*.cache/*/boost.*' -type f | wc -l) -[ "$nb" -eq 2 ] || { find a/app/target -name 'boost.*'; fail "A: expected two BMIs below their packages' directories, found $nb"; } +nq=$(find a/app/target -path '*.cache/*updater/boost.*' -type f | wc -l) +nf=$(find a/app/target -path '*.cache/boost.*' -type f | wc -l) +[ "$nq" -eq 1 ] && [ "$nf" -eq 1 ] || { find a/app/target -name 'boost.*'; fail "A: expected the updater's BMI below its directory and the app's at its name, found $nq and $nf"; } echo "ok: A, an app and its artifacts updater each have their own boost" # ── B ────────────────────────────────────────────────────────────────────── @@ -210,12 +212,12 @@ echo "ok: F, one file reached twice in one program is refused as such" # ── G ────────────────────────────────────────────────────────────────────── (cd a/updater && "$MCPP" build > "$TMP/g.log" 2>&1) || fail "G: a plain build failed" g.log -[ -z "$(find a/updater/target -type d -name modmap)" ] || fail "G: a plan without a collision wrote a module map" +[ -z "$(find a/updater/target -type d -name modmap)" ] || fail "G: a plan of the root's own modules wrote a module map" ninja=$(find a/updater/target -name build.ninja | head -1) [ -n "$ninja" ] || fail "G: no build.ninja" if grep -q 'module-map\|fmodule-mapper\|cache/[^ ]*/boost\.' "$ninja"; then - fail "G: a plan without a collision binds a module name" "$ninja" + fail "G: a plan of the root's own modules binds a module name" "$ninja" fi -echo "ok: G, a plan without a collision is laid out as before" +echo "ok: G, a plan of the root's own modules is laid out as before" echo "PASS: 847_a_module_name_is_unique_within_a_program" diff --git a/tests/e2e/849_a_staged_bmi_waits_for_a_module_compiled_here.sh b/tests/e2e/849_a_staged_bmi_waits_for_a_module_compiled_here.sh index a9c23ad47..2d0a8236a 100755 --- a/tests/e2e/849_a_staged_bmi_waits_for_a_module_compiled_here.sh +++ b/tests/e2e/849_a_staged_bmi_waits_for_a_module_compiled_here.sh @@ -124,12 +124,14 @@ N=$(find target -name build.ninja | head -1) [ -n "$N" ] || fail "B: no build.ninja" D=$(dirname "$N") # The BMI directory and extension are the toolchain's (gcm.cache/*.gcm for -# GCC, pcm.cache/*.pcm for clang); they are read from the stage edge. -stage=$(grep -E '^build [a-z]+\.cache/gen\.dep\.[a-z]+ : stage_file ' "$N" || true) +# GCC, pcm.cache/*.pcm for clang), and a dependency's BMIs lie below its +# package's directory there (pack drive and selection design 2026-10-01, B1); +# all three are read from the stage edge. +stage=$(grep -E '^build [a-z]+\.cache/[^ ]*gen\.dep\.[a-z]+ : stage_file ' "$N" || true) [ -n "$stage" ] || fail "B: the cached BMI has no stage edge" "$N" bmi=$(echo "$stage" | awk '{print $2}') -bmidir=${bmi%%/*}; ext=${bmi##*.} -table="$bmidir/gen.dep.table.$ext" +ext=${bmi##*.} +table="$(dirname "$bmi")/gen.dep.table.$ext" case "$stage" in *"|| $table"*) ;; *) fail "B: the stage edge does not wait for the generated module's BMI: $stage" ;; diff --git a/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh b/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh new file mode 100644 index 000000000..33cbef020 --- /dev/null +++ b/tests/e2e/871_a_pack_states_its_build_and_every_drive_takes_the_job_count.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# requires: pack python3 +# 871 -- `mcpp pack` states the build it performs as `mcpp build` states one, +# and every command that compiles runs at most `[build] jobs` compiles at once +# (pack drive and selection design 2026-10-01, A; mcpp#753). +# +# Each command assembled its own ninja options. `mcpp build` attached the +# report and passed the job count; `mcpp pack` did neither, so a release job +# whose pack recompiled mcpp showed `Planning` for six minutes and no +# `Compiling` line, and `mcpp test` and `mcpp pack` ran ninja's default number +# of jobs whatever `[build] jobs` said. The backend now takes both from the plan +# and the open report. +# +# Criteria: +# A. A cold `mcpp pack --format tar` writes a `Compiling` line for the +# package and `Finished` before `Packing`. +# B. Under `--message-format json`, standard output is one JSON document. +# C. With `[build] jobs = 1`, no two compile steps overlap in `.ninja_log` +# under `mcpp build`, `mcpp test` and `mcpp pack`. Four units of about a +# third of a second each overlap under ninja's default on any machine +# with two cores. +set -e + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT +fail() { echo "FAIL: $1"; shift; for f in "$@"; do echo "--- $f ---"; cat "$f" 2>/dev/null; done; exit 1; } +cd "$TMP" + +mkdir -p app/src app/tests +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[build] +jobs = 1 + +[targets.app] +kind = "bin" +main = "src/main.cpp" +EOF +# Four translation units whose compile takes a measurable time: a constant +# evaluation of a few hundred thousand steps each, in two nested loops so that +# no loop exceeds GCC's per-loop limit (262144 iterations) and the whole stays +# within clang's step limit. +for i in 1 2 3 4; do + cat > app/src/u$i.cpp < app/src/main.cpp <<'EOF' +#include +unsigned long long value1(); unsigned long long value2(); +unsigned long long value3(); unsigned long long value4(); +int main() { std::printf("%llu\n", value1() ^ value2() ^ value3() ^ value4()); } +EOF +cat > app/tests/t.cpp <<'EOF' +int main() { return 0; } +EOF +cd app + +# no_overlap