diff --git a/.agents/docs/2026-09-08-documentation-architecture-three-trees.md b/.agents/docs/2026-09-08-documentation-architecture-three-trees.md new file mode 100644 index 00000000..9abc6f57 --- /dev/null +++ b/.agents/docs/2026-09-08-documentation-architecture-three-trees.md @@ -0,0 +1,598 @@ +--- +subject: docs +status: active +--- + +# Three documentation trees, three audiences, and the rule for citing between them + +This restructures mcpp's documentation as a whole: the user documentation +(`docs/`), the specifications (`docs/specs/`), and the design records +(`.agents/docs/`). Each serves a different reader, so each gets its own +admission criterion, register, language policy, stability promise and +lifecycle — and the relation between them becomes a rule that a check can +enforce rather than a habit. + +It supersedes the documentation half of +`.agents/docs/2026-09-08-examples-curriculum-and-documentation-plan.md`. That +plan's example curriculum stands and is referenced here rather than restated; +its chapter-level proposals (splitting `05`, adding chapters 22 and 23) are +carried in unchanged and placed inside the architecture this document defines. + +Measured 2026-09-08 against `origin/main` at `6e1c65c6`. + +--- + +## 1. The measurement + +### 1.1 The three trees + +| tree | files | lines | index | +|---|---|---|---| +| `docs/` chapters (English) | 23 | 11,313 | `docs/README.md`, 35 lines | +| `docs/zh/` | 23 | — | `docs/zh/README.md` | +| `docs/specs/` | 5 | 1,156 | `docs/specs/README.md`, 42 lines | +| `.agents/docs/` | **268** | **109,146** | `.agents/docs/README.md`, **one heading, no body** | +| `.agents/skills/` | 4 | 997 | none | + +The design records are **8.8 times** the entire user-facing documentation +(11,313 + 1,156 lines), and their whole index is the single line +`# 开发/方案文档目录`. + +### 1.2 Seven defects, each independently verifiable + +1. **The architecture is stated exactly once, in a leaf.** The three-way + division of labour — user docs / specs / design docs, with an audience named + for each — exists as a table in `docs/specs/README.md`. `docs/README.md`, + which is where a reader arrives, does not mention `.agents/docs` at all. + +2. **The specifications are written in Chinese, inside the English tree, with no + `docs/zh/` counterpart.** Every chapter under `docs/` has a `docs/zh/` mirror + whose heading structure CI compares. `docs/specs/` has neither: it is + Chinese-primary and unmirrored. + +3. **The style checker's scope is a glob, and the glob is why.** + `.github/tools/check_docs_style.sh` iterates `docs/*.md docs/zh/*.md`. That + pattern does not descend, so `docs/specs/` is exempt from the register rules + and from the parity loop — by accident rather than by decision. Defect 2 is + the visible consequence. + +4. **`docs/README.md` lists two of four specifications.** SPEC-002 + (`target-side.md`) and SPEC-003 (`exit-codes.md`) exist, are indexed in + `docs/specs/README.md`, and are absent from the tree's front page. + +5. **User documentation cites design records.** Five chapters send a reader to + `.agents/docs/…` (`05`, `08` twice, `11`, `20`), and four `docs/zh/` chapters + do the same. A design record carries no stability promise and describes a + moment; a user chapter that ends in one has delegated a question it should + have answered. + +6. **Six code comments cite chapters that do not exist.** + `docs/35-pack-design.md`, `docs/04-schema-xpkg-extension.md` and + `docs/34-release-readiness.md` are named from `modules/manifest/src/`, + `src/pack/`, `src/pm/` and `src/publish/` — survivors of an earlier numbering. + Nothing checks that a cited document exists. + +7. **The design tree has no taxonomy and no status.** Filenames carry an + implicit one — 92 contain `design`, 60 `plan`, 20 `analysis`, 7 `review`, + and **65 contain none of the sixteen classifier words in use** — and nothing + records whether a record is being executed, has shipped, was superseded, or + was abandoned. `2026-09-05-heterogeneous-build-ecosystem-design-v2.md` and + `2026-09-06-ecosystem-plan-v3.md` encode that in the filename, which works + for the two documents whose authors thought of it. + +### 1.3 The renumbering cost, which overturns yesterday's recommendation + +| cited as | files citing | where | +|---|---|---| +| `docs/NN-…` | **103** | `.agents/`, `docs/`, `src/`, `modules/` | +| `examples/NN-…` | **59** | `.agents/` 17, `docs/` 18, `examples/` 9, `tests/` 7, `.github/` 4, `src/` 3 | + +`docs/05-mcpp-toml` alone is cited 90 times. + +Yesterday's plan proposed renumbering five example directories, estimating the +cost as "external links". The measurement says otherwise: the example numbers +are cited from **seven test files, four CI workflows and three source files**, +where a stale path is a broken job rather than a broken link. Both trees are +therefore treated the same way in §6: **numbers are stable; grouping happens in +the index.** + +--- + +## 2. Diagnosis + +### 2.1 The division of labour is right and is unreachable + +`docs/specs/README.md` already says what the three trees are for, and its table +is correct. The defect is placement: it sits two levels down, in the tree whose +audience is the narrowest of the three, in a language the enclosing tree does +not use. A contributor who wants to know where a document belongs will not find +it, so documents land where the last similar one landed. + +That is the whole mechanism behind defects 1, 5 and 7. Nothing enforced the +division because nothing published it. + +### 2.2 `docs/` already contains three audiences + +The numbered sequence reads as one audience and is not: + +| chapters | audience | +|---|---| +| 00, 01, 02, 03, 05, 06, 07, 10, 12–18, 20, 21 | people using mcpp | +| 08 (toolchain internals), 11 (machine output) | people writing against a mechanism | +| 04 (build from source), 09 (releasing mcpp) | people changing mcpp | + +`09-release.md` documents how a maintainer cuts a release. It is chapter nine of +a sequence whose first three chapters are hello-world, dependencies and +packaging. Nothing marks the transition. + +### 2.3 Citation direction is a habit + +Four directions are in use and only one of them is examined: + +- spec → design record, in a metadata row: **correct**, and it is the + convention `docs/specs/README.md` already prescribes. +- user chapter → spec: **correct** and underused. +- user chapter → design record: **wrong**, and present five times. +- code comment → chapter: **unchecked**, and wrong six times. + +--- + +## 3. The design: three classes + +Each class is defined by its reader. Everything else — register, language, +stability, lifecycle — follows from that and is stated so the answer to "where +does this belong" is mechanical. + +| | **用户文档** `docs/` | **规范** `docs/specs/` | **设计记录** `.agents/docs/` | +|---|---|---|---| +| **reader** | someone with a task in hand | someone implementing against the mechanism: index authors, downstream tools, contributors | whoever works on that change, and whoever later asks why it is like this | +| **question it answers** | how do I do X | what exactly is guaranteed, and is it implemented yet | why is it this way, and what was refuted | +| **emphasis** | completing the task; the shortest correct path | precision; every rule tagged with implementation status | the reasoning and the measurements, including the ones that overturned the plan | +| **register** | declarative reference; tutorials may address the reader | RFC 2119 (必须 / 应当 / 可以) | narrative permitted; "why" is the content | +| **language** | English + `docs/zh/` parity, CI-checked | **decision in §6.2** | the language of the round; new writing in English academic register | +| **stability** | additive; a spelling is kept as an alias | numbered, versioned, state machine | **immutable once the change lands** | +| **lifecycle** | kept current with the implementation | Draft → Review → Accepted → Superseded | active → landed → superseded / abandoned | +| **history** | none — a chapter describes today | a change record at the end | it *is* history | +| **admission criterion** | a reader with this task cannot finish without it | two independent implementations could disagree without it | a decision was made whose reasoning would otherwise be lost | +| **who reviews** | anyone who has done the task | whoever owns the mechanism | whoever did the work | + +Three consequences worth naming because they are the ones that get violated: + +**A user chapter never records history.** "This was a bug until 2026.8.16" is a +design record's sentence. A chapter states what is true and, where a version +matters, states the floor: "2026.9.6.5+". + +**A design record is never edited after its change lands** — except to add a +status line or a correction block that says what later measurement overturned. +The alternative is a document that silently becomes a claim about the present, +and the repository has already met the failure that produces: a decision +written a second time without reading the first. + +**A specification is the only tree with a normative voice.** If a user chapter +finds itself writing 必须, the content belongs in a spec and the chapter should +cite it. + +### 3.1 The fourth surface, named so it stops being invisible + +`.agents/skills/` holds four procedure documents for agents: +`mcpp-usage`, `mcpp-contributing`, `mcpp-release`, `mcpp-docs-style`. They are +not a fourth class of documentation; they are **executable procedure** — +ordered steps with criteria, addressed to an agent rather than a reader. + +The rule that keeps them from becoming a fourth copy: **a skill states the +procedure and cites the chapter for the explanation; it does not restate the +explanation.** `mcpp-release` and `docs/09-release.md` are the pair to watch — +the skill is the checklist, the chapter is the reasoning, and the version-number +rules must exist in exactly one of them. + +--- + +## 4. The citation rule + +Six edges; four allowed, two forbidden. + +``` + docs/ ────────────────▶ docs/specs/ allowed (cite for exact semantics) + docs/ ─ ─ ─ ─ ─ ─ ─ ▶ .agents/docs/ FORBIDDEN +docs/specs/ ──────────────▶ .agents/docs/ allowed, metadata row only (provenance) +docs/specs/ ──────────────▶ docs/ allowed (point at the how-to) +.agents/docs/ ────────────▶ anything allowed + code ──────────────▶ docs/ or docs/specs/ allowed, and the target must exist +``` + +**Why the forbidden edge is forbidden.** A design record describes a moment and +carries no stability promise. Sending a user into one means either the chapter +is incomplete, or the record holds something that has become normative. Both +have a fix, and neither is a link: + +> When a user chapter wants to cite a design record, the content is **promoted** +> — into the chapter if it is how-to, into a spec if it is a guarantee. The +> design record is then cited by the spec's metadata row, where provenance +> belongs. + +The five existing leak sites are the promotion worklist, and each names what it +would promote: schema ownership (→ SPEC-004), toolchain naming and the hermetic +link model (→ `08` or a spec), the machine-output protocol's design (→ `11`, +which already has SPEC-003 beside it), the heterogeneous design v2 (→ `20`). + +**Code comments cite documents, and the document must exist.** Six do not +today. The check is one line and is listed in §7. + +--- + +## 5. What changes in `docs/` + +### 5.1 Numbers stay; the index groups + +Renumbering is refused on the measurement in §1.3. `docs/README.md` becomes a +grouped index over the numbers that exist, and gains the two new chapters at the +next free numbers. + +| part | chapters | reader | +|---|---|---| +| **I — Using mcpp** | 00, 01, 05, 06, 07, 21, **22 (new)** | someone building something | +| **II — Shipping what you built** | 02, 10, 12 | someone publishing | +| **III — Toolchains and targets** | 03, 16, 14, 15, 13, 17 | someone whose target is not the host | +| **IV — Devices and accelerators** | 18, 20 | someone with a GPU or a board | +| **V — Extending mcpp from outside** | **23 (new)** | rule-package and index authors | +| **VI — Machine interfaces and compatibility** | 11, 19, SPEC-003 | tool authors, release engineers | +| **VII — Contributing to mcpp itself** | 04, 08, 09 | maintainers | + +Part VII is the change that fixes §2.2: `04`, `08` and `09` keep their numbers +and stop appearing to be step four, step eight and step nine of a user's path. + +### 5.2 The role index sits above the parts + +Before the parts, one table mapping an intention to a path across all three +teaching surfaces — chapters, examples and templates. This is the table +yesterday's plan specified; it is unchanged and belongs here because it is the +entry point for the whole documentation set, not for the examples alone. + +### 5.3 Splitting `05`, and the two new chapters + +Carried unchanged from the earlier plan, restated here as a table only: + +| moved out of `05` | lines | to | +|---|---|---| +| features and capabilities (§2.8–2.8.2) | 394 | **22 — Features and capabilities** (new) | +| `[xlings]`, the project environment (§2.13) | 317 | **17** | +| the C++ runtime contract (§2.3 `cxx_runtime`) | 242 | **03** | +| `[target.*]` conditioning (§2.7.1) | 138 | **14** | +| host tools from a dependency (§2.14) | 274 | **07** | +| `[hooks]` (§2.16) | 192 | **21** | + +`05` lands at about 1,570 lines. **23 — Authoring a rule package** is assembled +from `docs/07` §"Writing a rule package", the `mcpp::action` roles, the island +ladder's four rungs, `device_extensions` / `rule_module`, and the probe and +advisory channels. + +--- + +## 6. What changes in `docs/specs/` + +### 6.1 Four specs, and what is missing from the set + +SPEC-001 identity, SPEC-002 target side, SPEC-003 exit codes, SPEC-004 manifest +semantics. Two are missing from `docs/README.md` (defect 4) — a one-line fix. + +Two candidates for SPEC-005 and SPEC-006 emerge from §4's promotion worklist and +from the accelerator work: + +- **The artifact compatibility tag**, including the `accel` field's grammar, + the coverage relation (family targets, portable-form floors) and the matching + algorithm. It is normative, it has a second implementer today (`mcpp-index` + descriptors are written by hand against it), and it currently lives in + `docs/20` §"What a prebuilt artifact states" — a user chapter writing rules. +- **The `build.mcpp` directive protocol**, whose version number already + functions as a normative contract (`kProtocolVersion`; an engine refuses a + program declaring a higher one) and whose only description is a section of + `docs/07`. + +Both are promotions of existing text rather than new writing, and both are +listed as staged work rather than decided here. + +### 6.2 The language decision + +Today: Chinese, unmirrored, unchecked. Three options. + +| | cost | consequence | +|---|---|---| +| **(a) English primary + `docs/zh/specs/` mirror** | translate 1,156 lines once, then parity forever | uniform with the enclosing tree; reachable by the downstream tool authors the specs name as their audience | +| (b) move to a top-level `specs/`, keep Chinese | rewrite paths in ~15 citation sites | admits that specs are not user docs, and abandons the audience that cannot read them | +| (c) English only, no mirror | translate once | breaks the tree's own parity policy in the other direction | + +**Recommendation: (a).** The audience the specs themselves name — index authors +and downstream tooling — is the least likely of the three audiences to be +Chinese-reading, and the specs are the documents where a misreading is most +expensive. + +### 6.3 The metadata contract becomes checkable + +`docs/specs/README.md` already requires a metadata table and a change record in +every spec. Nothing checks it. §7 adds that, and the same check verifies that +every spec in the directory appears in both indexes. + +--- + +## 7. What changes in `.agents/docs/` + +268 files and 110,143 lines are **not rewritten**. Three additions, all of which +apply to new documents and are backfilled only where a reader needs them. + +### 7.1 Front matter, required on new records + +```yaml +--- +subject: heterogeneous # one of a short controlled list +status: landed # active | landed | superseded | abandoned +superseded_by: 2026-09-07-module-first-heterogeneous-surface.md +implements: [docs/20-heterogeneous-builds.md, docs/specs/SPEC-005] +--- +``` + +`status` is what the tree lacks most. A reader opening +`2026-09-05-accelerator-support-design.md` today cannot tell from the document +that `…-v2` and then `ecosystem-plan-v3` moved past it; the filename carries it +for the two authors who thought of the convention. + +### 7.2 A generated index + +`.agents/docs/README.md` is generated: grouped by `subject`, ordered newest +first within each, showing status and title. For the 268 existing records the +title comes from the first heading and the status is `landed` unless the +document says otherwise — a mechanical default that is right for almost all of +them, since they describe changes that shipped. + +The generator is a script, and the index is checked in so that reading the tree +on GitHub works. + +### 7.3 The subject list, and why it is short + +Derived from the filenames rather than invented: `toolchain`, `packaging`, +`resolution`, `target`, `freestanding`, `heterogeneous`, `ecosystem`, +`performance`, `platform`, `docs`, `process`. Eleven. A twelfth is added when a +document does not fit, which is a decision someone makes rather than a field +someone fills in freely — a free-text subject would reproduce the +sixty-five-uncategorised state in a new column. + +### 7.4 Two housekeeping items + +Four records carry no date prefix (`fix-xlings-package-home-detection.md`, +`llvm-install-failure-analysis.md`, `platform-abstraction-plan.md`, +`platform-remaining-ifdefs-report.md`) and `todos/` holds four more under a +directory the convention does not mention. Both are absorbed by the front matter +and the generated index without moving a file. + +--- + +## 8. The checks + +`.github/tools/check_docs_style.sh` keeps its three rules and gains a scope; a +new `check_docs_structure.sh` carries the rules that are about the architecture +rather than the prose. + +| # | rule | reads | +|---|---|---| +| 1 | the existing three: heading register, second person in reference docs, bilingual heading parity | text | +| 2 | **scope extended to `docs/specs/`** | the glob, corrected to descend | +| 3 | no `docs/**` file cites `.agents/` | the forbidden edge in §4 | +| 4 | a spec cites `.agents/` only inside its metadata table | line position | +| 5 | every `docs/…md` path named anywhere in the repository resolves | catches defect 6, in code as well as prose | +| 6 | every spec in `docs/specs/` appears in `docs/README.md` and in `docs/specs/README.md` | catches defect 4 | +| 7 | every spec has a metadata table and a change record | the contract `docs/specs/README.md` already states | +| 8 | a new `.agents/docs/*.md` has front matter with a known `subject` and `status` | new files only, by comparing against the merge base | +| 9 | `.agents/docs/README.md` matches what the generator would produce | the index cannot drift | + +Rule 5 is the one with reach beyond documentation: a comment in `src/` naming a +chapter is a citation, and today six of them are stale. Rule 8 is scoped to new +files on purpose — a rule that demanded front matter on 268 existing records +would be satisfied by a mechanical pass that adds a field nobody chose. + +**What none of them check.** Whether a chapter is *right*, whether a spec's +rules are complete, or whether a design record's reasoning holds. The checks +establish that each document is in the tree its content belongs to and is +reachable from that tree's index; a reader establishes the rest. + +--- + +## 9. Staging + +| stage | content | independent? | +|---|---|---| +| **1** | publish the architecture: the three-way table moves to `docs/README.md`; role index; parts I–VII; the two missing specs listed | yes — index only, no content moves | +| **2** | close defects 4, 5 and 6: promote the five leak sites, fix the six code citations | yes | +| **3** | checks 2–7 (scope, citation direction, resolvable paths, spec index and metadata) | after stage 2, so the tree is already clean when the check turns on | +| **4** | translate the specs, mirror to `docs/zh/specs/`, put them under the parity loop | yes; the largest single translation cost | +| **5** | front matter, subject list, generated index, checks 8–9 | yes | +| **6** | split `05`; add chapters 22 and 23 | after stage 1, which is where their entries in the index go | +| **7** | SPEC-005 (compatibility tag) and SPEC-006 (directive protocol), each a promotion | last; each is its own review | + +Stages 1–3 are a week's worth of work and close five of the seven defects. +Stage 4 is the largest and is separable. Stage 6 is the chapter work carried +from the earlier plan and is where its example work rejoins. + +--- + +## 10. What this deliberately does not do + +- **It does not renumber anything.** §1.3 measured the cost in test and CI + files, not link rot, and this reverses a recommendation made yesterday on an + estimate. +- **It does not rewrite the 268 design records.** Their value is that they were + written when the reasoning was fresh; a pass to normalise them would edit + documents whose defining property is that they are not edited. +- **It does not merge `.agents/skills/` into any tree.** They are procedure, and + §3.1 states the rule that keeps them from duplicating explanation. +- **It does not introduce a documentation site generator.** Every tree stays + readable as Markdown on GitHub, which is where its readers are; a generated + site is a separate decision with its own hosting and staleness questions. +- **It does not change what is *in* a chapter beyond the moves listed.** The + register work landed in #452 and the content is largely correct; this is an + architecture change, not a rewrite. + +--- + +## 11. Open questions for review + +1. **The specs' language (§6.2).** Recommendation is English primary with a + `docs/zh/specs/` mirror, at 1,156 lines of translation. The alternative that + is cheapest — leave them Chinese and exempt them explicitly rather than by + glob accident — is defensible if the specs' real audience is this ecosystem's + own contributors rather than the downstream tool authors they name. + +2. **Whether `08-toolchain-internals` belongs in part VII.** It documents + mechanism for people writing against it as much as for people changing mcpp. + Placing it under "Contributing" may hide it from the first group. + +3. **SPEC-005's scope.** The compatibility tag alone, or the tag plus the + prebuilt-artifact selection algorithm? The algorithm is implemented once, in + `src/pack/prebuilt.cppm`, and has no second implementer today — which is the + admission criterion in §3 arguing against including it. + +4. **Whether `status: landed` is the right default for the backfill.** It is + right for almost all of the 268 and wrong for the handful that were written + and never executed. The alternative is `unknown`, which is honest and makes + the generated index less useful on its first day. + +5. **Whether design records should carry a correction block.** §3 says a record + is immutable except for a status line. Several records in the tree have been + corrected in place by later measurement, which is how their own value was + preserved. Making that a named, dated block at the end — rather than an edit + in the body — would keep both properties. + +--- + +## 12. Implementation record, 2026-09-08 + +Shipped as PR #590 on `docs/architecture-three-trees`. Stages 1, 2, 3, 5 and 6 +of §8 are done; stage 4 (translating the specifications) and stage 7 (SPEC-005 / +SPEC-006) are not, and §12.4 says why. + +### 12.1 What the measurements say afterwards + +| surface | before | after | +|---|---|---| +| `docs/05-mcpp-toml.md` | 3,129 lines | 1,626 | +| largest chapter | 3,129 | 1,218 (`07`) | +| `.agents/docs/README.md` | one heading | generated, 313 lines over 269 records | +| user chapters citing a design record | 9 | 0 | +| specifications listed on the front page | 2 of 4 | 4 of 4 | +| stale `docs/NN-*.md` citations | 6 | 0 | +| manifest keys with no example | 13 | 7 | +| `build.mcpp` API names used by an example | 10 of 38 | 12 of 38 | +| structure rules enforced in CI | 0 | 10 | + +The seven manifest keys still uncovered are the ones §3.2 classifies as a code +block rather than an example — `[hooks]`, `scan_overrides`, `cxx_runtime`, +`module_extensions`, `platforms`, `[feature-xlings]` — **plus two the plan +intended to cover and did not**, for reasons worth stating rather than +carrying forward as debt: + +- `[resources]` compiles on PE targets only, and a declared file must exist on + every target. An example for it would add a binary `.ico` to the tree and + then assert nothing on the two platforms most of this repository's CI runs + on. It belongs to a Windows-facing example, which does not exist yet. +- `[runtime]` is the provider-neutral runtime contract a prebuilt library + declares. §4.2 folded it into the publishing example, which is blocked + (§12.4). + +### 12.2 What the review pass found + +Read as rendered text rather than as a diff, which is the rule the skill states +for exactly this reason: + +**Fourteen cross-references, in both languages, left pointing at section numbers +that had moved with their sections.** `§2.8.1` cited from inside the chapter +that now contains it; `§2.13` cited from the chapter that now *is* it; `§2.14` +cited from the chapter it moved to. Every heading structure stayed valid and the +bilingual parity check stayed green throughout, because neither of those is a +check about meaning. + +> A mechanical move keeps every structure valid and leaves the prose pointing at +> the old shape. The checks cannot see it; a reader can. + +**Chapter 17's "Related chapters" ended up mid-chapter**, and its own bullet for +`05` said "every manifest key, including `[xlings]`" — the one claim the move +made false. + +**Four translations were missing content that every check called equal.** The +style checker compares heading structure, which is what catches a page that has +fallen a section behind. It cannot see a table row or a fenced block that never +crossed. Found while comparing the two languages by hand after the split, then +by the rule written for it: + +| where | what was missing | +|---|---| +| `docs/zh/22` (from `05`) | the `[features]` section had **no body**: the TOML example that defines the syntax, and all three bullets | +| `docs/zh/05` §2.11 | the `identity` verdict table and the two paragraphs around it | +| `docs/zh/08` §7.4 | the shared-library format table, Mach-O's install name, and the refusal of an unservable target | +| `docs/zh/16` | **seven target rows** — the 简体中文 support matrix listed 14 targets where the English listed 21 | + +None was introduced by this batch. The last one is the argument for rule 10 on +its own: a bilingual pair can agree on every heading and disagree about which +targets the tool supports. + +**Two rules were written in the skill and not applied.** Chapter 22 had no +"Current limitations" while the skill calls that section mandatory. And "every +document states which rung it is on" was followed by no chapter and is not +useful to a reader; it became "a `Related documents` line naming the rung above +and below", which is what the chapters actually do and what carries the +gradient. A rule nothing follows is worse than no rule, because it makes the +skill unfalsifiable. + +### 12.3 What the work found in the implementation + +Three defects met while building `12-a-new-device-language`, each now in its +README because a rule author will meet all three: + +- An action's command runs from the **build directory**, so a rule joins + `manifest_dir()` to the package-root-relative paths `device_sources()` gives. +- A pipeline's exit status is its last command's. The first `toyc` summed with + `… | paste -sd+ - | bc`; when an earlier stage produced nothing `bc` still + exited 0, `set -e` never fired, and the program compiled, linked, ran and + printed `0`. +- **The compiler is a declared input.** Without it, editing `toyc.sh` left every + edge clean and the artifact kept the previous compiler's bytes. + +And one in the tooling, whose first description in this document was wrong in a +way worth keeping. `mcpp emit xpkg` writes `manifest = "mcpp.toml"` into the +`mcpp` segment; `mcpp xpkg parse` reports that key as **unknown** and exits 1, +and the "missing sources" error follows from it — the ignored key means nothing +derives sources from the manifest it names. + +> The first reading said "adding `sources` by hand makes it validate". It does +> not. It removes the second error, the summary prints, and the command still +> exits 1. The conclusion came from reading the OUTPUT rather than the STATUS, +> which is the failure this repository already has a memory for. + +`mcpp-index` uses that key in **0 of 218** descriptors: a package keeping its own +`mcpp.toml` omits the `mcpp` field entirely and mcpp looks the manifest up under +the version directory. So the defect is not a missing list — it is that `emit` +writes a descriptor shape the resolver in the same release does not implement +and no published descriptor uses. Recorded in `docs/21`; not fixed in a +documentation change. + +`build_examples.sh` refused all four new example roots for being in neither +`BUILD` nor `SKIP`. That is the denominator discipline working, and it is the +same shape §7 gives the coverage check. + +### 12.4 What is open, and why each is open rather than late + +**The specifications' language** (§6.2, §11.1). English primary with a 简体中文 +mirror is the recommendation and is 1,156 lines of translation. It is an open +question in this document, and translating before it is answered is the wrong +order. What did ship is the half that is not a question: the register rules now +cover `docs/specs/`, which they were exempt from by a glob rather than by a +decision. + +**SPEC-005 and SPEC-006** (§6.1). Each is a promotion of text that exists and +each is its own review. + +**A publishing round-trip example** (§4.3, B3). Blocked on the emit/parse +mismatch above: the loop it would teach cannot be shown end to end while the +descriptor `mcpp emit xpkg` writes is one `mcpp xpkg parse` refuses. +`[resources]` and `[runtime]` were to be folded into that track and are +therefore still uncovered. + +**14.7's `--no-accel` diagnostic and L0 of the rule ladder** are unaffected by +this batch and remain where the heterogeneous plan records them. diff --git a/.agents/docs/2026-09-08-examples-curriculum-and-documentation-plan.md b/.agents/docs/2026-09-08-examples-curriculum-and-documentation-plan.md new file mode 100644 index 00000000..b3712fc9 --- /dev/null +++ b/.agents/docs/2026-09-08-examples-curriculum-and-documentation-plan.md @@ -0,0 +1,526 @@ +--- +subject: docs +status: superseded +superseded_by: 2026-09-08-documentation-architecture-three-trees.md +--- + +# A curriculum for the examples, a reference for the documentation, and a check with a denominator + +This plans the `examples/` tree and the `docs/` chapters together, because the +request they answer is one request: that everything about **using** mcpp and +about **developing for** it be reachable by a reader who does not already know +where to look. + +It starts from a measurement rather than from an opinion about what is missing, +and it states a criterion for what earns an example so that the plan has a size +rather than an ambition. + +Measured 2026-09-08 against `origin/main` at `6e1c65c6` (engine 2026.9.8.1), +`mcpp:plugins` 0.4.0, `mcpp-index` and `xim-pkgindex` at their `origin/main`. + +--- + +## 1. The measurement this starts from + +Three surfaces, each with a denominator taken from the tree rather than from a +document. + +### 1.1 The manifest surface + +Denominator: the sections and keys `docs/05-mcpp-toml.md` documents (34 of them +with a distinct meaning). Numerator: whether any of the 21 example projects +contains that key in its own `mcpp.toml`. + +| covered by an example | 21 | +|---|---| +| **zero examples** | **13** | + +The thirteen: `[dev-dependencies]`, `[features]`, `[features.]`, +`[feature-deps.]`, `[feature-xlings.]`, `[scan_overrides]`, +`[profile.]`, `[runtime]`, `[package] platforms`, `[resources]`, +`[hooks]`, `cxx_runtime`, `module_extensions` (with `bmi_schedule`, +`build_program_timeout`, `device_extensions` and `rule_module` beside it). + +`[features]` is the one worth naming on its own. Every heterogeneous example +**consumes** a feature — `features = ["rules-cuda"]` on a dependency edge — and +**no example in the tree declares one**. The mechanism the entire accelerator +design rests on is visible only from the consuming side. + +### 1.2 The `build.mcpp` API + +Denominator: the names `docs/07-build-mcpp.md` documents (38). Numerator: the +names any example's `build.mcpp` calls. + +| used by an example | 10 | +|---|---| +| **zero examples** | **28** | + +Among the twenty-eight: `mcpp::action` (the primitive the whole rule layer is +built on, and the subject of a 136-line section in 07), `mcpp::runner`, +`mcpp::fact` / `mcpp::floor` (the probe channel), `mcpp::warning`, +`mcpp::has_feature`, `mcpp::xpkg_dir`, `mcpp::link_script`, `mcpp::target_os` +and the rest of the target-interrogation family. + +`mcpp::action` is reached indirectly — the rule packages call it, and the +examples call the rules — so a reader sees its effect and never its shape. That +is a defensible outcome for a consumer example and not for a repository that +also asks people to write rule packages. + +### 1.3 The command surface + +Denominator: the commands `print_usage()` in `src/cli.cppm` prints (21 +user-facing; the `dyndep` / `bmi-*` / `stage` family is excluded because, as the +source says, "nobody types it, ninja does"). + +| named in an example's README | 4 (`build`, `run`, `test`, `pack`) | +|---|---| +| **named in no example** | **17** | + +Including the entire library-author path: `new`, `add`, `update`, `search`, +`publish`, `emit xpkg`, `xpkg parse`. And the entire diagnosis path: `why`, +`self doctor`, `self env`, `self explain`, `clean --stale`, `cache`. + +### 1.4 The documentation's own shape + +| | lines | +|---|---| +| `docs/` total (English) | 12,469 | +| `docs/05-mcpp-toml.md` | **3,129 (25%)** | +| `docs/01-examples.md` | 64 | + +The chapter that indexes the curriculum is 64 lines and lists directories. The +chapter that documents the manifest is 3,129 lines and is simultaneously a field +reference, four conceptual essays and a compatibility record. + +### 1.5 What these numbers do not say + +A capability with no example is not automatically a defect. `bmi_schedule` is a +tuning key; an example directory for it would teach nothing a paragraph does not. +The numbers locate the question; §3.2 answers it. + +Two denominators are themselves imperfect and are stated so. The manifest count +is taken from what `docs/05` documents rather than from what the parser accepts, +so a key the parser accepts and the document omits is invisible to it — closing +that is part of §7. The command count excludes internal verbs by judgement, and +the judgement is recorded in the check's own table rather than left in this +paragraph. + +--- + +## 2. The defect the numbers describe + +### 2.1 Two structures, each grown along its own axis + +The examples are indexed by **build shape**: a program, a program with a +dependency, a static package, a workspace, a library, a cross build, a project +environment, a rule package, a device, a rendering pipeline. That ordering was +right when each new capability was a new shape. It broke at 09, where one number +acquired six sub-examples and the number line stopped being a line. + +The documentation is indexed by **mechanism**: the manifest, the build program, +toolchains, the target triple, the target side, devices, heterogeneous builds. +That is the right index for a reference and it is the wrong one for a first read. + +Neither is indexed by **the reader**. Someone who wants to publish a library +must know to read 10, then 02, then 12, then the `emit xpkg` paragraphs of 21, +and must discover that no example covers any of it. + +### 2.2 Three readers, traced + +**The library author.** Wants to publish. There is no example that goes from a +package to a descriptor to a consumer's `mcpp add`. `05-lib-distribution` stops +at the produced artifact; the descriptor half exists only in `docs/10` (160 +lines) and in the index repository's own conventions. + +**The rule-package author.** Wants a new device language, or a new generator. +The material exists: `docs/07` §"Writing a rule package" (115 lines inside a +944-line chapter), `examples/08-build-rules`, and — for anything about device +languages — `mcpp-plugins/README.md`, in a different repository. There is no +chapter, and `device_extensions` and `rule_module`, the two keys that make a new +device language cost no engine release, appear in no example. + +**The reader who wants features.** `docs/05` §2.8 is 130 lines and correct. No +example declares `[features]`, so the reader cannot see one work. + +### 2.3 A third teaching surface that nothing names + +`docs/13-baremetal.md` is 800 lines and its whole first lesson is: + +```bash +mcpp new blinky --template riscv-virt-rt +``` + +The lesson is a **template**, shipped by a package, not a directory under +`examples/`. `--template ocornut.imgui` is another. Any package may ship +`templates//`, and `src/scaffold/template.cppm` enumerates them. + +So the curriculum is not `examples/` alone, and an index listing only +directories is structurally unable to be complete. This is a decision to state, +not a gap to close: a bare-metal lesson belongs to the board package that +supplies the target world, and duplicating it into this repository would make +two copies that drift. What is missing is that **nothing tells a reader the +template surface exists**. + +--- + +## 3. Three decisions + +### 3.1 The examples are a curriculum; the documentation is a reference + +Neither should try to be the other. An example is ordered, runnable, and teaches +exactly one new thing relative to its predecessor. A chapter is complete, +indexed by mechanism, and assumes the reader arrives knowing what they want. + +The consequence for `docs/05`: the conceptual essays inside it are chapters, not +sections of a field reference (§5.2). + +The consequence for `docs/01`: it becomes the curriculum — tracks, order, what +each example is the **first** to teach, and the mapping from an intention to a +path through all three surfaces. + +### 3.2 What earns an example + +> A capability earns an example when it changes the **shape of a project** — the +> files it contains, the manifest it declares, or the commands its author types. +> A capability that is one line inside an existing project earns a code block in +> its chapter. A capability reached only through a command earns a scenario entry +> in `docs/21`. + +Applied to §1's thirteen uncovered manifest keys: + +| key | verdict | +|---|---| +| `[features]`, `[features.]`, `[feature-deps]`, `[dev-dependencies]`, `[profile.]` | **example** — they change what a project declares and what its author builds | +| `device_extensions`, `rule_module` | **example** — they are what a rule package IS | +| `[resources]`, `[runtime]` | **example**, folded into the publishing track rather than given their own | +| `[hooks]`, `scan_overrides`, `cxx_runtime`, `module_extensions`, `bmi_schedule`, `build_program_timeout`, `[package] platforms`, `[feature-xlings]` | **code block** — each is one line in a manifest that otherwise looks like an existing example's | + +Applied to §1's twenty-eight uncovered APIs: `mcpp::action`, `mcpp::runner`, +`mcpp::fact`/`floor`, `mcpp::warning` and `mcpp::has_feature` earn an example +because a rule package is a project shape. The target-interrogation family +(`target_os`, `target_arch`, `toolchain_dir`, `sysroot_dir`, …) earns code +blocks: each is one call inside a program the examples already contain. + +Applied to the seventeen uncovered commands: `new`, `add`, `search`, `publish`, +`emit xpkg` and `xpkg parse` earn an example because they are a **round trip** a +reader must see completed. `why`, `clean`, `cache`, `toolchain`, `self *` earn +scenario entries in `docs/21`, which is the chapter that exists for exactly this +and is 211 lines. + +This criterion is what keeps the plan at **four new examples** rather than +thirteen. + +### 3.3 One index over three surfaces + +Examples, templates and chapters are indexed from one place — the rewritten +`docs/01` — and the coverage check (§7) reads that index. A capability is +covered when the index names where it is taught, and the check verifies the +named place actually teaches it. + +--- + +## 4. The example tree + +### 4.1 Tracks + +The number line is replaced by six tracks. Numbering is retained inside a track +so existing links and muscle memory survive where they can. + +| track | question it answers | +|---|---| +| **A — The shape of a project** | I am writing a program or a library | +| **B — Publishing** | I want other people to use it | +| **C — The environment** | my build needs tools that are not the compiler | +| **D — Targets** | it does not run on the machine that builds it | +| **E — Devices** | part of it runs on a GPU or an accelerator | +| **F — Authoring for the ecosystem** | I am extending mcpp itself, from outside it | + +### 4.2 The tree + +| id | directory | status | the first to teach | +|---|---|---|---| +| A1 | `01-hello` | keep | a package, `import std`, `mcpp build` / `run` | +| A2 | `02-with-deps` | keep | `[dependencies]`, the lock file, `mcpp add` | +| A3 | `04-workspace` | keep | `[workspace]`, path dependencies | +| **A4** | **`03-features`** | **new** | `[features]` **declared**, `[feature-deps]`, `[dev-dependencies]`, `[profile.]`, `mcpp test` | +| B1 | `05-pack-static` | move from `03` | `mcpp pack --mode static`, `[resources]` folded in | +| B2 | `06-lib-distribution` | move from `05` | a library's interface and binaries, `[runtime]` folded in | +| **B3** | **`07-to-the-index`** | **new** | `mcpp emit xpkg` → `mcpp xpkg parse` → a descriptor → a consumer's `mcpp add`; the round trip completed | +| C1 | `08-project-subos` | move from `07` | `[xlings]`, `[xlings.workspace]`, a build program's `PATH` | +| D1 | `09-openkal-cross` | move from `06` | `--target`, one source for four machines | +| D2 | *(template)* | index only | bare metal, via `mcpp new … --template riscv-virt-rt`; owned by the board package | +| **E0** | **`10-heterogeneous/boundary`** | **new** | the island ladder: a consumer importing the **generated** module with no hand-written seam (§4.4) | +| E1 | `10-heterogeneous/cuda` | keep | the seam, a generated boundary, the driver as a fact and a floor | +| E2 | `10-heterogeneous/vulkan` | keep | a shader payload reached as a module | +| E3 | `10-heterogeneous/sycl` | keep | a second compiler with its own standard library | +| E4 | `10-heterogeneous/hip` | keep | the hand-written boundary, as the contrast to E1 | +| E5 | `10-heterogeneous/cann` | keep | a vendor outside the NVIDIA and Khronos lineages | +| E6 | `10-heterogeneous/multi-backend` | keep | several backends in one artifact, chosen at run time | +| E7 | `11-graphics/offscreen` | keep | a rendering pipeline whose result is pixels | +| F1 | `12-build-rules` | move from `08` | `host-module = true`, `mcpp::action` with `role = "check"` | +| **F2** | **`12-a-new-device-language`** | **new** | `device_extensions` + `rule_module`: a third-party rule package teaching mcpp a language the engine has never heard of | + +Four new directories; five renumbered; nothing deleted. + +### 4.3 What each new example is for, and its criterion + +**A4 `03-features`.** A library with an optional backend. Declares +`[features]` with a default set, `[feature-deps]` bringing a dependency in +behind one, `[dev-dependencies]` for its tests, and a `[profile.release]` +override. Its `build.mcpp` calls `mcpp::has_feature`. +*Criterion:* `mcpp build` with no features resolves a graph that names no +package belonging to the optional backend — the criterion `docs/20` already +states for the framework tier, applied to the smallest project that has it. + +**B3 `07-to-the-index`.** Two directories: a library, and a consumer. The +README walks `mcpp emit xpkg` to produce a descriptor, `mcpp xpkg parse +--json` to validate it, a local index registration, and the consumer's `mcpp +add`. *Criterion:* the consumer builds against the descriptor rather than +against a path dependency. This is the one example whose subject is the package +manager rather than the build system, and its absence is why seven commands have +no example. + +**E0 `10-heterogeneous/boundary`.** The ladder's bottom rung. `MCPP_EXPORT_C` +entry points, `mcpp.tools.island` generating the header and the module, and a +`main.cpp` that writes `import app.kernels;` — **no hand-written seam anywhere in +the tree**. Its README states what that costs: a C-shaped interface, and no place +for `cfg(accelerator = …)` to apply. E1 then reads as "and here is why you +usually add one". +*Criterion:* the project contains no `.cppm` other than generated ones, and the +program prints the right numbers. The plugins README claims this arrangement was +measured with GCC 16.1; nothing in either tree runs it, so this example is also +that claim's fixture. + +**F2 `12-a-new-device-language`.** A rule package in the example tree that +declares `device_extensions = [".toy"]` and `rule_module`, and a consumer whose +`[build] sources` names a `.toy` file. The "compiler" is a shell script that +transforms text, exactly as `tests/e2e/607` uses `cat` as a device linker: the +subject is the graph, not a vendor. +*Criterion:* the `.toy` file is compiled and its output joins the link, on an +engine release that has never heard of `.toy`. That is the property `docs/20` +credits the design with, and today its only instance is `rules-slang` in another +repository. + +### 4.4 The island ladder, which this round's discussion surfaced + +`mcpp.tools.island` generates two artefacts — the `extern "C"` header the device +compiler reads, and a module over it whose whole content is +`export using ::name;`. Four rungs exist in the mechanism today: + +| rung | who writes what | the consumer sees | +|---|---|---| +| L0 | nothing but marked entry points | `import app.kernels` — C-shaped | +| L1 | `scan()` generates; the project writes a seam | `import app.saxpy` — designed | +| L2 | `emit()` takes an explicit list; the project writes a seam | as L1 | +| L3 | the project writes header and module by hand | as L1 | + +Three of the four are documented in one paragraph of +`mcpp-plugins/README.md`; `tools-island` appears **zero times** in `docs/`. All +seven current examples are L1 or L3, so a reader cannot see L0 exist. + +The plan therefore does two things and not a third: it adds E0 and it documents +the ladder in the new chapter 23. It does **not** propose generating the seam's +C++ shape — `tools/island.cppm:29-31` states that "which functions, which types, +what happens on failure — is a design decision no generator makes well", and +overturning that is a design question of its own, listed in §10. + +--- + +## 5. The documentation chapters + +### 5.1 An entry layer indexed by the reader + +`docs/README.md` becomes a role index before it is a chapter list. Seven rows, +each naming an ordered path through all three surfaces: + +| I want to | read | run | +|---|---|---| +| write a program | 00, 05 §1 | A1, A2 | +| write a library others import | 10, 05 §2.4 | A3, A4 | +| publish it | 02, 10, 12 | B1, B2, B3 | +| build for another machine | 16, 15, 13 | D1, template `riscv-virt-rt` | +| use a GPU or an accelerator | 20, 18 | E0 → E1 → the rest | +| add a rule, a language or a generator | **23 (new)**, 07 | F1, F2 | +| add a package to the index | 10, `specs/package-identity` | B3 | +| change mcpp itself | 04, 09, 19 | — | + +### 5.2 Splitting `docs/05` + +`05` keeps the field reference and loses the essays, each of which goes to a +chapter that exists and is short: + +| section | lines | destination | +|---|---|---| +| §2.8, §2.8.1, §2.8.2 — features and capabilities | 394 | **new 22 — Features and capabilities** | +| §2.13 `[xlings]` — the project's environment | 317 | **17** (currently 199 lines) | +| §2.3 `cxx_runtime` — the C++ runtime contract | 242 | **03 — Toolchains** | +| §2.7.1 `[target.*]` conditioning | 138 | **14 — The target side** | +| §2.14 host tools from a dependency | 274 | **07 — build.mcpp** (which already has a section) | +| §2.16 `[hooks]` | 192 | **21 — Commands by scenario** | + +`05` lands at about 1,570 lines and becomes readable as what it is. + +### 5.3 Two chapters that must exist + +**22 — Features and capabilities.** Assembled from `05` §2.8; gains A4 as its +worked example. Nothing new is written except the example's walkthrough. + +**23 — Authoring a rule package.** New, and the chapter whose absence §2.2 +traces. It assembles: `docs/07` §"Writing a rule package"; `mcpp::action` roles +and the chained-action shape; `device_extensions` and `rule_module`; the island +ladder (§4.4) with its four rungs; the probe channel (`fact` / `floor`); the +advisory channel (`warning`); and the payload-declaration discipline +(`[feature-xlings]` under a `cfg(accelerator = …)` selector — two gates). It +points at `mcpp-plugins` for the shipped collection rather than restating it, +and it names F1 and F2 as its examples. + +### 5.4 The chapter table afterwards + +00–21 keep their numbers and meanings. 22 and 23 are added. `01` is rewritten as +the curriculum. `README.md` gains the role index. No chapter is removed. + +--- + +## 6. Defects to fix in the same batch + +Found while measuring, each independently verifiable: + +1. `examples/09-heterogeneous/README.md` never mentions that the boundary is + generated in two of its six sub-examples, while `docs/01`'s table does. +2. The same README's prose says "all four" three times and "The four beside it" + once, over a table listing six. +3. All seven heterogeneous and graphics examples pin + `plugins = { version = "0.3.0" }`. 0.4.0 is released and is the version in + which all six rules pass a depfile; a project copied from an example today + does not rebuild when a shader's `#include` changes. +4. `examples/09-heterogeneous/cuda/app/build.mcpp:20` states that "no source in + this project names a generated file", and line 51 of the same file states the + opposite, correctly — `src/cpu/saxpy.cpp:18` includes it. +5. `tools-island` appears zero times in `docs/`; `MCPP_EXPORT_C` appears twice, + both inside one table row. Closed by chapter 23. +6. `.cl` and `.metal` are in the engine's built-in device-extension table and no + published rule package claims either, so both produce a refusal. Either a rule + ships or the refusal says that no package claims the extension. This one is + engine or ecosystem work rather than documentation, and is recorded here + because the documentation currently implies support that does not exist. + +--- + +## 7. The check that keeps this true + +A capability added after this batch must be classified, or the build fails. + +**What it reads.** Three denominators, from the tree: + +- manifest keys, from the parse sites in `modules/manifest/src/` — not from + `docs/05`, which is what makes the check able to catch a key the document + omits; +- the `build.mcpp` API, from the exported names in `modules/buildmcpp/src/`; +- commands, from the `print_usage()` body in `src/cli.cppm`. + +**What it compares them against.** One checked-in table, `docs/coverage.toml`, +with exactly one row per capability and exactly one of three verdicts: + +```toml +[manifest."[features]"] +example = "examples/03-features" + +[manifest.bmi_schedule] +doc = "docs/05-mcpp-toml.md#build-concurrency" +reason = "one key in a manifest that otherwise looks like 01-hello" + +[api."mcpp::action"] +example = "examples/12-build-rules" +``` + +**What it refuses.** + +1. A capability in the tree with no row — a new key cannot ship unclassified. +2. A row naming an `example` whose project does not actually contain the + capability. For a manifest key the check parses that example's `mcpp.toml` + and asserts the key is present; for an API name it asserts the call appears + in that example's `build.mcpp`; for a command it asserts the README's command + block contains it. +3. A row naming a `doc` anchor that does not resolve. + +**Why the second refusal is written that way.** The obvious check — grep the +README for the key's name — passes on a document that merely mentions the key, +and fails the day someone rewords a heading. This repository has paid for that +shape before: a criterion that greps prose when it means to ask about state. The +check therefore reads the example's **manifest and sources**, and the prose is +not its subject. + +**What it cannot decide.** That an example teaches its subject *well*. The check +establishes that the capability is present where the index says it is; review +establishes the rest. + +--- + +## 8. Staging + +Each stage is independently mergeable and leaves the tree better than it found +it. + +| stage | content | why here | +|---|---|---| +| **1** | §6 defects 1–5; bump the seven examples to `plugins` 0.4.0 | pure corrections, no restructuring, and 3 is a live defect in copied projects | +| **2** | rewrite `docs/01` as the curriculum; add the role index to `docs/README.md`; name the template surface | the index must exist before things are moved into it | +| **3** | E0 and chapter 23 | the two halves of the gap this round's discussion found; E0 is also the fixture for a measured claim that nothing runs | +| **4** | A4 and chapter 22; split `[features]` out of `05` | the largest single uncovered mechanism | +| **5** | F2; finish the `05` split | rule authoring, which chapter 23 has by then described | +| **6** | B3 | the round trip; largest new example, and it needs a local index fixture | +| **7** | `docs/coverage.toml` and the check | last, because it is only enforceable once the rows it would demand exist | + +Renumbering (B1, B2, C1, D1, F1) happens in stage 2 with redirects left in +`docs/01`, so no stage both moves directories and adds content. + +--- + +## 9. What this deliberately does not do + +- **It does not delete an example.** Every current example teaches something no + other one does; the defect is the index, not the set. +- **It does not add an example per uncovered capability.** §3.2 is what keeps + four from becoming thirteen, and the criterion is stated so a future addition + is argued rather than assumed. +- **It does not duplicate the bare-metal lesson into `examples/`.** It belongs to + the board package that supplies the target world; what changes is that the + index names the template surface. +- **It does not restate `mcpp-plugins` in `docs/`.** Chapter 23 describes the + authoring surface and points at the collection; a second copy of a rule + collection's reference would drift on its own schedule. +- **It does not touch the Chinese translations' content policy.** Every chapter + added or split needs its `docs/zh/` counterpart in the same change, which CI + already enforces; the cost is counted in each stage. + +--- + +## 10. Open questions for review + +1. **Renumbering.** Stage 2 moves five directories. The alternative is to keep + today's numbers and let the tracks be a documentation-only grouping. Moving + makes the tree self-describing and breaks external links; not moving keeps + `03-pack-static` sitting between two basics. *Recommendation: move, with a + redirect table in `docs/01`.* + +2. **Whether the seam's C++ shape should ever be generated.** §4.4 leaves this + open deliberately. Generating it requires the generator to read intent it + cannot see today — that a pointer and a count are a span, that a returned + `int` is an error code — which is an IDL by another name. The question worth + deciding first is not "can we" but "how much convention before the generator + becomes a thing the design says it is not". + +3. **B3's index fixture.** The round trip needs an index to register against. A + local file-backed index in the example directory is self-contained and is not + what a real publisher does; using `mcpp-index` makes the example unrunnable + for a reader without publish rights. *Recommendation: local index, with the + README stating exactly which step differs in the real one.* + +4. **Whether `docs/21` should absorb the diagnosis commands or gain a sibling.** + Seventeen commands land there under §3.2. At 211 lines it can take them; at + double that it becomes the chapter nobody reads. *Recommendation: absorb now, + split when it passes 400 lines.* + +5. **Defect 6 (`.cl` and `.metal`).** Ship a rule package for one of them, or + change the refusal to state that no package claims the extension? The second + is small and honest; the first is ecosystem work with its own schedule. diff --git a/.agents/docs/2026-09-08-the-documentation-as-a-book.md b/.agents/docs/2026-09-08-the-documentation-as-a-book.md new file mode 100644 index 00000000..8ac607c7 --- /dev/null +++ b/.agents/docs/2026-09-08-the-documentation-as-a-book.md @@ -0,0 +1,634 @@ +--- +subject: docs +status: active +--- + +# The documentation as a book: a chapter-by-chapter design + +The previous record in this series +(`2026-09-08-documentation-architecture-three-trees.md`) settled **which tree** a +document belongs to and published that architecture. It did not design the book +inside `docs/`. It grouped the chapters that existed, renumbered them into bands, +and moved six sections out of an over-large reference. That is a +**reorganisation**. This document is the **design**: for every chapter, who reads +it, the one question it answers, what it contains, what it must not contain, +where it sits, why there, and how it is judged. + +Written because a reorganisation cannot fix what the measurements below show. A +tree indexed by mechanism gives a *task* no home, and no amount of renumbering +gives it one. + +--- + +## 1. What the measurement says, and why renumbering does not answer it + +### 1.1 No topic has an owner + +A topic is *owned* by the chapter that carries it as a `##` section. + +The first measurement counted headings whose text contained the topic's name, +and **that criterion was wrong in exactly the way this repository's own style +skill forbids** — a substring search standing in for a question about meaning. +It reported testing as a section in seven chapters. Reading those seven: + +| chapter | what its "test" section is actually about | verdict | +|---|---|---| +| `90-build-from-source` ×2 | **mcpp's own** test suite, for a contributor | a different subject | +| `50-machine-output` | the `--message-format json` schema | it owns that | +| `04-dependencies` | the `[dev-dependencies]` table | it owns that | +| `01-getting-started` | one step of a tutorial | legitimate; should link | +| `06-workspace` | the workspace fan-out | legitimate; should link | +| `11-publishing-a-library` | path overrides, under a heading that says "testing" | a naming defect | +| `03-mcpp-toml` | a worked example that happens to have tests | worked examples leave `03` | +| **`40-baremetal`** | **how a test runs, restated** | **the one real duplicate** | + +So the corrected reading: **one** duplicated explanation, one misnamed heading, +and a worked example that a separate decision moves anyway. + +**The conclusion survives the correction, and it is the part that mattered.** +Before `07` existed, no chapter answered "how do I test" — the seven partial +mentions each assumed a reader who already knew, and none of them was the place +to learn it. A topic can be unowned without being duplicated, and this one was. + +The same caution applies to the other rows of the first measurement, which are +kept here as what they are — a count of headings, not of explanations: +dependency resolution 7, C++ modules 7, diagnosis 4, caching 4, and **the model +0**. The last is the one that needs no re-reading: a section that does not exist +cannot be miscounted. + +### 1.2 There is no model to hang anything on + +`00-getting-started` goes from *Installation* to *Creating a Project* with no +section in between. mcpp is a build system, a package manager and a toolchain +provisioner in one program; a reader who does not know that reads every +subsequent chapter as an unrelated feature. The five nouns the whole tree uses +— package, graph, toolchain, target, index — are defined nowhere. + +### 1.3 The chapters were never given a shape + +| property | chapters that have it | +|---|---| +| an opening that names its reader and its question | **5 of 24** | +| a "Current limitations" section | **6 of 24** | +| length within 2× of the median (≈420 lines) | 18 of 24 | + +`02-mcpp-toml` is 1,626 lines and `05-build-mcpp` is 1,218; `51-supported-versions` +is 90. Nothing decided any of those. + +### 1.4 The conclusion + +> Grouping and renumbering fix the **index**. They do not fix the **book**. A +> chapter that was written because a mechanism existed keeps being about the +> mechanism, and a reader with a task keeps not finding it. + +--- + +## 2. The method: design from the reader's task, not from the mechanism + +Three rules produce every decision in §4 and §5. + +**R1 — One owner per topic.** Exactly one chapter owns a topic. Every other +chapter that touches it states one sentence and links. A second explanation is a +second thing to keep current, and the two diverge on their own schedule. + +**R2 — A chapter exists for a reader with a task, not for a mechanism with a +name.** "Features" is a mechanism; "make part of a package optional" is a task. +Where the two coincide the chapter keeps the mechanism's name, because that is +what the reader searches for — but the *contents* are decided by the task. + +**R3 — Every chapter states its reader, its question, and its exclusions in its +first fifteen lines.** The exclusions are the load-bearing half: they are what +stops the chapter from re-absorbing the topics R1 assigned elsewhere. + +A fourth rule governs the sequence rather than a chapter: + +**R4 — A part is a reader's arc, and its order is the order that reader needs +it in.** Not alphabetical, not chronological by when the feature shipped. + +Two more govern how a section is written, and both are about not handing the +reader a decision that the design already made: + +**R5 — One recommended path in the body; every other spelling in a +`
`.** mcpp supports more than one way to say many things, and it has a +style and a semantics of its own, so there is always a default. The body carries +that one. Legacy spellings, escape hatches and platform-specific forms fold +away. The criterion: a reader who reads only the body and opens nothing can do +the thing correctly without choosing. Folding a form away is not deprecation; +deprecation is said in words. + +**R6 — An increment is marked beside itself.** A key, flag or behaviour added +later carries its version floor on its own line (`2026.9.6.5+`), not at the top +of the chapter and never as "it used to be X". + +All six are recorded in `.agents/skills/mcpp-docs-style` so they outlive this +batch. + +--- + +## 3. The model the book teaches, stated once + +Everything in `docs/` is about five nouns and the seams between them. This is +the content of the new chapter §4.1 specifies, and it is written here first +because the chapter list is derived from it. + +| noun | what it is | the chapter that owns it | +|---|---|---| +| **package** | a directory with an `mcpp.toml`; identity is `(namespace, name)` | 02 | +| **graph** | what a build is: sources compile, objects link, actions extend | 05 | +| **toolchain** | a payload mcpp installs and pins, not a program found on the machine | 20 | +| **target** | the machine the artifact runs on, which is not the host | 21 | +| **index** | where packages come from, and what a descriptor promises | 11 | + +The seams, each of which is a chapter rather than a section, because each is a +place two nouns meet and a reader arrives with a question about the meeting: + +| seam | question a reader arrives with | +|---|---| +| features | how do I make part of a package optional | +| workspaces | how do several packages become one build | +| the target side | how does a manifest say "only on this target" | +| rules | how do I teach the graph something it has no rule for | +| devices | how do I run an artifact on a machine that is not this one | +| accelerators | how does part of my program get compiled for a device | + +**Why the model is a chapter and not a paragraph in `00`.** `00` is a tutorial: +a reader following it is typing, not building a mental model, and a model +inserted there is read as preamble to skip. The model chapter is short, it is +the first thing the index points at, and every other chapter may assume it. + +--- + +## 4. Chapters this design creates + +Three, each answering a question §1.1 measured as unowned. + +### 4.1 `00 — How mcpp Works` (new; current `00` becomes `01`) + +| | | +|---|---| +| **reader** | anyone, before anything else. Assumed by every other chapter | +| **question** | what are the moving parts, and which one is failing when something fails | +| **contains** | the five nouns of §3 with one paragraph each; the three seams a first build crosses (manifest → graph → toolchain); where each noun's state lives on disk (`target/`, the store, the index cache); the one diagram | +| **excludes** | any field name, any flag, any command beyond `mcpp build`. It is a model, not a reference | +| **why first** | §1.2. Without it every later chapter is an unrelated feature | +| **criterion** | a reader who has read only this chapter can say which noun a given error message is about | + +### 4.2 `04 — Dependencies and Resolution` (new) + +| | | +|---|---| +| **reader** | someone whose build now has more than their own code in it | +| **question** | where does a dependency come from, what does a version constraint mean, and what happens when two of them disagree | +| **contains** | `[dependencies]` in all its forms (index, path, git); what a bare version pins and what `>=` requires; one package one version, and the refusal when it cannot hold; `mcpp.lock` and `--locked`; `mcpp add` / `update` / `why`; `[dev-dependencies]` and `[build-dependencies]` and the difference in what each reaches | +| **excludes** | the *identity* rules (SPEC-001 owns them) and how to publish (11 owns that) | +| **why here** | it is the second thing every reader does, and today it is seven partial answers | +| **criterion** | a reader can predict which version resolves for a stated graph, and say why | + +### 4.3 `06 — Testing` (new) + +| | | +|---|---| +| **reader** | anyone with code that has to keep working | +| **question** | how do I run tests, what does mcpp consider a test, and how do I test what does not run on this machine | +| **contains** | `tests/**/*.cpp` as the convention; `mcpp test` and its selectors; `[dev-dependencies]`; the worker pool and `run_exclusive`; testing a cross or bare-metal target through a runner; `--message-format json` for a CI consumer, by reference | +| **excludes** | the runner *mechanism* (31 owns it) and the JSON schema (50 owns it) | +| **why here** | §1.1: seven chapters mention it, none owns it | +| **criterion** | a reader can run a test on a target their machine cannot execute | + +--- + +## 5. The book + +Bands are meaningful: the first digit is the part. Within a part the order is +the order that part's reader needs, which is R4. + +### 5.1 `0x` — Fundamentals + +The arc: understand the parts, get one program running, know where the examples +are, write the manifest, add dependencies, make things optional, test. + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 00 | How mcpp Works | anyone | what are the moving parts | fields, flags | +| 01 | Getting Started | a newcomer, typing | how do I get a program running | anything not on the path to a running program | +| 02 | Examples | a reader choosing a starting point | which example teaches what I need | the content of the examples | +| 03 | The mcpp.toml Manifest | an author | what may a manifest say | conditioning (22), features (05), the environment (23) | +| 04 | Dependencies and Resolution | an author with dependencies | where does a dependency come from, and which version wins | identity (SPEC-001), publishing (11) | +| 05 | Features and Capabilities | an author with something optional | how do I make part of a package optional | accelerator selection (32) | +| 06 | Testing | anyone | how do I run tests, including where they cannot run here | runners (31), the JSON schema (50) | +| 07 | Commands by Scenario | anyone, later | which command does the thing I want | everything each command means in depth | + +**Why `07` is last and not first.** It is a lookup, used after the reader knows +the nouns. A reader who opens it first gets a list of verbs with no model. + +**Why the manifest (03) precedes dependencies (04) and features (05).** Both are +manifest tables; a reader who has not seen a manifest cannot place them. + +### 5.2 `1x` — Publishing + +The arc: package an application, publish a library's source, publish its +binaries. + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 10 | Packaging an Application for Release | someone shipping a program | how do I produce something another machine can run | libraries (12) | +| 11 | Publishing a Library to mcpp-index | a library author | how does my package become one others can name | the descriptor grammar (SPEC-001) | +| 12 | Distributing a Prebuilt Library | a publisher of binaries | how do I ship compiled artifacts and state what they are compatible with | the compatibility tag's grammar (SPEC-005, planned) | + +**Why publishing precedes targets.** A library author publishes before they +cross-compile; an application author packages before they port. The reader who +needs `2x` knows they need it. + +### 5.3 `2x` — Toolchains and targets + +The arc: what a toolchain is and how it is chosen, how a target is named, how a +manifest conditions on one, what environment the project declares, and one +worked cross-compilation. + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 20 | Toolchain Management | anyone whose compiler matters | which compiler will build this, and how do I choose another | internals (91) | +| 21 | The Target Triple | someone building for another machine | how is a target named, and which are supported | conditioning (22) | +| 22 | The Target Side | an author supporting several targets | how does a manifest say "only there" | the accelerator axis (32) | +| 23 | The Project Environment | an author whose build needs tools | how does a project declare the tools its build runs | build programs (30-band) | +| 24 | Cross-Compilation Over openkal | someone cross-building a hosted target | how do I build for another OS from this one | bare metal (30) | + +### 5.4 `3x` — Extending the graph + +**This part is new as a grouping**, and it is where the design departs most from +what shipped. Build programs and rule packages are the same subject at two +scales: a project that needs the graph to do something it has no rule for, and a +package that supplies that rule to others. + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 30 | Build Programs: `build.mcpp` | an author whose build needs a step mcpp has no rule for | how do I add work to the graph | authoring a reusable rule (31) | +| 31 | Authoring a Rule Package | an ecosystem author | how do I package that step so other projects can use it | the shipped rules' spellings (`mcpp:plugins`) | + +**Why they are two chapters and not one.** Different readers with different +questions. The first has a project and a problem; the second has an audience. +The 1,218 lines of the current build-program chapter contain both, and its +"Writing a rule package" section is now a second copy of chapter 31. + +### 5.5 `4x` — Devices and accelerators + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 40 | Bare-Metal and Freestanding Targets | an embedded developer | how do I build for a machine with no OS | reaching it (41) | +| 41 | Reaching a Device | anyone whose artifact does not run here | how do I run and test it where it belongs | the target's construction (40) | +| 42 | Heterogeneous Builds | a GPU or accelerator developer | how does part of my program get compiled for a device | rule authoring (31) | + +**Why bare metal moved out of `3x`.** It is not a toolchain topic; it is a +target with no operating system, and everything a reader needs after that is +about reaching it and running on it — which is 41. The three chapters are one +arc. + +### 5.6 `5x` — Contracts for programs + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 50 | Machine-Readable Output | a tool or CI author | what may a program parse, and what is versioned | human-facing output | +| 51 | Supported Versions and Compatibility | anyone with a policy question | what may change between releases, and what may not | the exit-code table (SPEC-003) | + +### 5.7 `9x` — mcpp itself + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 90 | Building from Source and Contributing | a contributor | how do I build and change mcpp | how a user builds their project | +| 91 | Toolchain Internals | a contributor, or a user debugging a toolchain | how does mcpp actually resolve and assemble a toolchain | how to *choose* one (20) | +| 92 | Releasing mcpp | a maintainer | how is a release cut and verified | publishing a package (11) | + +--- + +## 6. What this changes against what shipped + +| decision | effect | +|---|---| +| three new chapters (00 model, 04 dependencies, 06 testing) | the three topics §1.1 measured as unowned get an owner | +| build programs move `0x` → `3x`, beside rule authoring | the two scales of one subject become one part | +| bare metal moves `3x` → `4x`, before devices | the embedded arc reads in order | +| every chapter gains a designed opening (reader, question, exclusions) | 5 of 24 have one today | +| every reference chapter gains a limits section | 6 of 24 have one today | +| the manifest reference sheds its worked examples and its appendix | `examples/` owns worked examples; the schema-ownership appendix is contributor material | +| `mcpp test` mentions in six chapters become one sentence and a link | R1 | + +**The renumbering this implies is the second in one batch, and that is the cost +of having reorganised before designing.** It is paid once here; §7 states the +order that keeps every citation resolving while it happens. + +--- + +## 7. Order of work + +1. Write `00`, `04` and `06` against their specs in §4. New content, no moves. +2. Apply R1: for each of the three topics, cut the six other explanations to a + sentence and a link. +3. Renumber into §5's bands, in one scripted pass with the link and label + rewrite the previous batch established. +4. Give every chapter its designed opening (R3) and, where it is a reference, a + limits section. +5. Split the manifest reference: worked examples out, appendix to `9x` or + SPEC-004. +6. Re-run the review in `.agents/skills/mcpp-docs-style` §13 against the eight + dimensions, with §5's table as the criterion for "面向人群" and "梯度". + +Steps 1 and 2 are the ones that make the book different. Step 3 is mechanical +and has a tested script. A batch that stops after step 3 has renumbered twice +and designed nothing, which is the failure this document exists to name. + +--- + +## 8. What this design does not claim + +- **It does not claim the current chapters are badly written.** Most are + accurate and several are excellent. The defect is that nothing decided what + each one is *for*, so topics landed wherever a mechanism needed them. +- **It does not merge chapters to reduce their number.** 24 chapters for a tool + that is three tools is not too many. Two of them are too long, and one + grouping was wrong; that is the whole of the structural change. +- **It does not settle the specifications' language.** That question stands + where the previous record left it. + +--- + +## 11. What is open after this batch, and the criterion for each + +### 11.1 Seventeen reference chapters have no limits section + +The style skill calls the section mandatory for a reference chapter. Two +chapters had the content under another heading and are renamed; the scenario, +model, tutorial and index chapters are exempt, because their scope is stated by +the "Not here" line in their opening and they claim no complete surface. + +Eight of the original seventeen were written from facts the chapter already +stated somewhere in its body — `05`, `20`, `21`, `22`, `23`, `41`, `50`, `51`. +That is the method that works: **promote a limit the chapter already states into +the section that collects them**, rather than inventing one. + +Eight remain: `04`, `07`, `10`, `11`, `24`, `90`, `91`, `92`. Scanning their +text for a limit statement returns nothing usable, which is the honest reading — +each needs a fact its area's owner can state. + +**They are open rather than written, and the reason is the rule itself.** A +fabricated limits section satisfies the check and measures nothing, which is +worse than the section being absent — the reader then believes the boundary has +been stated. Writing one requires a fact its area's owner can state and that a +reader can reproduce. + +*Criterion for closing one:* the section lists facts, each of which can be +reproduced on the current release, and each of which a reader could otherwise +only discover by hitting it. + +### 11.2 R5's debt was over-reported, and the measurement is why + +The first reading said four chapters present several spellings as equals, on the +evidence that `04`, `20`, `30` and `40` carry many code blocks and no +`
`. **That counted a proxy instead of the property.** A section with +five code blocks is usually five features documented in sequence — `20`'s are +glob exclusion, per-glob flags and generated files — or one continuous session, +which is what chapter `00`'s six blocks are. Folding any of those away would +break the thing they belong to. + +Measured on the property instead — a section that *offers alternatives at equal +weight*, marked by "alternatively", "either form", "也可以", "等价写法" — the +whole tree returns **three** hits, and none of the three is a choice presented +to a reader: two are prose about what is accepted, one is a comment in a command +line. + +So R5's real debt after `05` (the dependency spellings) and `01` (the two +installs) is close to zero. What remains is to apply the rule as new +alternatives appear, not to convert a backlog. + +*Criterion, restated so it is not measured by proxy again:* a reader who reads +only the body and opens nothing can do the thing correctly **without choosing +between two ways of doing it**. + +> Third time in this batch that a criterion counted something adjacent to the +> property: headings containing "test" for chapters that explain testing, +> substring spellings for a lookup index, code blocks for offered alternatives. +> The skill already says a criterion must not be a substring search; the variant +> worth naming is **counting a proxy**, which passes review because the number +> is real. + +### 11.3 The manifest reference still holds worked examples and an appendix + +`04` §3 is six worked examples, which `examples/` owns, and Appendix A is the +admission criteria for new manifest fields, which is contributor material and +belongs to `9x` or to SPEC-004. Both were named in §6 and neither is moved yet. + +### 11.4 The lookup index has no check + +§10's index is hand-built and correct today. Nothing compares it against the +reference chapter's own section list, so a key added to `04` and not indexed is +invisible. The check is the same shape as rule 4 (every specification appears in +every index) and is one loop. + +--- + +## 12. The ecosystem side, which the first design missed + +§5 designed the book for three readers — someone using mcpp, someone extending +one project's build, and someone changing mcpp. It missed a fourth, and the +measurement that found it is the sharpest in this record: + +> `xim:` payloads are named **84 times across 12 chapters**. How to make one is +> explained **nowhere**. `xim-pkgindex` appears 17 times, all of them in the +> chapter about releasing mcpp itself. + +Every toolchain, every device toolkit, every shader compiler and every emulator +in this ecosystem is a payload. The documentation taught the whole of consuming +them and none of producing them, and the omission was hidden by how often they +are mentioned. + +Two more of the same shape: a `compat:` runtime adapter — the layer that makes a +host library reachable from an artifact on Linux — is named in four chapters and +authored in none; and a board-support package is described from the consumer's +side in `40` and `41` with no chapter on writing one. + +### 12.1 Three chapters, and the band they complete + +`3x` was "extending the build graph" and is now **extending mcpp and its +ecosystem**: the two scales of one project's own step, then the three kinds of +package that serve everyone else. + +| # | chapter | reader | the one question | excludes | +|---|---|---|---|---| +| 32 | Authoring a Payload | someone packaging a tool or a prebuilt library | what is an `xim:` payload made of, and what must its descriptor say | source packages (11), host libraries (33) | +| 33 | Authoring a Runtime Adapter | someone making a host-supplied library reachable | why an artifact cannot see a library that is installed, and what fixes it | anything redistributable, which is a payload (32) | +| 34 | Authoring a Board-Support Package | someone bringing up a board | what does a BSP supply, and how does one package serve an emulator and hardware | using a BSP (40), the runner a consumer sees (41) | + +Each is written from a real published package rather than from the mechanism: +`xim-pkgindex/pkgs/g/glslang.lua`, `mcpp-index/pkgs/c/compat.vulkan-runtime.lua`, +and `mcpplibs/cortex-m-rt`. Every fact in them was read out of those files. + +### 12.2 The decision the adapter chapter records, which is not a packaging choice + +A proprietary driver's userspace is in ABI lockstep with a kernel module and its +licence forbids redistribution. Neither is solved by effort, so it is modelled +as a **host capability** and the adapter is how an artifact reaches it. An open +driver takes the other answer — it is a payload, and a machine using one needs +no adapter. **Which answer applies is decided by the licence and the ABI, not by +preference**, and that is the sentence a reader of scenario 10 leaves with. + +### 12.3 What this changes about §5's claim + +§5 said 24 chapters for a tool that is three tools is not too many. The count is +31 now, and the reason is that the tool is four things rather than three: a +build system, a package manager, a toolchain provisioner, **and an ecosystem +other people publish into**. The fourth had no chapters at all. + +--- + +## 13. Chapter 00 was designed wrong, and the review said so + +§4.1 specified `00 — How mcpp Works`: five nouns, three seams, where state lives +on disk. It was written, it was accurate, and it was the wrong chapter. + +**The review's verdict was that a general user does not care about the operating +principle.** What a reader opening the first chapter wants is what mcpp is, what +it can do, what its advantage is, and one example they can actually run. + +The design failed two of its own rules to get there: + +- **R2** — a chapter exists for a reader with a task. "Understand the machinery" + is not a task a first-time reader has; it is a task a maintainer has, and it + already has chapters in `9x`. +- **R7** — an advantage is shown by the artifact, not by the mechanism. A model + chapter explains how the advantage is produced and never demonstrates it. + +`00 — What mcpp Is` replaces it, and its shape came from the review too: +**background → who pays → what mcpp is → the guarantee → the smallest example +that shows it.** A first chapter earns the definition by first stating the +problem, and only then shows it solved. + +1. **The definition**, in the form the reviewer gave: mcpp = build system + + build plugins + package manager + toolchain management + the environment and + runtime (xlings), in one program. +2. **An analogy table** — CMake/Meson, `build.zig` and xmake rules, Conan/vcpkg, + Zig's bundled toolchain and rustup, Nix/conda — so a reader with existing + tools can place each part, with the disclaimer that places rather than + equates. Named beside it: **Cargo and Zig are the closest single-tool + analogues**, and for different halves of the same idea — Cargo for one + program being build, packages, lock and tests, Zig for the toolchain shipping + with the tool. The part neither has is the environment layer, which is why a + project can declare the non-compiler tools its build needs. +3. **The guarantee, stated once**: clone any mcpp project and `mcpp build` + works, without installing a compiler, configuring an environment, or hunting + dependencies. Plus the two boundaries that make it trustworthy. +4. **A session that was run**, with its real output: a five-line manifest, no + declared compiler or standard, `import std` compiling on a machine whose own + `g++` is 13.3.0 and cannot, and `mcpp self env` showing the GCC 16 mcpp + installed. 1.25 s of wall clock including the first run. +5. **What mcpp is for**: modules and the newest language features, and the + ecosystems that follow — embedded, heterogeneous and GPU, graphics, kernel + work. + +The one part of the old chapter a *user* wanted — the table from a message's +shape to the stage that produced it — moved to `09`, beside the other +diagnosis scenarios. The rest is deleted rather than relocated. + +**What this says about the method.** The seven-cell spec was filled for the old +chapter and it still produced the wrong chapter, because the cell that decides +everything — the reader — was answered with "anyone" and then served as if that +meant "someone who wants the model". A reader cell that names no task is not +filled in. + +**And the opening block came off.** Every other chapter opens with reader, +question and exclusions; on the front door that block reads as machinery. `00` +has no "not here" to declare because everything else *is* elsewhere, which its +closing paragraph says in a sentence instead. Rule 11 exempts `00` by name, with +that reason in the script. + +## 14. The register rules were written and then not enforced + +The user read chapter 00 and named three things: `背景:模块到了,工具链没跟上`, +`谁在为这个落差付账`, and the table header `大致相当于谁的活`. All three are +colloquial, all three violate §四 of the skill, and all three passed +`check_docs_style.sh`. + +Two independent defects in one check, and each is a form recorded elsewhere: + +**The predicate tested a marker rather than the property.** It matched `?`, 吗 +and 呢 — the punctuation a question usually carries. The property is the +interrogative *word*, and neither `谁在为这个落差付账` nor `打什么由谁决定` +carries the marker. The predicate is now the word set 谁 / 哪 / 什么 / 多少 / +为何 / 如何 / 怎样 / 怎么. + +**The object set was enumerated syntactically.** It read lines beginning with +`#`. The property a heading has — it names a topic, it is read out of order, it +is what a reader scans — is held equally by a table's header cells, which were +therefore never examined. Rule 4 adds them: the row above a `|---|---|` +separator, cell by cell. + +Extending both reported 100 headings and 57 header cells across 20 chapters, +in both languages. They are now noun phrases. The rename table is in the commit +rather than here, because it is a list of 157 strings and not a decision. + +**One consequence was not obvious.** Renaming 100 headings invalidates any link +that names a section by its anchor, and rule 9 of `check_docs_structure.sh` +discarded the fragment (`(?:#[^)]*)?`) — a link to a renamed section resolved to +the file and passed. It now computes GitHub's slug, including the `-1` / `-2` +suffix for repeated headings, and verifies the fragment. It immediately reported +two anchors in chapter 30 that had been wrong since they were written, before +any rename. + +### The rule that generalises + +R5 in the skill: **the scope of a background section is decided by the problem, +not by the tool.** Chapter 00's background was about modules, which is the part +of the problem mcpp is closest to. The problem a reader actually has is that a +C++ project needs four things at once — a build description, dependencies, a +compiler new enough, and an environment the result runs in — and no single tool +owns all four. CMake is the de facto standard for the first, and a de facto +standard is a statement about adoption rather than about the experience of use. +The environment is the layer that fails most often because it is the only one +nothing checks. Modules are the fourth constraint on that structure, not the +structure. + +## 15. The extension model had no owner, and the example that shows it was a toy + +The user asked where the plugin system is documented, and whether it reaches +preprocessing and languages of one's own. The tree had chapter 30 (build +programs) and chapter 31 (rule packages), and between them every primitive — +but no section stating the model: what the extension points are, what the +ecosystem has built from them, and where the boundary is. R1 says a topic has +exactly one owner, and this one had none. + +Chapter 31 now opens with it: five extension points with their effect and where +each is declared; seven things the ecosystem has built from them; three shapes +the model expresses (a new language whatever compiles it, preprocessing and +code generation, and a file that is partly C++ and partly another language); +and the boundary, which is measured rather than asserted — + +- a dependency's `device_extensions` is consulted *after* the built-in roles, + so a rule package cannot claim `.cpp`. Adding `".cpp"` to a rule's list is + not diagnosed and has no effect, measured; +- module-interface extensions are `[build] module_extensions`, a project axis, + because a module interface is scanned, produces a BMI and joins the link — + three engine behaviours rather than a command; +- an extension in neither table is refused by name, quoted from the run. + +### The example had to become real before it could carry this + +`examples/12-a-new-device-language` had a `.toy` that was "one integer per line, +and the entry point returns their sum", compiled by a shell script. It +demonstrated the two manifest keys and nothing else, and it was not a shape any +ecosystem author meets. + +`.toy` now has `let`, assignment, `if`/`else`, `while`, calls between kernels, +and the arithmetic and comparison operators. Its compiler is an ordinary mcpp +package — lexer, recursive-descent parser, semantic checks, C++ emitter — built +**for the build machine** through `tools = ["toyc"]` + `reexport = true` and +reached with `mcpp::dep_bin`. That makes it the tree's first example of a +dependency producing a host tool, a capability that until now existed only in +prose. + +**And it measured a boundary the documentation did not state.** The tool store's +key is the tool package's identity, version, host triple, compiler identity, +profile, features and the versions of its transitive dependencies. It holds no +source content, so editing a `path` tool's sources without changing its version +leaves the cached binary in place: `mcpp run` reported `Finished dev in 0.00s` +and printed the previous answer. Chapter 30 now states it, with the two ways +out. The action itself is not the gap — the rule declares the compiler binary +as an input, so a changed binary dirties the edge. What does not happen is the +rebuild that would change those bytes. diff --git a/.agents/docs/README.md b/.agents/docs/README.md index 839f7364..33623f14 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -1 +1,324 @@ -# 开发/方案文档目录 \ No newline at end of file +# Design records + +The reasoning behind changes to mcpp: what was measured, what was decided, +and what a later measurement refuted. A record describes the moment its +change was made and is not edited afterwards, so **nothing here is a +statement about the present**. What mcpp does today is in +[docs/](../../docs/README.md); what is guaranteed is in +[docs/specs/](../../docs/specs/README.md). + +**This file is generated** by `.github/tools/gen_agents_index.py` and is +checked in CI. A new record declares front matter: + +```yaml +--- +subject: heterogeneous # a short, reused word +status: landed # active | landed | superseded | abandoned +superseded_by: 2026-09-07-....md # when status is superseded +--- +``` + +270 records. + +## By subject + +Records that declare one. Everything else is listed by date below. + +### docs + +- [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active +- [A curriculum for the examples, a reference for the documentation, and a check with a denominator](2026-09-08-examples-curriculum-and-documentation-plan.md) — superseded by [2026-09-08-documentation-architecture-three-trees.md](2026-09-08-documentation-architecture-three-trees.md) +- [Three documentation trees, three audiences, and the rule for citing between them](2026-09-08-documentation-architecture-three-trees.md) — active + +## By date + +### 2026-09 + +- [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active +- [A curriculum for the examples, a reference for the documentation, and a check with a denominator](2026-09-08-examples-curriculum-and-documentation-plan.md) — superseded by [2026-09-08-documentation-architecture-three-trees.md](2026-09-08-documentation-architecture-three-trees.md) +- [Three documentation trees, three audiences, and the rule for citing between them](2026-09-08-documentation-architecture-three-trees.md) — active +- [第六轮生态复核:工具平面的目标轴](2026-09-07-round6-ecosystem-review.md) +- [一个包一个版本:xlings 地址的身份,以及 2026.9.6.5 之后的文档对齐](2026-09-07-package-identity-and-doc-alignment.md) +- [A module-first surface for graphics acceleration and heterogeneous computing](2026-09-07-module-first-heterogeneous-surface.md) +- [mcpp.toml 语义与风格的统一](2026-09-07-mcpp-toml-unified-semantics-design.md) +- [异构计算与图形的跨平台生态:完整矩阵与补齐方案](2026-09-07-heterogeneous-cross-platform-ecosystem.md) +- [通用构建基础设施:缺口、归属与验证](2026-09-07-general-build-infrastructure-gaps-design.md) +- [Ecosystem review — round 5b](2026-09-06-round5b-ecosystem-review.md) +- [Ecosystem review — round 5, F1 (llama.cpp on Vulkan)](2026-09-06-round5-f1-ecosystem-review.md) +- [The heterogeneous ecosystem, v3: what is built, what is not, and what decides each](2026-09-06-ecosystem-plan-v3.md) +- [多设备生态:实施计划与任务表](2026-09-05-multi-device-implementation-plan.md) +- [多设备加速生态:载荷、适配面、验证 lane 与框架验证](2026-09-05-multi-device-ecosystem-design.md) +- [Heterogeneous C++ builds and their ecosystem, v2: closing the host surface](2026-09-05-heterogeneous-build-ecosystem-design-v2.md) +- [加速器支持 实施计划](2026-09-05-accelerator-support-implementation-plan.md) +- [加速器支持:完整设计方案](2026-09-05-accelerator-support-design.md) +- [具名 runner、通用命令面、部分后端,与生态闭环](2026-09-04-named-runners-and-the-universal-command-surface.md) +- [Four gaps left by the ecosystem batch, and what to do about each](2026-09-04-four-gaps-after-the-ecosystem-batch.md) +- [商业级可用:mcpp × xlings 的裸机与嵌入式总体方案](2026-09-04-commercial-grade-baremetal-embedded-plan.md) +- [`[xlings]` is mcpp's surface for xlings' local project mechanism](2026-09-03-xlings-workspace-as-the-one-table.md) +- [The runner beyond bare metal: design](2026-09-02-runner-beyond-baremetal-design.md) +- [Issue #544: runner beyond bare metal — implementation plan](2026-09-02-issue544-runner-implementation-plan.md) +### 2026-08 + +- [Issue #540, verified: seven filed findings, six confirmed, one misaimed, four more underneath](2026-08-31-issue540-seven-audit-findings.md) +- [Project build hooks as owned intervals (#496)](2026-08-30-project-build-hooks-owned-intervals.md) +- [Three issues, measured: #532, #533, #534](2026-08-30-issues-532-533-534-analysis.md) +- [Four issues, measured: #527 (workspace half), #529, #535, #537](2026-08-30-issues-527-529-535-537-analysis-and-design.md) +- [Cross-repo fix plan: #532, #533, #534](2026-08-30-cross-repo-fix-plan-532-533-534.md) +- [The build-rule package: identity enforced, shape documented](2026-08-29-build-rule-package-spec.md) +- [issue #519:依赖的链接形态 —— 一条不变量,两个高度](2026-08-28-issue519-dependency-linkage-form.md) +- [目标侧被解析出来了,只发给了一个编译单元](2026-08-27-openkal-native-path-three-issues.md) +- [openkal 生态:接口判据、面的补全,与端口层的组合](2026-08-27-openkal-ecosystem-design-plan.md) +- [#516 解决方案:glob walk 在 Windows 上撞到 ANSI 代码页拼不出的目录名就崩](2026-08-27-issue516-windows-acp-glob-walk-fix.md) +- [支持矩阵:56 格实测,四处待修、一处生态空缺](2026-08-26-the-support-matrix-measured.md) +- [目标矩阵:六张表(载荷体系 / openkal 体系 × 三个构建机)](2026-08-26-target-matrix-six-tables.md) +- [目标矩阵:应该是什么,现在是什么,差在哪](2026-08-26-target-matrix-should-be-versus-is.md) +- [已经解析出的答案,没有被用来做决定](2026-08-26-resolved-but-not-consulted.md) +- [声明了却没被兑现:第八条,以及文档与词表的一次实测对账](2026-08-26-declared-but-not-made-to-exist.md) +- [`crossTarget` 非空被当成「系统来自图」](2026-08-26-cross-target-implies-graph.md) +- [aarch64 Linux:让整个生态可用,并且可测](2026-08-26-aarch64-linux-ecosystem-closure.md) +- [一个谓词族,六处缺陷:目标侧的分层判据](2026-08-25-the-two-layer-predicate-family.md) +- [目标体系分析:三元组承载了四件事,而它只有三段](2026-08-25-target-system-analysis.md) +- [OS × 工具链 × 目标:组合矩阵与理由](2026-08-25-os-toolchain-target-matrix.md) +- [mcpp 目标词表:一套规范,映射到各编译器](2026-08-25-mcpp-target-vocabulary-spec.md) +- [mcpp 目标侧设计](2026-08-24-target-side-design.md) +- [目标侧架构:五层、四来源、四规则](2026-08-24-target-side-architecture.md) +- [目标侧来自依赖图之后:七项优化方案](2026-08-24-graph-target-side-optimization-plan.md) +- [目标侧解析:预构建体系与构建期体系的统一架构](2026-08-23-target-side-resolution-architecture.md) +- [三项未完成事项的详细方案](2026-08-21-three-remaining-items-plan.md) +- [裸机生态的四项未完成事项:方案](2026-08-21-freestanding-outstanding-four.md) +- [裸机方向的优化方案](2026-08-21-baremetal-optimization-plan.md) +- [mcpp 在内核 / 嵌入式 / freestanding 方向的评估](2026-08-21-baremetal-ecosystem-assessment.md) +- [PR #455–#459 深度 review:裸机 freestanding 从「能编」到「能用」](2026-08-20-pr455-459-freestanding-review.md) +- [`mcpp pack` 生产侧与消费侧模型:整体架构 review](2026-08-20-pack-and-consumer-model-review.md) +- [openkal 0.4: what one portable program found, and what it says about the method](2026-08-20-openkal-portable-program-findings.md) +- [openkal: implementation plan and outcome](2026-08-20-openkal-implementation-plan.md) +- [openkal 设计方案:通用内核 ABI 规范](2026-08-20-openkal-design.md) +- [openkal: plan for industrial completeness](2026-08-20-openkal-completeness-plan.md) +- [openarch:接口层与多指令集后端的实现方案](2026-08-20-openarch-implementation-plan.md) +- [openarch 实现方案:arch 机制层](2026-08-20-openarch-implementation-design.md) +- [`mcpp pack` 的 `kind = "shared"` 产物带走了构建机:#460 的实测、根因与优化方案](2026-08-20-issue460-shared-library-runpath.md) +- [裸机 / 嵌入式 / 内核方向的生态定位与缺口(设计方案)](2026-08-20-freestanding-ecosystem-positioning.md) +- [freestanding 生态实施计划与依赖图](2026-08-20-freestanding-ecosystem-implementation-plan.md) +- [裸机:用户面能感受到的变化(场景 + 伪代码)](2026-08-20-baremetal-user-facing-scenarios.md) +- [裸机 / freestanding 支持 — 实施计划(2026-08-19)](2026-08-19-freestanding-baremetal-implementation-plan.md) +- [mcpp 裸机 / freestanding 支持 — 架构设计方案(2026-08-19)](2026-08-19-freestanding-baremetal-design.md) +- [裸机 / freestanding — 第三阶段:从「能跑」到「能用」](2026-08-19-baremetal-phase3-usable-plan.md) +- [裸机 / freestanding — 全生态打通实施计划(第二阶段)](2026-08-19-baremetal-ecosystem-closure-plan.md) +- [Windows 动态库分发、cl.exe 消费,与 `.ixx` 的默认支持](2026-08-18-windows-shared-library-and-module-extensions.md) +- [四项遗留的统一分析:判据挂错轴,以及 cl.exe 到底怎么办](2026-08-18-open-items-analysis-and-axis-discipline.md) +- [C++ 裸机 / freestanding:深度调研与 mcpp 路线分析(2026-08-18)](2026-08-18-freestanding-baremetal-analysis.md) +- [Windows 三条轴:落地报告(mcpp 2026.8.17.1)](2026-08-17-windows-three-axes-final-report.md) +- [库分发:`mcpp pack ` 与二进制包(2026-08-17)](2026-08-17-library-distribution-design.md) +- [mcpp 分发架构:全面分析与方案(2026-08-17)](2026-08-17-distribution-architecture-analysis-and-design.md) +- [Windows 工具链的三条轴:来源、SDK、运行时(2026-08-16)](2026-08-16-windows-toolchain-three-axes-design.md) +- [工具链架构 review:两种来源、选择与切换、构建与分发(2026-08-16)](2026-08-16-toolchain-architecture-review.md) +- [MSVC × xlings 生态打通 —— 综合报告(2026-08-16)](2026-08-16-msvc-ecosystem-final-report.md) +- [MSVC 在 xlings 生态里打通 —— 跨仓库计划、依赖与验收(2026-08-16)](2026-08-16-msvc-ecosystem-cross-repo-plan.md) +- [MSVC 纳入 mcpp 工具链体系 —— 设计 + 验证方案(2026-08-16)](2026-08-16-msvc-as-a-managed-toolchain.md) +- [xmake + clang 的 `import std`:错误消息把人指向了死路(2026-08-15)](2026-08-15-xmake-clang-import-std.md) +- [改一处实现,重编多少?—— 模块写法、编译器、与 BMI 的实际行为(2026-08-15)](2026-08-15-module-edit-granularity.md) +- [#426 #427 与 main 当前红 —— 核实与修复方案(2026-08-15)](2026-08-15-issues-426-427-analysis.md) +- [#412 #415 #416 #417 #418 #421 #422 —— 逐条核实与修复方案(2026-08-15)](2026-08-15-issues-412-422-analysis.md) +- [bench 可断续:每个测量点独立落盘,进度可算可显示(2026-08-15)](2026-08-15-bench-resumable-design.md) +- [bench 改为「本地真跑、CI 不跑」:方案与 Linux 实测计划(2026-08-14)](2026-08-14-bench-local-first-design.md) +- [mcpp 构建性能:架构层面的分析与方案(2026-08-13)](2026-08-13-build-performance-architecture.md) +- [构建性能优化:综合报告(2026-08-13)](2026-08-13-build-optimization-status.md) +- [模块化 C++ 构建性能深度分析与优化方案](2026-08-12-modular-build-performance-deep-analysis.md) +- [mcpp 冷构建深度优化方案](2026-08-12-cold-build-optimization-plan.md) +- [`bench/` 构建引擎基准套件 —— 架构与实施计划](2026-08-12-bench-suite-architecture-and-plan.md) +- [源文件角色表 与 build.mcpp 运行上限 —— 把两个硬编码变成两条声明](2026-08-11-source-kind-table-and-build-program-timeout.md) +- [`$ORIGIN` 被 SubOS farm 遮蔽 —— helloegui 运行期 undefined symbol 分析与修复方案](2026-08-11-runtime-search-origin-precedence-analysis.md) +- [`$ORIGIN` 优先级 + 共享库运行时契约 —— 实施计划](2026-08-11-origin-precedence-implementation-plan.md) +- [实施计划:运行期搜索闭包 与 binding 降级](2026-08-11-graphics-runtime-search-closure-implementation-plan.md) +- [图形栈剩下的那一半:链接期看得见、运行期看不见](2026-08-11-graphics-runtime-search-closure-and-binding-degradation.md) +- [PR #400 收尾设计方案 —— 重新判定阻塞点,并把串行收口改成并行](2026-08-10-pr400-completion-design.md) +- [图形栈全面不可用 —— 三层独立故障,和一条没有主人的依赖链](2026-08-10-graphics-stack-usability-design.md) +- [实施计划:图形栈闭合与分发档位](2026-08-10-graphics-closure-implementation-plan.md) +- [图形栈打通:一个标签、一条没人依赖的边、一个被钉住的 pin](2026-08-10-graphics-closure-and-distribution-tiers-design.md) +- [验收记录:图形栈闭合与分发档位(2026.8.10.2)](2026-08-10-graphics-closure-acceptance.md) +- [xlings × mcpp 生态契约收敛与优化设计](2026-08-09-xlings-mcpp-ecosystem-convergence-design.md) +- [PR #400 中文交接文档](2026-08-09-pr400-handoff-zh.md) +- [mcpp Template, Runtime, Graphics, and AUR Validation Ledger](2026-08-09-mcpp-template-runtime-graphics-aur-validation.md) +- [mcpp Template, Runtime, Graphics, and AUR Convergence Implementation Plan](2026-08-09-mcpp-template-runtime-graphics-aur-implementation-plan.md) +- [mcpp 模板、运行时、图形栈与 AUR 聚焦设计](2026-08-09-mcpp-template-runtime-graphics-aur-focused-design.md) +- [mcpp Open Issue 全量深度核验报告](2026-08-09-issue-triage-full-sweep.md) +- [xlings 运行时底座 —— 实施计划](2026-08-08-xlings-runtime-substrate-implementation-plan.md) +- [机器可读输出协议 —— 拆分实施计划](2026-08-08-wire-protocol-implementation-plan.md) +- [载荷版本与契约漂移:四个缺陷,一条线](2026-08-08-payload-version-and-contract-drift-design.md) +- [机器可读输出协议 —— 对 RFC #379 的核对与修正](2026-08-08-machine-readable-output-protocol-design.md) +- [Configure-Only Compile Database Implementation Plan](2026-08-08-configure-only-cdb-implementation-plan.md) +- [`build --configure-only` 与可靠 CDB 设计](2026-08-08-configure-only-cdb-design.md) +- [编译器是能力 —— 跨仓实施计划](2026-08-08-compiler-as-capability-implementation-plan.md) +- [xlings 作为 mcpp 的运行时底座:运行时身份、链接契约与环境契约](2026-08-07-xlings-as-runtime-substrate-design.md) +- [两处「模型比生态少一层」:Windows 资源输入 与 版本身份](2026-08-07-windows-resources-and-version-identity-design.md) +- [依赖提供物与构建期输入:两个缺口,同一个形状](2026-08-06-provisions-and-build-inputs.md) +- [命令长度:把「靠崩溃发现的规模上限」从架构上消掉](2026-08-06-command-length-architecture.md) +- [issue #355:依赖产出的 host 工具(codegen 工具链缺口)](2026-08-05-issue355-dependency-host-tools-design.md) +- [build.mcpp 机制架构设计:一个 hook,多种节点](2026-08-05-build-mcpp-extensibility-architecture.md) +- [`ci-fresh-install` 11 个 job 全红 —— 两个独立缺陷的修复方案](2026-08-04-ci-fresh-install-two-defects.md) +- [Windows → Linux 交叉工具链(路径 A:canadian-cross payload)— 设计方案](2026-08-03-windows-host-linux-cross-design.md) +- [issue #344:全局 build cache 的对象地址必须与消费方无关](2026-08-03-issue344-cache-object-address-design.md) +- [索引版本下限(E0006)不应让旧客户端不可用 —— 分析与方案](2026-08-03-index-floor-should-degrade-not-brick.md) +- [索引的可用性不得决定 mcpp 的可用性 —— 纯 mcpp 侧优化方案](2026-08-03-index-availability-must-not-decide-mcpp-availability.md) +- [B3 — 产物命名按 target 而非 host(交叉构建每次重链)— 修复方案](2026-08-03-b3-target-aware-artifact-naming.md) +- [Windows 可用性 — 实施计划](2026-08-02-windows-usability-implementation-plan.md) +- [Windows 可用性设计:裸机无感可用 + build.mcpp 全方言 + 测试面补齐](2026-08-02-windows-usability-design.md) +- [深度分析:issue #336(macOS 静态 libc++)与 mcpp-index PR #142(boost-ext.ut)](2026-08-02-issue336-pr142-analysis.md) +- [宿主编译单一生产者:让 build.mcpp 的能力等于 mcpp 的能力](2026-08-02-host-compile-single-producer-design.md) +- [宿主编译单一生产者 — 实施计划](2026-08-02-host-compile-implementation-plan.md) +- [issue #331 逐条核验 + Windows 无 MSVC 默认工具链分析](2026-08-01-issue331-windows-msvc-triage.md) +### 2026-07 + +- [`mcpp test --workspace`:跨平台模型一致性、输出差异与 macOS 停滞的根因分析](2026-07-31-test-workspace-observability-analysis.md) +- [`mcpp test` 可观测性与有界性 —— 实施计划](2026-07-31-test-observability-implementation-plan.md) +- [C++20 作为一等 `standard` 档位:`import std` 全平台可用性设计](2026-07-31-cpp20-standard-support-design.md) +- [C++20 档位支持 — 实施计划(单 PR,目标 2026.7.31.1)](2026-07-31-cpp20-implementation-plan.md) +- [索引刷新策略收敛:从「时间驱动」到「解析驱动」— 设计](2026-07-30-issue315-index-refresh-policy-design.md) +- [索引刷新策略收敛 — 实施计划](2026-07-30-issue315-implementation-plan.md) +- [BMI staging 原语 + BMI 缓存根收敛 — 实施计划](2026-07-30-issue311-implementation-plan.md) +- [BMI staging 原语 + BMI 缓存根收敛 — 设计](2026-07-30-issue311-bmi-staging-and-cache-root-design.md) +- [依赖构建产物的全局缓存收敛 — 设计](2026-07-30-dep-build-cache-scoping-design.md) +- [依赖构建产物的全局缓存收敛 — 实施计划](2026-07-30-dep-build-cache-implementation-plan.md) +- [日期版本号 + xlings pin 收敛 — 实施计划](2026-07-27-date-version-and-xlings-pin-plan.md) +- [日期版本号 + xlings pin 收敛 — 设计](2026-07-27-date-version-and-xlings-pin-design.md) +- [裸名 wire address 修复 — Implementation Plan(mcpp 0.0.109)](2026-07-26-bare-name-wire-address-implementation-plan.md) +- [裸名依赖的 wire address 收敛 — Design(mcpp 0.0.109)](2026-07-26-bare-name-wire-address-design.md) +- [xlings#381 索引键补齐命名空间维度 — 设计/优化方案](2026-07-25-xlings381-index-namespace-keying-design.md) +- [SPEC-001 落地 — Implementation Plan(mcpp 0.0.106)](2026-07-25-spec001-implementation-plan.md) +- [mcpp `name` / `namespace` 规范实现(定稿)](2026-07-25-name-namespace-canonical-implementation-spec.md) +- [`package.name` / `package.namespace` 双向验证报告](2026-07-25-name-namespace-bidirectional-verification-report.md) +- [#278 包身份双侧收敛 — Implementation Plan](2026-07-25-issue278-implementation-plan.md) +- [#278 包身份口径收敛 — 索引侧 + 依赖侧完整方案](2026-07-25-issue278-descriptor-name-form-canonicalization-design.md) +- [mcpp test 演进批次二 — Implementation Plan](2026-07-24-test-batch2-plan.md) +- [mcpp test 架构评估与设计方案](2026-07-24-mcpp-test-design-review.md) +- [批次三:#273 沙盒围栏 + CI wine 缓存 — Implementation Plan](2026-07-24-issue273-containment-plan.md) +- [嵌入式平台支持 — 方案设计 (Embedded Platform Support)](2026-07-24-embedded-platform-support-design.md) +- [mcpp test: per-test isolation, filter, JSON output — Implementation Plan](2026-07-23-test-isolation-json-plan.md) +- [v0.0.102 批次设计 —— #261 / #257 / #258 / #254(+#256 收尾)](2026-07-22-v0.0.102-batch-254-261-design.md) +- [索引组织迁移 + 采纳 xlings 0.4.68 per-repo artifact 来源 — 设计方案](2026-07-22-issue267-269-index-artifact-and-org-migration-design.md) +- [Issue #267/#269 索引组织迁移 + artifact 采纳 — 实施计划(0.0.103)](2026-07-22-issue267-269-impl-plan.md) +- [Issue 分析报告 —— #254 / #256 / #257 / #258 / #259 / #261](2026-07-22-issue-triage-254-261.md) +- [契约准入(Contract Admission)设计 —— 索引级 + 包级 floor](2026-07-22-contract-admission-design.md) +- [Issue #253 —— per-feature `flags` + per-OS `features` 设计](2026-07-20-issue-253-feature-flags-and-per-os-features-design.md) +- [v0.0.99 批次设计 —— #243 feature 转发 + #238 xlings 升级 + #230 windows 复验](2026-07-19-v0.0.99-feature-forwarding-238-230-design.md) +- [#233 对象路径消歧的两个后续缺口(#240 / #239)——根因分析与统一修复方案](2026-07-19-object-path-disambiguation-followups-239-240-design.md) +- [大型源码直编包全平台化:平台三修(#247/#248/#249)+ build.mcpp 构建期生成能力 + 描述符复杂度治理](2026-07-19-large-source-pkg-platform-fixes-and-buildmcpp-generation-design.md) +- [#230–#243 批次总账 + 架构评估(治理文档)](2026-07-19-issues-230-243-batch-ledger-and-architecture-assessment.md) +- [Issue #243 —— feature 依赖转发(`dep/feat`)设计](2026-07-19-issue-243-feature-forwarding-design.md) +- [mcpp 0.0.97 架构级修复 —— 实施设计与任务拆分(单 PR / 逐簇 commit)](2026-07-18-v0.0.97-architectural-remediation-implementation-plan.md) +- [Issue #215+ 分类分析与架构级修复方案](2026-07-18-issue-triage-215plus-architectural-remediation.md) +- [mcpp 通用构建能力需求清单(G1–G9)](2026-07-17-mcpp-feature-requests.md) +- [汇编源一等公民 + 通用构建能力(G1–G9)设计方案](2026-07-17-asm-sources-and-general-build-capabilities-design.md) +- [工具链 × 目标 命名统一 — 设计方案(实现 + 显示 + 使用)](2026-07-15-toolchain-target-naming-unification-design.md) +- [Linux → Windows MinGW 交叉工具链 — 设计方案](2026-07-15-mingw-linux-cross-windows-design.md) +- [c++fly:一键启用"最新标准 + 全部实验特性"(语言 + 标准库)设计](2026-07-14-std-features-experimental-gate-design.md) +- [c++fly(0.0.91)单 PR 实施计划](2026-07-14-single-pr-091-implementation-plan.md) +- [工具链后端抽象层 + MSVC 原生构建 + MinGW 生态入驻 — 设计方案](2026-07-13-toolchain-backend-abstraction-msvc-mingw-design.md) +- [0.0.90 单 PR 实施计划(post-089 路线图全量)](2026-07-13-single-pr-090-implementation-plan.md) +- [0.0.89 后路线:std 模块方言旗标一致性(#210)+ 遗留优化清单 — 设计方案](2026-07-13-post-089-roadmap-and-std-dialect-flags-design.md) +- [MSVC System-Toolchain Detection — Implementation Plan](2026-07-13-msvc-system-toolchain-implementation-plan.md) +- [MSVC System-Toolchain Detection — Design](2026-07-13-msvc-system-toolchain-detection-design.md) +- [编译器方言触点审计(0.0.88 基线)](2026-07-13-compiler-dialect-touchpoint-audit.md) +- [项目本地模式下的索引作用域:别把默认全局的官方索引(xim)注入项目组(架构分析 + 修复设计)](2026-07-09-project-index-scope-global-infra-fix.md) +- [Scanner backend abstraction: per-package opt-in, P1689 as lingua franca, plan-vs-ddi reconciliation (Design)](2026-07-08-scanner-backend-abstraction-design.md) +- [Root-cause remediation for the 0.0.85 rollout incidents (Design)](2026-07-08-root-cause-remediation-design.md) +- [Index version semantics + descriptor grammar v2 (long brackets) + single-source-of-truth lint (Design)](2026-07-08-index-version-semantics-and-descriptor-grammar-design.md) +- [Descriptor & index evolution — 0.0.85 release train roadmap (cross-repo)](2026-07-08-descriptor-index-evolution-roadmap.md) +- [Hermetic toolchain link model — one-shot cross-repo fix for issue #195](2026-07-07-hermetic-toolchain-link-model-design.md) +### 2026-06 + +- [Workspace-aware `mcpp test` + zero-shell self-contained mcpp-index (Design)](2026-06-30-workspace-test-and-zero-shell-index-design.md) +- [Bare OS-alias sugar for `[target.*]` conditional tables (Design)](2026-06-30-target-bare-alias-sugar-design.md) +- [L3 `build.mcpp` — native imperative build program (implementation design)](2026-06-30-l3-build-mcpp-implementation-design.md) +- [The `mcpp` build-module library for `build.mcpp` (Architecture & Design)](2026-06-30-build-mcpp-module-library-design.md) +- [Windows Runtime-DLL Deployment & `compat.openblas` Windows Support (Design)](2026-06-29-windows-runtime-dll-deployment-and-openblas.md) +- [mcpp.toml: Build Environment, Platform-Conditional Config, and `build.mcpp` (Design)](2026-06-29-manifest-environment-and-platform-design.md) +- [Feature System v2 — Stage 2: feature-activated optional dependencies (Design)](2026-06-29-feature-optional-dependencies-s2-design.md) +- [Feature System v2 — Capability-Oriented Model (Design)](2026-06-29-feature-capability-model-design.md) +- [分析报告:`abi:` 能力检查把「libc ABI」与「C++ stdlib」混为一谈 —— glfw 在 clang/libc++ 下误报 ABI mismatch](2026-06-27-glfw-abi-glibc-vs-libcxx-conflation-analysis.md) +- [设计:ABI/工具链兼容性「维度化」模型 —— 一步到位的单 PR 方案](2026-06-27-abi-compat-model-single-pr-design.md) +- [LLVM libatomic 自包含缺口分析与修复设计(libatomic.so.1 cannot open)](2026-06-26-llvm22-libatomic-self-containment-design.md) +- [Identity-First Package Resolution — Filename Is Not a Key](2026-06-26-identity-first-resolution-no-filename.md) +- [gtest_main 冲突修复:src 轨 feature 控制 + dev 轨 main 检测 + `mcpp add --dev`](2026-06-25-gtest-main-feature-and-add-dev-design.md) +- [依赖入口对象的条件链接(fix `mcpp test` duplicate `main`)设计方案](2026-06-25-dependency-archive-linking-design.md) +- [compile_commands.json 测试覆盖缺失:分析报告与设计方案](2026-06-25-cdb-test-coverage-design.md) +- [离线优先的索引刷新 + mcpp-index 发布机制](2026-06-24-offline-first-index-and-mcpp-index-publish.md) +- [macOS `mcpp build`:`library not found for -lSystem` 根因与修复](2026-06-24-macos-link-lsystem-sdk.md) +- [Issue #43 分诊与关闭记录 — macOS 全新安装首跑](2026-06-24-issue43-macos-first-run-triage.md) +- [Termux / Android (aarch64) 适配分析报告](2026-06-23-termux-android-adaptation.md) +- [原生 aarch64 musl-gcc 用 musl 1.2.5 重建(canadian-cross)— 复现指南](2026-06-23-aarch64-musl-gcc-canadian-cross-rebuild.md) +- [mcpp 后续修复:统一汇总 + 方案设计 + PR 拆分](2026-06-22-mcpp-followups-design.md) +- [aarch64 / Android 支持 —— 跨仓库 MVP 顶层设计方案](2026-06-22-aarch64-android-mvp-design.md) +- [跨仓库修复方案:残缺/被删依赖被 `.mcpp_ok` 盲区放过](2026-06-21-xcb-and-install-integrity-cross-repo-fix.md) +- [Package Resolution Architecture — Identity-First Locator Design](2026-06-20-package-resolution-architecture.md) +- [Runtime Launch Hygiene & Multi-Distro Coverage Implementation Plan](2026-06-19-runtime-launch-and-multi-distro-plan.md) +- [Pack Mode Redesign — Two-Axis Model, Clearer Names, `system` Mode](2026-06-19-pack-mode-redesign.md) +- [Per-Target 构建配置设计:配置发散归编译单元,目标只携本地标志](2026-06-18-per-target-build-config-design.md) +- [CLI Modularization — Architecture & Implementation Plan](2026-06-10-cli-modularization.md) +- [Library / Component Download Progress — Design](2026-06-09-library-download-progress-design.md) +- [mcpp.toml Schema 所有权设计:语法封闭 · 词汇开放](2026-06-04-manifest-schema-ownership.md) +- [mcpp core: runtime closure (rpath) + toolchain defaults](2026-06-03-runtime-closure-and-toolchain-defaults.md) +- [mcpp 模板系统(package-based templates)— 设计 v2](2026-06-03-package-templates.md) +- [mcpp: GL Runtime Closure Plan](2026-06-03-gl-runtime-closure-plan.md) +- [Module-First Usage Requirements Architecture](2026-06-02-usage-requirements-architecture.md) +- [2026-06-02 imgui mcpp dependency fixes](2026-06-02-imgui-mcpp-dependency-fixes.md) +- [Dotted Dependency Selector Architecture](2026-06-02-dotted-dependency-selectors.md) +- [C++ 标准一等配置设计](2026-06-01-cpp-standard-first-class-design.md) +- [CI 工具链缓存优化分析](2026-06-01-ci-toolchain-cache-optimization.md) +### 2026-05 + +- [Index Refresh And Dependency Cache Label Fix](2026-05-31-index-refresh-cache-labels-plan.md) +- [mcpp 0.0.35: Package-Owned Build Metadata Plan](2026-05-30-package-owned-build-flags-plan.md) +- [BMI Cache And Custom Index Build Fix](2026-05-30-bmi-cache-custom-index-fix.md) +- [mcpp build 报错输出优化方案](2026-05-26-build-error-output-optimization-plan.md) +- [设计方案:中断安装统一恢复机制](2026-05-23-interrupted-install-recovery-design.md) +- [mcpp 可观察性设计方案](2026-05-22-observability-design.md) +- [Bug 分析:LLVM 共享库 RUNPATH 失效的完整链路](2026-05-22-llvm-runpath-bug-analysis.md) +- [Fix: LLVM shared libraries have stale RUNPATH after install](2026-05-22-fix-llvm-shared-lib-runpath.md) +- [Fallback 代码提取方案 — 代码架构重构](2026-05-22-fallback-code-extraction-plan.md) +- [mcpp Fallback 架构设计方案](2026-05-22-fallback-architecture-design.md) +- [设计方案:ensure_base_init_ok + mcpp self init --force](2026-05-22-ensure-base-init-design.md) +- [分析:Ctrl+C 中断 bootstrap 后 mcpp 进入不可用状态](2026-05-22-ctrl-c-interrupted-bootstrap-analysis.md) +- [resolve_xpkg_path() 的 copy 优先级问题分析](2026-05-22-copy-priority-analysis.md) +- [设计方案:Payload-first 工具链环境管理](2026-05-21-payload-first-sysroot-design.md) +- [Linux sysroot 缺少内核头文件导致 std module 预编译失败](2026-05-21-linux-sysroot-missing-kernel-headers.md) +- [MSVC STL Discovery — msvc.cppm 模块设计](2026-05-20-msvc-stl-discovery-plan.md) +- [Windows 平台成熟度提升方案](2026-05-19-windows-platform-maturity-plan.md) +- [Windows 成熟度提升 V2 方案](2026-05-19-windows-maturity-v2-plan.md) +- [Windows E2E 与 macOS 对齐方案](2026-05-19-windows-e2e-parity-plan.md) +- [Windows Pack Design](2026-05-19-pack-windows-design.md) +- [Windows LLVM/Clang 支持设计方案](2026-05-17-windows-llvm-support-design.md) +- [mcpp](2026-05-16-readme-draft.md) +- [macOS Support Design — LLVM/Clang 自含工具链方案](2026-05-16-macos-support-design.md) +- [macOS LLVM 默认工具链跨平台适配方案](2026-05-16-macos-llvm-default-toolchain.md) +- [`[indices]` 功能增强设计方案](2026-05-16-indices-enhancement-design.md) +- [mcpp 功能特性清单](2026-05-16-feature-list.md) +- [MCPP 跨平台 Clang/LLVM 支持分析报告](2026-05-16-cross-platform-clang-analysis.md) +- [std.compat 支持 + cxx_scan restat + 增量零重编 E2E](2026-05-15-stdcompat-restat-e2e.md) +- [Fingerprint 稳定性与 Fast-Path 一致性 — 优化方案](2026-05-15-fingerprint-stability-and-fastpath-coherence.md) +- [Clang 编译管线平权 + 工具链抽象层设计](2026-05-15-clang-parity-and-toolchain-abstraction.md) +- [MCPP LLVM/Clang Toolchain Support Analysis and Design](2026-05-13-llvm-clang-toolchain-support-design.md) +- [Workspace Phase 1 Implementation Plan](2026-05-12-workspace-implementation-plan.md) +- [mcpp Workspace 设计方案](2026-05-12-workspace-design.md) +- [compile_commands.json 设计方案](2026-05-12-compile-commands-design.md) +- [mcpp 构建优化深度分析报告](2026-05-12-build-optimization-analysis.md) +- [Namespace Field Design — mcpp 0.0.6](2026-05-11-namespace-field-design.md) +- [2026-05-08 — 包索引仓库配置 (Package-Index Repo Configuration)](2026-05-08-package-index-config.md) + +## Undated + +Records written before the date prefix was the convention. + +- [Fix: xlings 包内 mcpp 的 MCPP_HOME 检测](fix-xlings-package-home-detection.md) +- [LLVM 工具链安装失败分析](llvm-install-failure-analysis.md) +- [Platform Abstraction Layer — Architecture & Implementation Plan](platform-abstraction-plan.md) +- [Remaining Platform Macros Outside src/platform/ — Analysis Report](platform-remaining-ifdefs-report.md) + +## todos/ + +Work items rather than records of a decision. + +- [aarch64-linux 生态适配分析报告](todos/2026-06-22-aarch64-linux-ecosystem-support-analysis.md) +- [aarch64 glibc-world / LLVM 工具链构建计划(deferred)](todos/2026-06-23-aarch64-glibc-world-llvm-buildout-plan.md) +- [TODO: e2e 套件按耗时分片(并行 matrix)](todos/2026-06-24-e2e-suite-sharding.md) +- [macOS 首跑遗留问题(已记录,待处理)](todos/2026-06-24-macos-first-run-remaining.md) diff --git a/.agents/skills/mcpp-docs-style/SKILL.md b/.agents/skills/mcpp-docs-style/SKILL.md index ec667d26..9f248a39 100644 --- a/.agents/skills/mcpp-docs-style/SKILL.md +++ b/.agents/skills/mcpp-docs-style/SKILL.md @@ -1,27 +1,143 @@ --- name: mcpp-docs-style -description: Use when writing or editing anything under docs/ (English or 简体中文), README files, or long-form design records — states the register mcpp documentation is written in (declarative, precise, professional), the constructions that are not admitted (question headings, conversational asides, internet slang, figurative jargon), and the bilingual parity rules. +description: Use when writing or editing anything under docs/ (English or 简体中文), docs/specs/, README files, or the design records under .agents/docs/ — states which tree a document belongs to, that docs/ is a usage manual for what mcpp has already implemented rather than a design account, the register it is written in (academic, declarative, precise, no emoji, no internet slang), the requirement that a document match the current implementation, the gradient a topic is documented along, the coverage a surface owes, and the bilingual parity rules. --- -# mcpp 文档风格规范 +# mcpp 文档规范 -## 适用范围 +本规范回答六个问题:**这份文档的归属**(第一节)、**为什么要有这一章**(第二节)、 +**怎么写**(第三至七节)、**它必须对得上什么**(第八节)、 +**它欠多少覆盖**(第十节)、**怎么评审它**(第十四节)。 -`docs/**`(含 `docs/zh/**`)、`README.md`、`.agents/docs/**` 的对外部分。 +最核心的一条在第一节:**用户文档是已实现功能的使用手册**,不是设计说明。 代码注释与 commit message **不受本规范约束** —— 它们的读者、篇幅与目的都不同, -那里允许并鼓励叙述「为什么」以及实测过程。本规范约束的是**面向用户的文档**。 +那里允许并鼓励叙述「为什么」以及实测过程。 -## 一、总原则 +## 一、文档的归属:三棵树与各自的准入判据 + +一份文档属于哪棵树,由**读者**决定,不由篇幅或主题决定。 + +| 树 | 读者 | 准入判据(问自己这一句) | 稳定性 | +|---|---|---|---| +| `docs/**` | 手上有任务的人 | 手上有这个任务的人,没有它做不完 | 增量;旧拼法留作别名 | +| `docs/specs/**` | 对着机制做实现的人:索引作者、下游工具、贡献者 | **没有它,两个独立实现会不一致** | 编号 + 版本 + 状态机 | +| `.agents/docs/**` | 做这次改动的人,以及以后问「为什么是这样」的人 | 做了一个决定,理由否则会丢 | **落地即不可变** | +| `.agents/skills/**` | 照着做的人或 agent | 这是**步骤**,不是解释 | 随流程变 | + +### 用户文档是**已实现功能的使用手册** + +这是本规范最核心的一条。 + +> `docs/**` 服务的是**要把事情做成的用户**。它写 mcpp **已经实现**的东西怎么用, +> 不写这些东西**为什么被设计成这样**,也不写**什么设计了、什么还没设计**。 + +不写进用户文档的四类内容,它们全部属于 `.agents/docs/`: + +1. **设计理由与取舍** ——「为什么是一个机制而不是两个」「这个边界由模型的性质 + 决定而不是本文档的雄心」。 +2. **被否掉的替代** ——「三种替代方案都不能去掉它」「某原语写出来又撤回了」。 +3. **路线图与设计状态** ——「计划中」「将来会支持」「已设计未实现」「下一步是」。 + 用户文档里一个能力只有两种状态:**能用**(带版本下界)与**不支持**(一句话)。 +4. **实现内幕** —— 除非用户不知道它就会用错。 + +**判据(逐段问一遍):把这一段删掉,读者还能不能正确地用?** + +- 能 → 删掉,或移进设计记录。 +- 不能 → 它不是设计论证,是**使用信息**;改写成事实陈述,去掉论证语气。 + +**边界写成事实,不写成论证。** 「当前边界」一节(第十节要求它必须存在)是一份 +清单,不是一段说理: + +| 不采用 | 采用 | +|---|---| +| OpenMP offload 与 stdpar 没有可分的岛,因此落在这套机制的论域之外 —— 这是模型的性质,而不是本文档雄心的缺口。 | 未支持:OpenMP offload、stdpar、Metal、HIP 的 AMD 平台。 | +| 之所以不发出 `accel` 字段,是因为该字段的含义是「实测所得」,而 mcpp 目前无从测量,把声明写进一个语义为实测的字段会让身份说谎。 | `mcpp pack` 不产出 `accel` 字段;需要它的发布方在描述符里手写。 | + +理由**就是不给**。要理由的读者是另一类读者,他去读设计记录 —— 而用户文档 +不链接设计记录(见上面的引用方向)。 + +**已测状态**(2026-09-08):九个用户章节含设计论证短语,`05` 21 处、`13` 8 处、 +`20` 6 处,`20` 另有两个设计型标题。清理按 +`.agents/docs/2026-09-08-documentation-architecture-three-trees.md` 的分阶段进行。 + +### 另外三条最容易被违反的推论 + +1. **用户章节不记历史。** 「过去是 X,2026.8.16 起改成 Y」是设计记录的句子。 + 章节陈述今天是什么;版本相关写**下界**(`2026.9.6.5+`)。 +2. **设计记录落地后不再编辑**,除了追加状态行或一个具名带日期的更正块。 + 就地改写会让一份「某时刻的记录」悄悄变成「对现在的断言」。 +3. **用户章节里出现「必须 / 禁止」,说明内容属于规范。** 把它移进 + `docs/specs/`,章节引用它。只有规范有规范性语气。 + +### 引用方向是规则,不是习惯 + +``` +docs/ ──▶ docs/specs/ 允许:引用精确语义 +docs/ ─╳─ .agents/docs/ 禁止 +docs/specs/ ──▶ .agents/docs/ 允许,仅限元数据表里的溯源行 +docs/specs/ ──▶ docs/ 允许 +.agents/docs/ ──▶ 任何 允许 +代码注释 ──▶ docs/ 或 specs/ 允许,且被引用的文件必须存在 +``` + +**为什么那条边被禁止**:设计记录描述一个时刻,不带稳定性承诺。用户章节想引用 +它,只说明两件事之一 —— 章节不完整,或那份记录里的内容已经变成规范性的。两者 +的修法都不是加链接,而是**把内容提升上来**(是操作就进章节,是保证就进规范)。 + +## 二、重构的定义,以及每章的设计规格 + +**重构不是在既有文档上修补,是重新设计。** 分组、重编号、把段落搬到别的章,这些 +是**重组**;它们改的是索引,不是书。重构要回答的是:每一章**为什么存在**、给谁看、 +放在哪里、按什么顺序、包含什么、传递什么信息 —— 并且把**为什么**写下来。 + +判据:拿掉某一章,读者少了哪个问题的答案?答不上来,这一章就没有被设计过。 + +### 四条产生每个决定的规则 + +| | 规则 | 排除的形态 | +|---|---|---| +| **R1** | **一个主题一个拥有者。** 恰好一章拥有一个主题;其余每一章只写一句话并链接过去 | 同一件事被解释两遍,然后各自过期 | +| **R2** | **一章为**有任务的读者**而存在,不为**有名字的机制**而存在** | 按机制建目录,于是「测试」「依赖解析」这类**任务**没有家 | +| **R3** | 每章在**前 15 行**内写明读者、它回答的那一个问题、以及它**排除**什么 | 排除是承重的:它是防止这一章重新吸收 R1 已经分配出去的主题的闸 | +| **R4** | 一个部分是**某类读者的一段弧**,其内部顺序是那类读者需要它的顺序 | 字母序、按特性发布时间排序 | +| **R5** | **背景一节的范围由问题决定,不由方案决定。** 先把读者实际面对的那个问题写完整,再写本工具触及其中的哪一部分 | 只写与本工具最近的那一条成因,读者据此以为问题就这么大 | + +### 每章的设计规格 + +动一章之前,先把这张表填出来。填不出来的那一格,就是还没设计的地方。 + +| 项 | 填写要求 | +|---|---| +| 读者 | 谁在读它。一句话说不出来就是没定位 | +| 那一个问题 | 它存在的理由,一个问句 | +| 包含 | 哪些内容归它拥有(R1) | +| **排除** | 哪些内容**不**归它,以及归谁 | +| 位置与理由 | 在哪个部分、第几位,**为什么在这里而不是别处** | +| 前后 | 上一章与下一章,以及为什么是这两章 | +| 判据 | 读完这一章的人能做到什么 | + +### 出版级的含义 + +这套规格施加于**每一章**,而不是只施加于新写的章。一份文档集合的质量由它最差的 +那一章决定,因为读者不知道哪一章是被设计过的。 + +**先设计再动手。** 先重组、再设计,会得到两次重编号和一份没有被设计过的书 —— +第二次重编号的成本,就是没有先设计的代价。 + +## 三、总原则 文档是**参考资料**,不是博客,也不是聊天记录。判据只有一条: -> 一位不认识作者、只想解决自己问题的工程师,能不能在最短时间内 -> 拿到准确的事实,并且不会误以为某个说法比实际更随意或更绝对。 +> 一位不认识作者、只想解决自己问题的工程师,能不能在最短时间内拿到准确的事实, +> 并且不会误以为某个说法比实际更随意或更绝对。 + +由此得到四条可执行的规则:**学术、陈述、精确、克制**。 -由此得到三条可执行的规则:陈述、精确、克制。 +「学术」在这里是具体的三件事,不是气质:每个断言的强度与它的证据相符(第七节)、 +每个可粘贴的东西都可复现(第八节)、每个枚举都有分母(第十节)。 -## 二、标题 +## 四、标题 **标题一律是名词短语或陈述句,不使用疑问句、不使用口语片段。** @@ -42,22 +158,49 @@ description: Use when writing or editing anything under docs/ (English or 简体 | What decides what gets packed | What determines the package contents | | Consuming one | Consuming a package | | What you may rely on, and what changes | Stability guarantees | +| 0x —— 人人都需要 | 0x —— 基础 | +| 0x — Everyone | 0x — Fundamentals | +| 背景:模块到了,工具链没跟上 | 背景:C++ 工程侧的工具现状 | +| 谁在为这个落差付账 | 这一现状的代价 | +| 大致相当于谁的活 | 可对照的工具 | +| 长什么样 | 形式 | +| 一个 flag 由哪根轴决定 | 决定一个 flag 的轴 | +| 一条运行时搜索路径可以住在哪里 | 运行时搜索路径的允许位置 | 「…的原因」「…的依据」「…的范围」是把 why 型标题转成名词短语的常用形。 **保留 why 本身,去掉疑问语气。** -## 三、词汇 +**判据是疑问词,不是问号。**「谁在为这个落差付账」「打什么由谁决定」都不带问号, +都是疑问句。检查脚本第一版只匹配 `?` / 吗 / 呢,两句全部通过。判据是这一组词: +谁、哪、什么、多少、为何、如何、怎样、怎么。 + +**表头单元格与标题同规。** 一个列头按每一条要紧的性质都是标题:它命名一个主题、 +被跳读、并且是读者扫描时看的那一行。`| 部分 | 大致相当于谁的活 |` 通过了当时 +全部的检查,而它是全树最直白的一处违规。 + +这条同样管**部分名与段位名**,不只管章节标题。「人人都需要」描述的是受众、 +是一个句子片段;「基础」是这一段**是什么**。受众写在每章开头的「读者」那一行, +不写在目录的骨架上。 + +## 五、词汇 ### 不采用的类别 -1. **网络用语与口语**:搞定、干活、坑、真香、翻车、打脸、一把梭、白给、 +1. **emoji 与装饰性符号**:✅ ❌ ⚠️ ⭐ 🎉 🚀 💡 🔥 以及同类。 + 状态用词表达:**已实现 / 部分实现 / 未实现**、**是 / 否**、 + **已验证 / 未验证**。一个符号要靠图例才能读,而词不用。 + - `docs/**`、`docs/specs/**`、`README*` 今天是**零 emoji**,保持。 + - `.agents/docs/**` 的既有记录里有四千余处(⚠ / ✅ / ⭐ / ❌)。 + **新记录不使用;既有记录不回改** —— 设计记录落地即不可变, + 为统一符号去改写历史记录,改的是它唯一的价值。 +2. **网络用语与口语**:搞定、干活、坑、真香、翻车、打脸、一把梭、白给、 凉了、炸了、神器、黑科技、敲黑板、划重点。 -2. **拟人与比喻性行话**:姊妹篇、腿(fat package 的一份产物)、travel(源码 +3. **拟人与比喻性行话**:姊妹篇、腿(fat package 的一份产物)、travel(源码 「旅行」)、picky、happy path 的中文直译。技术术语本身可以是比喻 (rpath、sysroot),但**不要新造比喻**。 -3. **填充语**:其实、说白了、简单来说、众所周知、显然、当然、值得一提的是。 +4. **填充语**:其实、说白了、简单来说、众所周知、显然、当然、值得一提的是。 如果一件事显然,就不必说;如果不显然,「显然」会让读者怀疑自己。 -4. **含糊的程度词**:很快、非常、极其、基本上、差不多。用数字或范围替代 —— +5. **含糊的程度词**:很快、非常、极其、基本上、差不多。用数字或范围替代 —— 「2.42×」「64.77s」「四个平台中的三个」。 ### 人称 @@ -75,7 +218,7 @@ description: Use when writing or editing anything under docs/ (English or 简体 `your toolchain : …` 是程序打印的原文,**逐字复现是要求,不是文风问题**。 检查脚本因此会先剔除行内代码段再判定。 -## 四、句式 +## 六、句式 - **陈述句优先。** 命令式仅用于操作步骤(「运行 `mcpp build`」)。 - **一句话一个事实。** 从句套从句的长句拆开。 @@ -84,7 +227,7 @@ description: Use when writing or editing anything under docs/ (English or 简体 - **破折号克制使用**:插入语用逗号或括号;破折号留给「随后是对前半句的 重述或收束」这一种用法。 -## 五、断言的强度必须与证据相符 +## 七、断言的强度必须与证据相符 这是本规范里最实质的一条,也是最容易违反的一条。 @@ -103,7 +246,102 @@ description: Use when writing or editing anything under docs/ (English or 简体 写「已在 Linux / macOS / Windows 验证」比写「全平台可用」更有价值, 因为前者可被检验。 -## 六、双语对照 +**「支持」有三档,分开写。** 同一个「是」可能意味着三件不同的事,合并写就是 +把最弱的一档说成最强的: + +| 档 | 含义 | +|---|---| +| 已端到端运行 | CI 或本机跑过,产物达成了断言 | +| 已安装并编译 | 组件装得上、代码编得过,没有跑过 | +| 已声明 | 描述符里有,没有装过 | + +## 八、文档必须对应当前实现 + +一份与实现脱节的文档比没有文档更坏:读者按它写出来的东西编不过,而错的是 +文档,他不知道。 + +1. **写作与核对一律读 `origin/main`,不读工作树。** 工作分支可能落后若干个 + 发布;「现在的实现是什么」只有 `origin/main` 能回答。 +2. **每一个可粘贴的东西都必须可复现**:命令、输出、报错原文、路径、版本号。 + 判据是「在当前发布版上跑一遍能不能得到这一行」。做不到就删掉,或标注版本 + 下界。**诊断信息里那行可粘贴的版本号也是承诺** —— 它会被读者原样敲进去。 +3. **改实现的 PR 同时改被它作废的文档。** 判据:这次改动触到的每一处 + `docs/` 断言都重新读一遍,而不是等下一次文档 PR。 +4. **用户章节不引源码行号。** 读者手上没有那一版源码树。引文件与符号 + (`src/pack/prebuilt.cppm` 的 `tag_check`)。规范可以引文件与符号; + 设计记录可以引行号,因为它记录的是一个时刻。 +5. **一份文档「对齐到哪个版本」必须可判定。** 规范由元数据表的「对应实现」 + 回答;章节由它写出的版本下界回答。都没有,就说明没人能判断它是不是过期的。 +6. **过期的判据不要用子串搜索。** 「grep 到这个词就算讲过了」会在有人改一次 + 措辞时静默变空转。要判断一份文档是否覆盖某个能力,读**结构化的东西** —— + 示例的 `mcpp.toml`、源码里的键表、`print_usage()` 的正文。 +7. **也不要数一个代理量。** 子串搜索的孪生形态:数**代码块个数**来判断「是不是把 + 几种做法并列了」、数**含某词的标题**来判断「哪一章拥有这个主题」。数字是真的, + 而被量的对象不是那个性质 —— **正因为数字是真的,评审很难发现**。判据要直接指向 + 性质:并列的替代由「alternatively / 也可以 / 等价写法」这类**并列标记**识别, + 主题归属由「这一节是不是在解释它」识别。 + + 本轮三次同形:用含 test 的标题数「哪几章解释测试」、用字面拼写查反查索引、用 + 代码块数量查并列替代。三次的数字都对,三次量的都不是那个性质。 + +## 九、梯度:一个主题的五级台阶,以及只链接相邻级 + +文档目录要有梯度,读者才能按自己的深度进入。同一个主题从浅到深有五级: + +| 级 | 形态 | 语气 | +|---|---|---| +| 0 入口 | 角色索引:「我想做 X」→ 读哪几章、跑哪个示例、用哪个模板 | 指路 | +| 1 教程 | 最小可跑的一份工程,从头跟到尾 | 可用第二人称 | +| 2 参考 | 按机制索引,字段完整 | 陈述,不用第二人称 | +| 3 规范 | 语义、约束、匹配规则,每条带实现状态 | RFC 2119 | +| 4 记录 | 为什么是这样,以及什么被推翻了 | 允许叙述 | + +**规则:每份文档开头用一行「相关文档:」指出它的上一级与下一级,并且只链接 +相邻级。** 参考文档向上链到教程与示例、向下链到规范;它**不直接把读者丢进设计 +记录**,那是跨两级 —— 也正是第一节那条被禁止的边。 + +梯度本身由**入口**承载,不由每份文档自报级别:`docs/README.md` 的角色索引把 +「我想做 X」映射到章节、示例与模板。一份自称「本章是第 2 级」的文档对读者没有 +用处,而一条指出上下一级的链接有。 + +一个能力的文档化按这个顺序推进,不跳级:先有一个能跑的最小形态,再有字段参考, +再在「两个实现会不一致」时抽出规范。**倒过来做会得到一份没有人验证过的规范。** + +## 十、覆盖度 + +「写了」不等于「覆盖了」。覆盖度要有分母,而**分母取自代码树,不取自文档**—— +用文档自己的列表当分母,只能证明这份文档自洽。 + +- manifest 键:取自解析点(`modules/manifest/src/`) +- 构建程序 API:取自导出名(`modules/buildmcpp/src/`) +- 命令:取自 `print_usage()` 的正文 +- 设备扩展名:取自 `modules/source-kind/src/` 的表 + +每个能力有且只有三种归宿,新增一个能力时**在同一个 PR 里回答它归哪一类**: + +| 归宿 | 判据 | +|---|---| +| **一个示例** | 它改变**工程的形状** —— 文件、manifest、或作者敲的命令 | +| **一个代码块** | 它是既有工程里的一行 | +| **一条场景条目** | 它只经由命令到达(`docs/21`) | + +两条配套要求: + +- **每份参考章节必须有「当前边界 / Current limitations」一节。** 没写边界的 + 文档等于声称自己完整。这一节不是可选的,而且**是一份事实清单,不是说理** + (写法见第一节)。 + + **教程、模型、索引与场景章节不欠这一节** —— 它们的范围由开头的「不在这里」交代, + 而它们本来就不声称覆盖一个完整的表面。 + + **写不出来就不要写。** 一节编造的边界会让检查通过而什么都没测到,那比缺这一节 + 更坏。写不出来时,把「这一章的边界尚未写出」记进设计记录,连同判据 —— 谁能说出 + 它、以及那句话要能被复现。 +- **缺口要写出来,不要留白。** 「这一项尚无示例」是一条信息;什么都不说, + 读者只能靠踩到才知道。缺口写在**它所属的那棵树**里:用户文档写「不支持 + X」,设计记录写为什么以及打算怎么办。 + +## 十一、双语对照 `docs/X.md` 与 `docs/zh/X.md` 是**同一份文档的两个版本**,不是两篇文章。 @@ -114,41 +352,204 @@ description: Use when writing or editing anything under docs/ (English or 简体 - 改动一侧时**同时改另一侧**。只改一侧会让两份文档随时间分叉, 而读者无从知道哪一份是新的。 -## 七、结构 +## 十二、结构 -- 顶部一段引言说明**这份文档回答什么问题**,以及相关文档的链接 - (用「相关文档:」,不用「姊妹篇」)。 -- 表格用于枚举与对照,散文用于因果。**不要用散文列举**。 -- 「当前边界 / Current limitations」一节是必要的,不是可选的: - 没有写出边界的文档,读者只能靠踩到才知道。 +### 每份文档的开头三行 -## 八、机器检查 +用户章节开头必须回答三件事,各一行,在前 15 行之内: -规则里可判定的那一半由 `.github/tools/check_docs_style.sh` 执行: +``` +**读者:** … 谁在读它 +**本章回答的那一个问题:** … 它存在的理由 +**不在这里:** … 它刻意排除什么,以及那些内容归谁 +``` + +第三行是承重的:**排除**是防止这一章重新吸收别处已经拥有的主题的那道闸(第二节 +的 R1:一个主题一个拥有者)。再加一行「在此之前 / 在此之后」,指出相邻级(第九节)。 + +规范另有两项硬性要求:开头一张元数据表(编号、标题、状态、版本、最后修改、 +对应实现、相关设计文档),结尾一份变更记录。 + +### 一条推荐路径写在正文,其余收进 `
` + +mcpp 在很多场景支持不止一种做法,但它有自己的设计风格与语义,因此**总有一条 +默认推荐**。正文只写那一条;其余的形态 —— 遗留拼法、逃生舱、只在某个平台成立的 +写法、为兼容保留的别名 —— 收进折叠块: + +```markdown +推荐写法。<正文,一条路径> + +
+其它形态:子表形式、旧拼法 + +… + +
+``` + +**判据:一个只读正文、不展开任何折叠块的读者,能不能不做选择就把事情做对?** +能 → 对。需要在 N 个并列的做法里自己挑一个 → 错,那是把设计决定推给了读者。 + +把一种做法降进折叠块**不表示它被弃用**。弃用要明说,并写清替代与从哪个版本起。 + +### 增量标在它自己旁边 + +一个后来才加进来的键、旗标或行为,版本下界写在**它那一行或那一段**旁边 +(`2026.9.6.5+`),不写在章节顶部,也不写成「从前是 X,后来变成 Y」的叙述 —— +后者是设计记录的句子(第一节)。 + +### 书与工具书:两种属性,两种索引 + +一套文档同时要能被**从头读**和被**反查**。这两件事不能由同一张表兼任 —— 一张按 +阅读顺序排的目录,回答不了「我手上有 `[feature-deps]`,该看哪一章」;一张按字母排 +的索引,读者从头读会不知道先读哪个。 + +所以是两种索引,各司其职: + +| 属性 | 承担它的东西 | 判据 | +|---|---|---| +| **书** | 章节的段位与部分内顺序(第九节的梯度) | 一个从头读的人不需要跳级 | +| **工具书** | **反查索引**:manifest 键 / 命令 / 概念 → 章节;以及每章开头的「不在这里」 | 一个拿着一个记号来的人,一步到位 | + +反查索引的**分母取自代码树**(第十节):参考章节里出现的每一个键,反查表里都要有 +一行。少一行,读者就会认为那个键没有文档。 + +### 优势由产物自己说明,不靠形容词 + +用户文档与示例要让读者**明显感到** mcpp 的长处,而做到这一点的方式不是形容词。 +「简洁」「好用」「强大」本身不携带信息,读者读到的是一个主张。 + +写成可验证的三样东西之一: + +| 不采用 | 采用 | +|---|---| +| 打包非常简单 | 六行 manifest,一条 `mcpp pack`,产出一个静态二进制 | +| 增量构建很快 | `Finished dev in 0.06s` | +| 不带加速器时开销很小 | 不点名加速器的构建**一个字节都不下载** | +| 依赖是可选的 | `counters` 不带 feature 出现 **0 次**,带 feature **2 次** | + +**判据:把所有形容词删掉,读者还能不能看出优势?** 能 → 对。删掉之后只剩机制 +描述 → 那份「感受」本来就只在形容词里。 + +**类比可以,对照不可以 —— 两者的区别是它服务谁。** + +| | 目的 | 归属 | +|---|---|---| +| **类比** | 让读者把新概念挂到已有认知上 | 用户文档,**可以** | +| **对照** | 主张 mcpp 在某个维度上更好 | 设计记录,用户文档**不可以** | + +「工具链管理大致相当于 rustup 在 Rust 里的角色」是类比 —— 它给一个部分定位,不作 +评价。「比 CMake 简洁」是对照 —— 它是一个主张,而用户文档不是提出主张的地方。 + +类比要**带一句免责**:上面每个工具在它自己的领域里做的都比 mcpp 多。少了这一句, +定位会被读成等价。 + +### 渐进式叙事:最短可跑 → 常见形状 → 完整表面 → 边角 + +复杂或小众的特性不从机制讲起。四段,顺序固定: + +| 段 | 内容 | 读者在这一段结束时 | +|---|---|---| +| 最短可跑 | 能跑的最小形态,连同它的真实输出 | 手上有一个跑起来的东西 | +| 常见形状 | 绝大多数工程实际会写的那一种 | 能照着改成自己的 | +| 完整表面 | 字段、旗标、取值 | 查得到 | +| 边角 | 平台差异、限制、失败形态 | 知道什么时候会撞墙 | + +**判据:读者读到第几屏时手上有一个能跑的东西?** 第一屏之后还没有,就是把机制讲在 +了可跑之前。 + +一次只加一条轴。`examples/09-heterogeneous/boundary` 是这条规则的形状:它先只讲 +边界(不需要设备),`cuda` 再加设备编译器,`multi-backend` 再加第二个后端。 + +### 两个视角都要设计 + +- **全局**:入口的角色索引、部分的划分、编号所在的段位、章节之间的顺序。 +- **局部**:每章的开头三行、每一节推荐哪一条路径、表格用于枚举而散文用于因果、 + 段落不超过约六行、代码块前有一句说明它演示什么。 + +一份全局清楚而局部混乱的文档,读者找得到却读不懂;反过来则读者读得懂却找不到。 +两者都要过第十四节的评审。 + +## 十三、机器检查 + +规则里可判定的那一部分由 `.github/tools/check_docs_style.sh` 执行: ``` bash .github/tools/check_docs_style.sh ``` -它检查三条:标题不是疑问句/口语片段;参考文档不使用第二人称; -`docs/X.md` 与 `docs/zh/X.md` 的标题结构一致(按层级序列比对, -并剔除代码块内的 `#` 注释 —— 第一版脚本把 ```sh 块里的 `# GET, never HEAD` -数成了标题,报出一个并不存在的结构分歧)。 +它今天检查三条:标题不是疑问句/口语片段;参考文档不使用第二人称; +`docs/X.md` 与 `docs/zh/X.md` 的标题结构一致(按层级序列比对,并剔除代码块内的 +`#` 注释 —— 第一版脚本把 ```sh 块里的 `# GET, never HEAD` 数成了标题,报出一个 +并不存在的结构分歧)。 + +**注意它的作用域是 `docs/*.md docs/zh/*.md`,不递归**,所以 `docs/specs/` 今天 +不在检查范围内。这是通配符的后果,不是决定;扩作用域与新增下列检查已列入 +`.agents/docs/2026-09-08-documentation-architecture-three-trees.md`:emoji、 +禁止边、被引用的 `docs/…md` 路径必须解析得到、规范双索引完整、规范元数据表与 +变更记录存在。 + +**它不检查第六、七、九节** —— 断言强度与证据是否相符、文档是否对得上当前实现、 +覆盖是否有分母,都需要读者判断,而那三条是本规范里最重要的。 +**脚本能做的事不等于规范的全部。** -**它不检查第五节** —— 断言强度与证据是否相符需要读者判断,而那是本规范里 -最重要的一条。脚本能做的事不等于规范的全部。 +## 十四、评审判据 -## 九、自检清单 +文档改动**至少评审一次**,而且**不由写它的那一遍来评审** —— 刚写完就自审,读到的 +是自己的意图而不是文本。判据:评审时只读渲染后的成文,不读 diff。 + +八个维度,每个都有一条可执行的判据,不是感觉: + +| 维度 | 判据 | +|---|---| +| **面向人群** | 一句话说出这份文档的读者是谁。说不出,就是没定位。二次判据:从入口的角色索引能不能指到它 | +| **梯度** | 它是第九节五级里的哪一级?它链接的是不是相邻级?跨级链接一律是缺陷 | +| **渐进性** | 一个从零开始的读者,能不能不跳级地到达这里 —— 前置的最小可跑形态存在吗 | +| **直观** | 只读前 15 行,能不能答出「这章讲什么、我要不要读」 | +| **覆盖度** | 分母是什么(第十节)?「当前边界」一节在不在,且是事实清单不是说理 | +| **陈述方式** | 陈述句;无第二人称(教程除外);无 emoji;每条断言的强度与证据相符;「支持」分三档 | +| **信息密度** | 随机抽三段,逐段问「删掉它读者少知道什么」。答不上来就是低密度 —— 典型是复述上一段、为强调而重复、以及把一个事实拆成三句 | +| **易读** | 表格用于枚举、散文用于因果;一句一个事实;段落不超过约六行;代码块前有一句说明它演示什么 | +| **一条推荐路径** | 只读正文、不展开任何 `
`,读者能不能不做选择就把事情做对(第十二节) | +| **增量标注** | 每个后加的键/旗标/行为,版本下界写在它自己旁边,而不是章节顶部或历史叙述 | +| **章节规格** | 第二节那张表能不能填满 —— 读者、那一个问题、包含、排除、位置与理由、前后、判据 | +| **冲击力** | 把形容词删掉,优势还看得出来吗 —— 有没有最短可跑的产物、真实输出、可数的数字 | +| **渐进性(局部)** | 读者读到第几屏手上有一个能跑的东西;是不是一次只加一条轴 | +| **可查阅** | 拿着一个 manifest 键 / 命令 / 概念,能不能一步查到章节;反查索引有没有漏行 | + +**用户文档额外一条,优先级高于以上八条**:逐段问「删掉它读者还能不能正确地用」 +(第一节)。设计理由、被否掉的替代、路线图,一律不在用户文档里。 + +评审的产出是**一份逐条的结论**,不是「看起来不错」。每个维度给出:通过 / +不通过 + 具体位置。 + +## 十五、自检清单 提交文档改动前: ``` +[ ] 这份文档属于哪棵树,判据答得上来 +[ ] 用户文档:逐段问过「删掉它读者还能不能正确地用」,设计理由/被否替代/ + 路线图都不在里面 +[ ] 用户文档:一个能力只写「能用(带版本下界)」或「不支持(一句话)」 +[ ] 「当前边界」是事实清单,不是说理 +[ ] 有多种做法时,正文只写推荐那一条,其余在 `
` 里 +[ ] 后加的键/旗标带版本下界,且标在它自己旁边 +[ ] 优势由最短可跑产物 / 真实输出 / 数字说明,不由形容词说明 +[ ] 复杂特性按「最短可跑 → 常见形状 → 完整表面 → 边角」推进 +[ ] 第二节的章节规格七格都能填出来 +[ ] 没有 docs/** → .agents/** 的引用 +[ ] 开头点明了它在梯度里的哪一级,且只链接相邻级 [ ] 标题没有疑问句、没有口语片段 -[ ] 没有网络用语、没有新造比喻 +[ ] 没有 emoji、没有网络用语、没有新造比喻 [ ] 没有第二人称(教程体除外) [ ] 每条「实测」都有数字、路径或报错原文 +[ ] 「支持」按三档分开写,没有把「已声明」写成「已运行」 [ ] 没有未经验证的全称断言 -[ ] 中英两版结构对应,代码块逐字一致 +[ ] 每个可粘贴的命令与输出都在当前发布版上复现过,或标了版本下界 +[ ] 本次实现改动作废的文档已在同一个 PR 里改掉 +[ ] 新增的能力已归入示例 / 代码块 / 场景条目三者之一 [ ] 有「当前边界」一节 +[ ] 中英两版结构对应,代码块逐字一致 [ ] `bash .github/tools/check_docs_style.sh` 通过 ``` diff --git a/.github/tools/build_examples.sh b/.github/tools/build_examples.sh index ffcec76e..1f4a3b44 100755 --- a/.github/tools/build_examples.sh +++ b/.github/tools/build_examples.sh @@ -37,6 +37,26 @@ BUILD=( # The device payloads are NOT installed: they are gated on the accelerator, # and this builds without one. examples/09-heterogeneous/multi-backend + # The island boundary with nothing on top. Its island is an ordinary C file, + # so it needs no device and no device payload -- only `mcpp:plugins` with + # `tools-island`. It is the one example that runs the arrangement the + # plugins README records as measured: a consumer importing the GENERATED + # module and linking against an implementation another compiler produced. + examples/09-heterogeneous/boundary + # Declares features rather than consuming them. The next step asserts the + # criterion that matters -- that the default build's RESOLUTION does not + # name the optional package -- which a build alone cannot show. + examples/11-features/counters + examples/11-features/greeter + # A device language the engine does not know, and the compiler for it. + # + # `toyc` is built here as an ordinary package as well as by the app as a + # host tool, and the two are not the same signal: this one fails at the + # compiler, the app's fails somewhere in `tools = [...]` / `reexport` / + # `dep_bin`, and a single line telling them apart is worth one build of a + # three-file package. + examples/12-a-new-device-language/toyc + examples/12-a-new-device-language/app ) # `key|reason`. @@ -47,6 +67,7 @@ SKIP=( "examples/07-project-subos|provisions a project-local sub-OS, which e2e 27_self_contained_home covers directly and far more cheaply" "examples/08-build-rules/rules-embed|a rule package is not a standalone build: its interface imports the bundled mcpp module, which exists only inside a consumer's build. Verified by building 08-build-rules/app, the same way mcpp-plugins verifies its own members" "examples/08-build-rules/rules-tidy|same" + "examples/12-a-new-device-language/rules-toy|a rule package is not a standalone build: its module imports the bundled mcpp module, which exists only inside a consumer's build program. Verified by building 12-a-new-device-language/app" "examples/09-heterogeneous/cuda/app|needs the CUDA payload set, and a device to run; the rule package is covered by mcpp-plugins' own CI" "examples/09-heterogeneous/hip/app|same, for the HIP payloads" "examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts" diff --git a/.github/tools/check_docs_structure.sh b/.github/tools/check_docs_structure.sh new file mode 100755 index 00000000..911ba7cf --- /dev/null +++ b/.github/tools/check_docs_structure.sh @@ -0,0 +1,336 @@ +#!/usr/bin/env bash +# check_docs_structure.sh — the rules of .agents/skills/mcpp-docs-style that are +# about WHERE a document lives rather than how its prose reads. +# +# check_docs_style.sh covers the register. This covers the architecture: +# +# 1. no user chapter cites a design record +# 2. a specification cites one only in its metadata table +# 3. every docs/*.md path named outside .agents/ resolves +# 4. every specification is listed in all three indexes +# 5. every specification has a metadata table and a change record +# 6. no emoji under docs/ or in a top-level README +# 7. the generated design-record index is current +# 8. a new design record declares its subject and status +# 9. every relative link in docs/ and examples/ resolves +# 10. a translation carries the same tables and code blocks +# 11. every chapter states its reader, its question and its exclusions +# 12. a citation naming a section lands in the chapter that contains it +# 13. every table the manifest reference documents is in the lookup index +# +# What it deliberately does NOT check: whether a chapter documents what is +# implemented, whether an assertion's strength matches its evidence, or whether +# a surface's coverage has a denominator. Those need a reader, and they are the +# three most important rules in the skill. +# +# Usage: bash .github/tools/check_docs_structure.sh +set -uo pipefail +cd "$(dirname "$0")/../.." || exit 1 + +fail=0 +bad() { echo "FAIL: $*"; fail=1; } + +# ── 1. no user chapter cites a design record ────────────────────────────── +# +# The two READMEs are exempt BY DECISION, not by accident: they are where the +# three-tree architecture is published, so they name `.agents/docs/` in order to +# say what it holds and that a chapter must not send a reader there. Every other +# file under docs/ that names it has delegated a question it should have +# answered -- see "引用方向是规则" in the skill. +ARCHITECTURE_PAGES="docs/README.md docs/zh/README.md" +for f in docs/*.md docs/zh/*.md; do + case " $ARCHITECTURE_PAGES " in *" $f "*) continue ;; esac + if grep -n '\.agents/' "$f" >/dev/null 2>&1; then + while IFS= read -r hit; do + bad "$f: a user chapter cites a design record: ${hit}" + done < <(grep -n '\.agents/' "$f") + fi +done + +# ── 2. a specification cites a design record only in its metadata table ─── +# +# Provenance belongs in the metadata row. A citation in the body is the same +# delegation rule 1 refuses, one tree over. +for f in docs/specs/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + # The metadata table is the leading block: everything before the first `##`. + body_start=$(grep -n '^## ' "$f" | head -1 | cut -d: -f1) + [ -z "$body_start" ] && body_start=1 + if awk -v s="$body_start" 'NR >= s && /\.agents\//' "$f" | grep -q .; then + bad "$f: cites a design record outside its metadata table" + fi +done + +# ── 3. every docs/*.md path named outside .agents/ resolves ─────────────── +# +# A comment in src/ naming a chapter is a citation. Six of them named chapters +# that had not existed since an earlier numbering, and nothing reported it. +while IFS= read -r p; do + [ -f "$p" ] || bad "a document names \`$p\`, which does not exist" +done < <(git grep -ohE 'docs/[0-9]{2}-[a-z0-9-]+\.md' -- ':!.agents' | sort -u) + +# ── 4. every specification is listed in all three indexes ──────────────── +for f in docs/specs/*.md; do + base="$(basename "$f")" + [ "$base" = "README.md" ] && continue + grep -q "$base" docs/README.md || bad "docs/README.md does not list docs/specs/$base" + grep -q "$base" docs/zh/README.md || bad "docs/zh/README.md does not list docs/specs/$base" + grep -q "$base" docs/specs/README.md || bad "docs/specs/README.md does not list $base" +done + +# ── 5. every specification has a metadata table and a change record ────── +# +# docs/specs/README.md has required both since the directory existed. Nothing +# checked, so the requirement held only for specs whose author read the README. +# +# Four rows are required rather than "a table", and the fourth is why: a +# specification whose implementation version is not stated cannot be judged +# stale by anyone. Two spellings are in use for it and both are accepted. +for f in docs/specs/*.md; do + base="$(basename "$f")" + [ "$base" = "README.md" ] && continue + meta="$(head -30 "$f")" + for row in '规范编号' '状态' '最后修改'; do + printf '%s' "$meta" | grep -q "$row" \ + || bad "$f: metadata table has no \`$row\` row" + done + printf '%s' "$meta" | grep -qE '对应实现|最低实现版本' \ + || bad "$f: metadata table states no implementation version, so nothing can decide whether it is stale" + grep -qE '^#{2,3} .*(变更记录|Change record|Change log)' "$f" \ + || bad "$f: no change record" +done + +# ── 6. no emoji under docs/ or in a top-level README ───────────────────── +# +# Status is a word: 已实现 / 未实现, yes / no, verified / not verified. A symbol +# needs a legend and a word does not. `.agents/docs/` is NOT checked: its +# records carry four thousand of them and are immutable once their change +# lands, so normalising them would edit documents whose value is that they are +# not edited. New records follow the rule by review. +EMOJI='[✅❌⚠⭐🎉🚀💡🔥👍✨📦🔧]' +for f in docs/*.md docs/zh/*.md docs/specs/*.md README.md README.zh-CN.md; do + [ -f "$f" ] || continue + if grep -nP "$EMOJI" "$f" >/dev/null 2>&1; then + while IFS= read -r hit; do + bad "$f: emoji — state the status as a word: ${hit%%:*}: $(echo "$hit" | cut -d: -f2- | cut -c1-60)" + done < <(grep -nP "$EMOJI" "$f") + fi +done + +# ── 7. the design-record index is current ──────────────────────────────── +# +# 269 records and the index was one heading. It is generated now, so it cannot +# drift -- and a generated file that is checked in must be compared against the +# generator or it drifts anyway. +python3 .github/tools/gen_agents_index.py --check || fail=1 + +# ── 8. a new design record declares its subject and status ─────────────── +# +# From the date the convention starts. The 268 records that predate it are not +# rewritten: a record describes the moment its change was made, and a pass that +# added a field nobody chose would edit documents whose value is that they are +# not edited. +CONVENTION_FROM="2026-09-08" +for f in .agents/docs/[0-9]*.md; do + [ -f "$f" ] || continue + d="$(basename "$f" | cut -c1-10)" + [[ "$d" < "$CONVENTION_FROM" ]] && continue + head -1 "$f" | grep -q '^---$' \ + || { bad "$f: a record dated $CONVENTION_FROM or later has no front matter"; continue; } + fmblock="$(awk 'NR>1 && /^---$/ {exit} NR>1' "$f")" + printf '%s' "$fmblock" | grep -qE '^subject: *[a-z]' \ + || bad "$f: front matter declares no \`subject\`" + printf '%s' "$fmblock" | grep -qE '^status: *(active|landed|superseded|abandoned) *$' \ + || bad "$f: front matter declares no valid \`status\` (active | landed | superseded | abandoned)" +done + +# ── 9. every relative link in docs/ and examples/ resolves, fragment included ─ +# +# Rule 3 catches `docs/NN-*.md` named anywhere, including from source comments. +# This is the other half: a Markdown link in a document that points at a file +# which is not there. Both halves are needed -- a chapter moved in this batch +# would satisfy one and break the other. +# +# THE FRAGMENT IS PART OF THE LINK. The first version of this rule discarded +# it (`(?:#[^)]*)?`), so a link to a section that had been renamed resolved to +# the file and was reported correct. Renaming 100 headings for register in one +# batch is exactly the change that produces those, and two hand-written anchors +# in chapter 30 were already wrong before the renames began. +python3 - <<'PYCHECK' || fail=1 +import re, pathlib, sys, unicodedata + +def slug(heading): + """GitHub's heading slug: lowercase, drop punctuation and symbols, spaces to hyphens. + + Category P* and S* covers what github-slugger removes -- ASCII punctuation, + the em dash, the backticks around inline code, and the full-width comma and + colon the Chinese chapters use -- while `-` and `_` are kept because an + anchor is allowed to contain them. + """ + t = re.sub(r"^#+\s+", "", heading).strip().lower() + keep = [] + for ch in t: + if ch in "-_": + keep.append(ch) + elif ch.isspace(): + keep.append(" ") + elif unicodedata.category(ch)[0] in "PS": + continue + else: + keep.append(ch) + return "".join(keep).replace(" ", "-") + +def anchors_of(path): + """Every anchor the file defines, with GitHub's -1/-2 suffix for repeats.""" + seen, out, infence = {}, set(), False + for line in path.read_text(errors="ignore").splitlines(): + if line.startswith("```"): + infence = not infence + continue + if infence or not re.match(r"^#{1,6} ", line): + continue + s = slug(line) + n = seen.get(s, 0) + seen[s] = n + 1 + out.add(s if n == 0 else f"{s}-{n}") + return out + +files = list(pathlib.Path("docs").rglob("*.md")) + list(pathlib.Path("examples").rglob("*.md")) +cache = {} +bad = 0 +for f in files: + for m in re.finditer(r"\]\(([^)\s]*?)(?:#([^)\s]+))?\)", f.read_text(errors="ignore")): + t, frag = m.group(1), m.group(2) + if t.startswith(("http", "mailto:")): + continue + target = (f.parent / t) if t else f + if t and not target.exists(): + print(f"FAIL: {f}: link to `{t}` does not resolve") + bad += 1 + continue + if not frag or target.suffix != ".md" or not target.is_file(): + continue + key = target.resolve() + if key not in cache: + cache[key] = anchors_of(target) + if frag not in cache[key]: + print(f"FAIL: {f}: `#{frag}` is not a heading in {target.as_posix()}") + bad += 1 +sys.exit(1 if bad else 0) +PYCHECK + +# ── 10. a translation carries the same tables and code blocks ──────────── +# +# check_docs_style.sh compares HEADING STRUCTURE, which is what catches a page +# that has fallen a section behind. It does not see a table row or a code block +# that never made it across, and two of those were sitting in the tree: the +# 简体中文 `[features]` section had no body at all, and 简体中文 §2.11 was +# missing the `identity` verdict table. Both predate this check and both are +# invisible to every other one. +python3 - <<'PYPARITY' || fail=1 +import pathlib, sys, re +bad = 0 +for en in sorted(pathlib.Path("docs").glob("*.md")): + zh = pathlib.Path("docs/zh") / en.name + if not zh.exists(): + continue + def count(f): + rows = blocks = 0 + infence = False + for line in f.read_text(errors="ignore").split("\n"): + if line.startswith("```"): + if not infence: + blocks += 1 + infence = not infence + continue + if infence: + continue + if line.startswith("|"): + rows += 1 + return rows, blocks + er, eb = count(en) + zr, zb = count(zh) + if er != zr: + print(f"FAIL: {en.name}: {er} table rows in English, {zr} in 简体中文") + bad += 1 + if eb != zb: + print(f"FAIL: {en.name}: {eb} code blocks in English, {zb} in 简体中文") + bad += 1 +sys.exit(1 if bad else 0) +PYPARITY + +# ── 11. every chapter states its reader and its question ───────────────── +# +# `.agents/skills/mcpp-docs-style` R3: reader, the one question, and the +# EXCLUSIONS, in the first fifteen lines. The exclusions are the load-bearing +# half -- they are the gate that stops a chapter re-absorbing a topic another +# chapter owns. Five of 24 chapters had this before the design; a rule nothing +# checks is a rule that decays back to five. +# Chapter 00 is exempt: it is the book's front door, and a metadata block is a +# reference-chapter device. It has no "not here" to declare, because everything +# else IS elsewhere -- which is what its closing paragraph says instead. +for f in docs/[0-9]*.md docs/zh/[0-9]*.md; do + [ -f "$f" ] || continue + case "$(basename "$f")" in 00-*) continue ;; esac + head -18 "$f" | grep -qE '^\*\*(Reader|读者)' \ + || bad "$f: no designed opening — the first lines must name the reader" + head -18 "$f" | grep -qE '(question this chapter answers|本章回答的那一个问题)' \ + || bad "$f: the opening names no question" + head -22 "$f" | grep -qE '^\*\*(Not here|不在这里)' \ + || bad "$f: the opening states no exclusions" +done + +# ── 12. a citation that names a section lands in the chapter that has it ─ +# +# `See *One package, one version* in [04](04-mcpp-toml.md)` survived a split +# that moved the section to chapter 23, in five places and two languages. Rule 3 +# could not see it -- the path resolved; it was the wrong chapter. A citation +# that names a section by TITLE is checkable against that chapter's headings. +python3 - <<'PYCITE' || fail=1 +import re, pathlib, sys +EN = re.compile(r"See \*([^*]{3,60})\* in \[\d{2}[^\]]*\]\((\d{2}-[a-z0-9-]+)\.md\)") +ZH = re.compile(r"见\s*\[\d{2}[^\]]*\]\((\d{2}-[a-z0-9-]+)\.md\)\s*的\*([^*]{2,40})\*") +bad = 0 +for f in list(pathlib.Path("docs").glob("[0-9]*.md")) + list(pathlib.Path("docs/zh").glob("[0-9]*.md")): + text = f.read_text(errors="ignore") + for m in EN.finditer(text): + title, chap = m.group(1), m.group(2) + t = (f.parent / f"{chap}.md") + if not t.exists() or title.lower() not in t.read_text(errors="ignore").lower(): + print(f"FAIL: {f}: cites *{title}* in {chap}, which does not contain it"); bad += 1 + for m in ZH.finditer(text): + chap, title = m.group(1), m.group(2) + t = (f.parent / f"{chap}.md") + if not t.exists() or title not in t.read_text(errors="ignore"): + print(f"FAIL: {f}: cites *{title}* in {chap}, which does not contain it"); bad += 1 +sys.exit(1 if bad else 0) +PYCITE + +# ── 13. every table the manifest reference documents is in the lookup index ─ +# +# docs/README.md carries a reverse index -- a key in front of a reader to the +# chapter that owns it -- and it is hand-built. A key added to the reference and +# not indexed is invisible: the reader concludes it is undocumented. The +# denominator is the reference chapter's own `###` headings, and the comparison +# is on the KEY NAME rather than on its spelling, because the two documents +# legitimately write `[targets.]` and `[targets.]`. +python3 - <<'PYLOOKUP' || fail=1 +import re, pathlib, sys +ref = pathlib.Path("docs/04-mcpp-toml.md").read_text(errors="ignore") +idx = pathlib.Path("docs/README.md").read_text(errors="ignore") +keys = set() +for m in re.finditer(r"^### [0-9.b]+ `([^`]+)`", ref, re.M): + tok = m.group(1) + name = re.sub(r"^\[|\].*$", "", tok) # [targets.] -> targets. + name = name.split(".")[0].split(" ")[-1] # -> targets ; "[package] platforms" -> platforms + keys.add(name) +missing = sorted(k for k in keys if k not in idx) +for k in missing: + print(f"FAIL: docs/README.md lookup index does not mention `{k}`, which docs/04 documents") +sys.exit(1 if missing else 0) +PYLOOKUP + +if [[ "$fail" -eq 0 ]]; then + echo "OK: docs structure checks pass" +fi +exit "$fail" diff --git a/.github/tools/check_docs_style.sh b/.github/tools/check_docs_style.sh index 56e220ff..5062115e 100755 --- a/.github/tools/check_docs_style.sh +++ b/.github/tools/check_docs_style.sh @@ -8,6 +8,13 @@ # 2. reference docs do not address the reader in the second person # (tutorials do — they are listed below, not inferred) # 3. docs/X.md and docs/zh/X.md have the same heading structure +# 4. a table's header cells follow the same register as a heading +# +# Rule 4 exists because rule 1 read only lines beginning with `#`, and a column +# header is a heading by every property that matters: it names a topic, it is +# read out of order, and it is what a reader scans. `| 部分 | 大致相当于谁的活 |` +# passed every check in this file while being the plainest register violation +# in the tree. # # What it deliberately does NOT check: whether a claim's strength matches its # evidence. That is the most important rule in the skill and it needs a reader. @@ -20,7 +27,7 @@ fail=0 bad() { echo "FAIL: $*"; fail=1; } # Tutorials address the reader on purpose: the reader is following along. -TUTORIALS="00-getting-started.md 01-examples.md 04-build-from-source.md" +TUTORIALS="01-getting-started.md 03-examples.md 90-build-from-source.md" # Headings outside fenced code blocks. `# …` inside a ```sh block is a shell # comment, and counting it made the first version of this script report a @@ -32,23 +39,80 @@ headings() { ' "$1" } -for f in docs/*.md docs/zh/*.md; do +# THE REGISTER RULES COVER docs/specs/ TOO, AND USED NOT TO. +# +# The glob was `docs/*.md docs/zh/*.md`, which does not descend, so the four +# specifications were exempt from rules 1 and 2 by accident rather than by +# decision. They are in scope now. The PARITY loop below still is not: the +# specifications are 简体中文 with no docs/zh/ mirror, and whether that changes +# is an open question rather than a defect this script should assert. +for f in docs/*.md docs/zh/*.md docs/specs/*.md; do base="$(basename "$f")" # ── 1. heading register ─────────────────────────────────────────────── + # + # An interrogative WORD is the test, not a question mark. `谁在为这个落差付账` + # and `打什么由谁决定` carry no `?` and are questions all the same, and the + # first version of this rule matched `?`/`吗`/`呢` and passed both. + # + # THE ONE EXEMPTION IS BY NAME. `# 00 —— mcpp 是什么` mirrors the English + # `What mcpp Is`, which is a noun clause rather than a question, and a chapter + # title is the chapter's registered name: it appears in three indexes and in + # every citation of the chapter. Exempting it here states the decision; + # narrowing the rule to `##` and deeper would have hidden it. while IFS= read -r h; do - case "$h" in - *"?"*|*"吗"*|*"呢"*) + if [[ "$f" == "docs/zh/00-what-mcpp-is.md" && "$h" == "# 00 —— mcpp 是什么" ]]; then + continue + fi + # Inline code is quoted material: `cxx_stdlib` and `mcpp.why.toolchain` + # must not be read for register. + bare="$(sed -E 's/`[^`]*`//g' <<< "$h")" + case "$bare" in + *"?"*|*"?"*|*"吗"*|*"呢"*) bad "$f: question heading — use a noun phrase: $h" ;; + *"谁"*|*"哪"*|*"如何"*|*"为何"*|*"什么"*|*"多少"*|*"怎样"*|*"怎么"*) + bad "$f: interrogative heading — use a noun phrase (…的原因 / …的依据 / …的范围): $h" ;; esac - case "$h" in - *"一段话"*|*"讲完"*|*"姊妹篇"*|*"干活"*|*"怎么"*|*"会怎样"*|*"不许"*) + case "$bare" in + *"一段话"*|*"讲完"*|*"姊妹篇"*|*"干活"*|*"会怎样"*|*"不许"*|*"跟上"*|*"付账"*|*"长什么样"*) bad "$f: conversational heading: $h" ;; *"The whole idea"*|*"in one paragraph"*|*"Consuming one"*|*"the thing that"*) bad "$f: conversational heading: $h" ;; esac done < <(headings "$f") + # ── 4. table header register ────────────────────────────────────────── + # + # A header row is the row directly above the `|---|---|` separator, so the + # separator is what identifies it; matching every `|` line would read data. + while IFS= read -r cell; do + case "$cell" in + *"?"*|*"?"*|*"吗"*|*"呢"*) + bad "$f: question in a table header — use a noun phrase: $cell" ;; + *"谁"*|*"哪"*|*"如何"*|*"为何"*|*"什么"*|*"多少"*|*"怎样"*|*"怎么"*) + bad "$f: interrogative table header — use a noun phrase: $cell" ;; + esac + shopt -s nocasematch + if [[ "$cell" =~ ^(what|which|who|how|why|where|whether)([[:space:]]|$) ]]; then + bad "$f: interrogative table header — use a noun phrase: $cell" + fi + shopt -u nocasematch + done < <(awk ' + /^```/ { infence = !infence; prev = ""; next } + infence { next } + /^\|[[:space:]:|-]+\|[[:space:]]*$/ && prev ~ /^\|/ { + line = prev + gsub(/`[^`]*`/, "", line) # quoted material, as above + n = split(line, cells, "|") + for (i = 2; i < n; i++) { + c = cells[i] + gsub(/^[[:space:]]+|[[:space:]]+$/, "", c) + if (c != "") print c + } + } + { prev = $0 } + ' "$f") + # ── 2. second person in reference docs ──────────────────────────────── case " $TUTORIALS " in *" $base "*) ;; diff --git a/.github/tools/check_version_pins.sh b/.github/tools/check_version_pins.sh index 0f7316df..aba1bcfc 100755 --- a/.github/tools/check_version_pins.sh +++ b/.github/tools/check_version_pins.sh @@ -136,7 +136,7 @@ fi # (b) The version BOOTSTRAPPED FROM (.xlings.json) names a mcpp that is already # published, and is NOT required to equal the version being built. It # deliberately lags, and is bumped in a separate commit AFTER the release -# exists in xim-pkgindex (see docs/09-release.md). Requiring equality here is +# exists in xim-pkgindex (see docs/92-release.md). Requiring equality here is # what an earlier revision of this script got wrong: it sent CI to install a # version that did not exist yet, and every job died with # `package 'mcpp@' not found`. diff --git a/.github/tools/gen_agents_index.py b/.github/tools/gen_agents_index.py new file mode 100644 index 00000000..00fc3396 --- /dev/null +++ b/.github/tools/gen_agents_index.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Generate .agents/docs/README.md from the records themselves. + +The design tree holds 268 records and its index was one heading. This writes an +index that cannot drift, because `check_docs_structure.sh` compares the file on +disk against what this produces. + +WHAT IT DERIVES AND WHAT IT READS. + +The date comes from the filename, the title from the first `#` heading, and both +are mechanical. `subject` and `status` are read from YAML front matter when a +record declares it, and are shown only then -- a subject inferred from filename +keywords would misfile records, which is worse for a reader than no grouping. +New records declare it; the 268 that predate the convention are listed by date +with their titles, which is what they can support without anyone rewriting them. + +Usage: + python3 .github/tools/gen_agents_index.py # write + python3 .github/tools/gen_agents_index.py --check # exit 1 if stale +""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +DOCS = ROOT / ".agents" / "docs" +INDEX = DOCS / "README.md" + +DATED = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-(.+)\.md$") +STATUSES = {"active", "landed", "superseded", "abandoned"} + + +def front_matter(text): + """Return the YAML-ish front matter as a dict, or {} when there is none.""" + if not text.startswith("---\n"): + return {} + end = text.find("\n---", 4) + if end < 0: + return {} + out = {} + for line in text[4:end].split("\n"): + if ":" not in line or line.startswith(" "): + continue + k, v = line.split(":", 1) + out[k.strip()] = v.strip() + return out + + +def title_of(text, fallback): + for line in text.split("\n"): + if line.startswith("# "): + return line[2:].strip() + return fallback + + +def collect(): + dated, undated, todos = [], [], [] + for p in sorted(DOCS.rglob("*.md")): + if p == INDEX: + continue + rel = p.relative_to(DOCS).as_posix() + text = p.read_text(errors="ignore") + fm = front_matter(text) + entry = { + "rel": rel, + "title": title_of(text, p.stem), + "status": fm.get("status", ""), + "subject": fm.get("subject", ""), + "superseded_by": fm.get("superseded_by", ""), + } + m = DATED.match(p.name) + if rel.startswith("todos/"): + todos.append(entry) + elif m: + entry["date"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" + dated.append(entry) + else: + undated.append(entry) + dated.sort(key=lambda e: (e["date"], e["rel"]), reverse=True) + return dated, undated, todos + + +def row(e): + note = "" + if e["status"]: + note = f" — {e['status']}" + if e["status"] == "superseded" and e["superseded_by"]: + note += f" by [{e['superseded_by']}]({e['superseded_by']})" + return f"- [{e['title']}]({e['rel']}){note}" + + +def render(): + dated, undated, todos = collect() + out = [ + "# Design records", + "", + "The reasoning behind changes to mcpp: what was measured, what was decided,", + "and what a later measurement refuted. A record describes the moment its", + "change was made and is not edited afterwards, so **nothing here is a", + "statement about the present**. What mcpp does today is in", + "[docs/](../../docs/README.md); what is guaranteed is in", + "[docs/specs/](../../docs/specs/README.md).", + "", + "**This file is generated** by `.github/tools/gen_agents_index.py` and is", + "checked in CI. A new record declares front matter:", + "", + "```yaml", + "---", + "subject: heterogeneous # a short, reused word", + "status: landed # active | landed | superseded | abandoned", + "superseded_by: 2026-09-07-....md # when status is superseded", + "---", + "```", + "", + f"{len(dated) + len(undated) + len(todos)} records.", + "", + ] + + by_subject = {} + for e in dated + undated + todos: + if e["subject"]: + by_subject.setdefault(e["subject"], []).append(e) + if by_subject: + out += ["## By subject", "", + "Records that declare one. Everything else is listed by date below.", ""] + for subject in sorted(by_subject): + out.append(f"### {subject}") + out.append("") + out += [row(e) for e in by_subject[subject]] + out.append("") + + out += ["## By date", ""] + month = None + for e in dated: + m = e["date"][:7] + if m != month: + month = m + out += [f"### {month}", ""] + out.append(row(e)) + out.append("") + + if undated: + out += ["## Undated", "", + "Records written before the date prefix was the convention.", ""] + out += [row(e) for e in undated] + out.append("") + + if todos: + out += ["## todos/", "", + "Work items rather than records of a decision.", ""] + out += [row(e) for e in todos] + out.append("") + + return "\n".join(out) + + +def main(): + want = render() + if "--check" in sys.argv: + have = INDEX.read_text() if INDEX.exists() else "" + if have != want: + print("FAIL: .agents/docs/README.md is stale — run " + "`python3 .github/tools/gen_agents_index.py`") + return 1 + print("OK: .agents/docs index is current") + return 0 + INDEX.write_text(want) + print(f"wrote {INDEX.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml index abb0256d..78192120 100644 --- a/.github/workflows/ci-fresh-install.yml +++ b/.github/workflows/ci-fresh-install.yml @@ -55,7 +55,7 @@ concurrency: # NOT to be confused with the .xlings.json workspace pin, which this used to be # kept equal to. That one is the BOOTSTRAP compiler for the self-host builds and # has a different requirement — it must be a released mcpp that can build the -# CURRENT source tree — so it stays hand-maintained. See docs/09-release.md. +# CURRENT source tree — so it stays hand-maintained. See docs/92-release.md. jobs: # ────────────────────────────────────────────────────────────────── diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index c87d5c45..1f5a74c4 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -101,6 +101,13 @@ jobs: - name: Check documentation style and bilingual parity run: bash .github/tools/check_docs_style.sh + # The architecture half: which tree a document lives in, whether every + # path it names resolves, and whether each specification is reachable + # from all three indexes. Same placement and the same reason -- it needs + # no toolchain, and what it catches is invisible to every other job. + - name: Check documentation structure + run: bash .github/tools/check_docs_structure.sh + - uses: ./.github/actions/bootstrap-mcpp - name: Configure mirror + Build mcpp from source (self-host) @@ -285,6 +292,117 @@ jobs: export MCPP_VENDORED_XLINGS="$XLINGS_BIN" bash .github/tools/build_examples.sh + # TWO CRITERIA A BUILD CANNOT SHOW, from the examples whose READMEs state + # them. Both are the shape this repository has paid for before: a green + # build over a result nobody compared. + - name: "examples: the feature criterion and the device language" + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + set -e + + # 11-features. NOT "the default build works" -- that passes while the + # optional package is resolved and merely unused. The criterion is + # that the default build's RESOLUTION does not name it. + cd examples/11-features/greeter + "$MCPP" build 2>&1 | tee /tmp/f-default.log + "$MCPP" build --features metrics 2>&1 | tee /tmp/f-metrics.log + grep -qi counters /tmp/f-default.log && { + echo "FAIL: the default build resolved 'counters'"; exit 1; } + grep -qi counters /tmp/f-metrics.log || { + echo "FAIL: --features metrics did not resolve 'counters'"; exit 1; } + echo "ok: the optional package is absent without its feature" + cd - >/dev/null + + # 12-a-new-device-language. Three criteria, and a probe run exits + # non-zero on purpose -- the program returns 1 when the answer is not + # 42 -- so those runs are guarded with `|| true` and judged by their + # output rather than their status. + cd examples/12-a-new-device-language/app + "$MCPP" run | tee /tmp/toy.log + grep -q 'answer() = 42' /tmp/toy.log || { + echo "FAIL: the .toy did not reach the link"; exit 1; } + # THE LANGUAGE IS EXECUTED, NOT PATTERN-MATCHED: 21 is what the + # emitted `while` loop computes, and no constant in the tree holds it. + grep -q 'gcd(1071, 462) = 21' /tmp/toy.log || { + echo "FAIL: the emitted loop did not run"; exit 1; } + + # Editing the .toy reaches the artifact. + sed -i 's/scale(gcd(1071, 462), 2)/scale(gcd(1071, 462), 3)/' src/kernels/answer.toy + "$MCPP" run > /tmp/toy2.log 2>&1 || true + sed -i 's/scale(gcd(1071, 462), 3)/scale(gcd(1071, 462), 2)/' src/kernels/answer.toy + grep -q 'answer() = 63' /tmp/toy2.log || { + cat /tmp/toy2.log; echo "FAIL: editing the .toy did not reach the artifact"; exit 1; } + + # THE COMPILER IS A DECLARED INPUT OF THE ACTION -- the half that + # fails silently, and the one criterion here that has to be built to + # isolate it. Bumping the tool's version is NOT that criterion: the + # tool's path is on the action's command line, so a new path re-runs + # the edge whether or not it is also declared as an input. Measured: + # with `a.input(compiler)` removed, a version bump still reached the + # artifact, and this step passed. + # + # The isolating change is different BYTES AT THE SAME PATH. Build a + # compiler that behaves differently, overwrite the cached binary in + # place, and the command line is byte-identical. + # + # BOTH DIRECTIONS ARE THE CRITERION, not either one. Falsified by + # removing `a.input(compiler)`: the artifact followed the first + # overwrite anyway and stopped following the restore, so a check that + # asserted only the first direction would have passed on a rule that + # tracks nothing. + # + # `n.value * 2` keeps `0` at `0`: a probe that changed every literal + # would turn `while (b != 0)` into a loop that divides by zero, and + # this step would report a crash rather than an answer. + probe_on() { sed -i 's/return std::format("{}", n.value);/return std::format("{}", n.value * 2);/' ../toyc/src/compile.cppm; } + probe_off() { sed -i 's/return std::format("{}", n.value \* 2);/return std::format("{}", n.value);/' ../toyc/src/compile.cppm; } + install_toyc() { + ( cd ../toyc && "$MCPP" build >/dev/null ) + cp "$(find ../toyc/target -name toyc -type f -perm -u+x | head -1)" "$1" + } + + # `mcpp cache dir` prints a legacy-directory note on a second line. + store="$("$MCPP" cache dir | head -1)/tool" + cached="$(find "$store" -path '*toyc@0.1.0*/bin/toyc' | head -1)" + [ -n "$cached" ] || { + echo "FAIL: no toyc in the tool store under $store"; exit 1; } + + probe_on; install_toyc "$cached"; probe_off + "$MCPP" run > /tmp/toy3.log 2>&1 || true + grep -q 'answer() = 168' /tmp/toy3.log || { + cat /tmp/toy3.log + echo "FAIL: a changed compiler binary did not reach the artifact." + echo " rules-toy must declare the compiler among the action's inputs." + exit 1; } + install_toyc "$cached" + "$MCPP" run > /tmp/toy4.log 2>&1 || true + grep -q 'answer() = 42' /tmp/toy4.log || { + cat /tmp/toy4.log + echo "FAIL: restoring the compiler binary did not reach the artifact --" + echo " the action is not tracking the compiler's bytes." + echo " rules-toy must declare the compiler among the action's inputs." + exit 1; } + + # THE STORE HOLDS NO SOURCE CONTENT, which the example's README and + # docs/30 both state. Editing the compiler's sources at the same + # version changes nothing, because nothing rebuilds the tool. + probe_on + "$MCPP" run > /tmp/toy5.log 2>&1 || true + grep -q 'answer() = 42' /tmp/toy5.log || { + cat /tmp/toy5.log + echo "FAIL: the tool store now sees source content." + echo " The example's README and docs/30 state that it does not; update them." + exit 1; } + + # ... and bumping the version is the way out both of them offer. + sed -i 's/^version = "0.1.0"/version = "0.1.1"/' ../toyc/mcpp.toml + "$MCPP" run > /tmp/toy6.log 2>&1 || true + sed -i 's/^version = "0.1.1"/version = "0.1.0"/' ../toyc/mcpp.toml + probe_off + grep -q 'answer() = 168' /tmp/toy6.log || { + cat /tmp/toy6.log; echo "FAIL: bumping the tool version did not rebuild it"; exit 1; } + echo "ok: the compiler is a declared input, and the store is keyed on the version" + - name: "Graphics example: render offscreen on lavapipe and assert the pixels" run: | export MCPP_VENDORED_XLINGS="$XLINGS_BIN" diff --git a/CHANGELOG.md b/CHANGELOG.md index 86676deb..cf50f479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -596,7 +596,7 @@ e2e 616 是双向的,而单向的那一半不值得跑:「列出的路径都存 通过,「盘上的示例都被列出」在一份把一半链接指错地方的文档上通过。两条合起来才是那条性质。 它当场抓到一个既有缺口:`examples/04-workspace`、`05-lib-distribution`、 -`06-openkal-cross`、`07-project-subos` 都在盘上,而中英两份 `docs/01-examples.md` +`06-openkal-cross`、`07-project-subos` 都在盘上,而中英两份 `docs/03-examples.md` 一个都没列。现在列上了。 这是结构判据,它自己也这么说:它不构建任何东西,所以回答不了「这个示例还能不能跑」。 @@ -677,7 +677,7 @@ C++20 modules 的第二个编译器(icpx,或带 SYCL 前端的 clang)。 配套的规则包在 `mcpp:plugins` 0.2.0:`mcpp.rules.sycl` 驱动 `xim:dpcpp` 载荷, `mcpp.rules.hip` 在 NVIDIA 平台上把 HIP 当作 CUDA 运行时之上的一层头文件, `mcpp.rules.spirv` 增加 `glslc` 路线(`xim:shaderc` 使它从一句声明变成一条路线)。 -文档见 `docs/20-heterogeneous-builds.md` 的「lanes」一节。 +文档见 `docs/42-heterogeneous-builds.md` 的「lanes」一节。 ### `mcpp clean --stale`:只清 target/ 里已无构建使用的指纹目录 (#565) @@ -779,7 +779,7 @@ e2e 610 分别断言这五条(含 `mcpp` 命名空间零告警、其他命名空 文件的相反的话。 表因此扩到「由另一个编译器消费的语言」:CUDA 与 HIP,GLSL 的各个 stage 与无 stage 的 -`.glsl`,HLSL,OpenCL C,Metal。共 18 个扩展名,完整清单见 `docs/20-heterogeneous-builds.md`。 +`.glsl`,HLSL,OpenCL C,Metal。共 18 个扩展名,完整清单见 `docs/42-heterogeneous-builds.md`。 这次扩表不能改变任何今天可用的构建,理由有两条且互相独立:设备扩展名**本来就不在** 默认 source glob 里,所以没有 glob 变宽;而这些扩展名今天在 `sources` 里是**硬错误**, @@ -814,7 +814,7 @@ e2e 610 分别断言这五条(含 `mcpp` 命名空间零告警、其他命名空 ### 文档 -- 第 20 章由「加速器」更名为「异构硬件构建」(`docs/20-heterogeneous-builds.md`),副题指明 +- 第 20 章由「加速器」更名为「异构硬件构建」(`docs/42-heterogeneous-builds.md`),副题指明 GPU 与 AI 加速器目标以及宿主/设备混合编译;`accel` 键不变。 - 第 5 章与第 7 章(中英)补入 feature 选择的规则集合与 `mcpp.rules.*` / `mcpp.tools.*` 命名;第 7 章中文版此前缺少命名一节,本次补齐。 @@ -957,7 +957,7 @@ generic target 取得同样的覆盖,所以空的下界不放宽任何东西。 设计与调研:`.agents/docs/2026-09-05-accelerator-support-design.md`、 `.agents/docs/2026-09-04-ai-accelerator-toolchain-ecosystem-survey.md`。 -新增手册章节 `docs/20-accelerators.md`(中英双份)。 +新增手册章节 `docs/42-heterogeneous-builds.md`(中英双份)。 ## [2026.9.4.3] — 2026-09-04 @@ -1159,7 +1159,7 @@ M-profile:MPU 按基址与上限描述区域,没有页表项。A-profile 有真 `mcpp why runners` 列出本工程提供的 runner,与其余解析结果并列; `mcpp run --list-runners` 是同一份读取,单独报告。 -新增 `docs/18-devices.md`、`docs/19-supported-versions.md`(中英双份)。 +新增 `docs/41-devices.md`、`docs/51-supported-versions.md`(中英双份)。 指令协议版本 6。 ## [2026.9.4.1] — 2026-09-04 @@ -2543,7 +2543,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 本次发布修的三条缺陷全部来自「一个事实在多处各自推导」。各包的载荷路径由 `MCPP_XPKG_*_DIR` 另行回答,与 `PATH` 是两个问题。 - 新增 `examples/07-project-subos/` 与 [第 17 章](docs/17-the-project-environment.md)。 + 新增 `examples/07-project-subos/` 与 [第 17 章](docs/23-the-project-environment.md)。 ### 测试 @@ -2792,7 +2792,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 ### 文档 -- 新增[第 16 章 —— 目标三元组](docs/16-the-target-triple.md)(中英双份): +- 新增[第 16 章 —— 目标三元组](docs/21-the-target-triple.md)(中英双份): 三段各是什么、第三段在每个平台上命名的是不同的轴、两种体系的分界、 以及该用哪种拼法。 @@ -2909,7 +2909,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 判据是失败模态:选错不会让链接失败,会得到一个能跑、偶尔崩的程序。 `[build] runner` 早已按同一条规则处理。 -- **`docs/14-target-side.md`(中英)与 `docs/specs/target-side.md`(SPEC-002)。** +- **`docs/22-target-side.md`(中英)与 `docs/specs/target-side.md`(SPEC-002)。** ### 变更 @@ -3890,7 +3890,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 (它不改任何一条边 —— 进了会让「抬高超时」重建全世界)。 - 旧版 mcpp 遇到 `module_extensions` 会警告+忽略,然后把那些文件当普通翻译单元 编译 —— **错误的构建**而不是干净的失败。发布用了这个键的包必须声明 mcpp 版本下限 - (见 `docs/10-publishing-a-library.md`)。 + (见 `docs/11-publishing-a-library.md`)。 ## [2026.8.10.3] — 2026-08-10 @@ -4069,7 +4069,7 @@ windows-x86_64),而不是手头有哪几台 runner —— 一台拿到发布二 机器可读输出有契约了。设计与实测见 `.agents/docs/2026-08-08-machine-readable-output-protocol-design.md`,用户文档见 -`docs/11-machine-output.md`。 +`docs/50-machine-output.md`。 ### 新增 @@ -4317,11 +4317,11 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 ### 文档 -- `docs/05-mcpp-toml.md` 的示例此前写的是 `import mcpp.rules.protobuf;`,**做不到**:mcpp 用依赖的裸 `package.name` 注册 host 模块,而 SPEC-001 要求 `name` 是单一原子段。随之澄清一条会咬人的约束:规则包的名字必须是**合法 C++ 模块名**(`grpcgen` 可以,`grpc-rules` 不行),否则报的是 `module 'grpc_rules' not found`,不会提示你名字有问题。 +- `docs/04-mcpp-toml.md` 的示例此前写的是 `import mcpp.rules.protobuf;`,**做不到**:mcpp 用依赖的裸 `package.name` 注册 host 模块,而 SPEC-001 要求 `name` 是单一原子段。随之澄清一条会咬人的约束:规则包的名字必须是**合法 C++ 模块名**(`grpcgen` 可以,`grpc-rules` 不行),否则报的是 `module 'grpc_rules' not found`,不会提示你名字有问题。 ## [2026.8.5.1] — 2026-08-05 -`build.mcpp` 机制的**架构地基**:把「一条指令是什么」收敛成一张表,并补上三个今天就存在的稳定性缺口。架构分析见 `.agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md`(本次实现其中的步 0 与步 1)。 +`build.mcpp` 机制的**架构地基**:把「一条指令是什么」收敛成一张表,并补上三个今天就存在的稳定性缺口。架构分析见 `.agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md`(本次实现其中的步 0 与步 1)。 ### 改进 @@ -4516,7 +4516,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 ### 修复 -- **`[build] static_stdlib = false` 对测试二进制静默失效(#336)。** #124 在 0.0.52 明确写下这个 opt-out,`docs/05-mcpp-toml.md` 至今也还这么写;但 #202(0.0.86)把测试二进制改成与分发目标相同的静态 `-load_hidden` libc++ 时,新增的那条推导**没有带上 `staticStdlib` 门**。结果是约一年时间里 macOS 上的 `mcpp test` 没有任何办法回到动态 libc++,而文档一直在承诺它可以。 +- **`[build] static_stdlib = false` 对测试二进制静默失效(#336)。** #124 在 0.0.52 明确写下这个 opt-out,`docs/04-mcpp-toml.md` 至今也还这么写;但 #202(0.0.86)把测试二进制改成与分发目标相同的静态 `-load_hidden` libc++ 时,新增的那条推导**没有带上 `staticStdlib` 门**。结果是约一年时间里 macOS 上的 `mcpp test` 没有任何办法回到动态 libc++,而文档一直在承诺它可以。 - **macOS 上全局对象在静态初始化期访问 `std::cout` 必崩(#336)。** Mach-O 没有按优先级排序的初始化段(`init_priority` 只在单个 TU 内有效),归档成员的初始化器按链接顺序排在最后;而 libc++ 的 `` 不像 libstdc++ / MSVC STL 那样自带 `ios_base::Init` 守卫,流的构造只存在于库内对象里。两件事叠起来的后果是:默认配置下,任何在构造函数里碰 `std::cout` 的全局对象都会读到 vptr 为零的流,进程启动即 SIGSEGV —— 而且**包侧无法修复**,因为 `std::ios_base::Init` 在 libc++ 的头文件里只有前向声明(`ios:70`),标准为静态初始化次序提供的官方解药在 libc++ 上用户根本写不出来。 @@ -4550,7 +4550,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 ### 修复 -- **`--offline` 不再拒绝本地 git 远端。** `docs/05-mcpp-toml.md` 写明 `--offline` 的语义是「完全不碰网络……已安装的东西照常构建」,`prepare.cppm` 里依赖下载闸的注释也把线画在同一处:「这条线以上全是本地操作,一个依赖齐备的离线构建必须成功」。但 `git = "../sibling-repo"` 这种指向本地目录的远端,`ls-remote`/`clone` 都只是文件系统读取,拒绝它买不到任何隔离性。现在按远端形态判定——`file://`、以及不带 scheme 也不是 `git@host:path` 的存在路径,算本地(Windows 盘符 `C:\repo` 含冒号但不含 `@`,因此仍归本地)。 +- **`--offline` 不再拒绝本地 git 远端。** `docs/04-mcpp-toml.md` 写明 `--offline` 的语义是「完全不碰网络……已安装的东西照常构建」,`prepare.cppm` 里依赖下载闸的注释也把线画在同一处:「这条线以上全是本地操作,一个依赖齐备的离线构建必须成功」。但 `git = "../sibling-repo"` 这种指向本地目录的远端,`ls-remote`/`clone` 都只是文件系统读取,拒绝它买不到任何隔离性。现在按远端形态判定——`file://`、以及不带 scheme 也不是 `git@host:path` 的存在路径,算本地(Windows 盘符 `C:\repo` 含冒号但不含 `@`,因此仍归本地)。 - **克隆被中途杀掉后不再永久提供错误的 commit。** 缓存目录以 commit 命名,但内容是 `git clone` 之后再 `git checkout` 两步做出来的;进程死在两步之间,目录名和 HEAD 就对不上了,而后续构建只检查目录存不存在。现在分支依赖会比对 `git rev-parse HEAD`,不符即删除重克隆(tag/rev 以 ref 名为身份,无可比之物)。 @@ -4633,7 +4633,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 **最危险的一条判据**(`SuppressedInconclusive`):「本地查不到」单独不能推出「需要刷新」。xim 描述符不写 `namespace`,`(xim, x)` 永远匹配不上身份门 —— 把这种 miss 当真,任何带 xim 依赖的工程会**每次构建都刷**,比被删掉的 TTL 更糟。判据复用 `IndexRoute::authoritative_for`(#307),单测 + e2e 双闸锁住。 - 语义变化:`^1.2` 对**本地索引已知的版本**求解。上游新发的 1.3.0 需要 `mcpp index update` 或 `mcpp update` 才可见 —— 这正是那两个命令存在的意义,已写进 `docs/05-mcpp-toml.md`。 + 语义变化:`^1.2` 对**本地索引已知的版本**求解。上游新发的 1.3.0 需要 `mcpp index update` 或 `mcpp update` 才可见 —— 这正是那两个命令存在的意义,已写进 `docs/04-mcpp-toml.md`。 - **`mcpp update` 不再是空操作。** 它此前只删 mcpp.lock 条目、然后叫用户去跑 `mcpp build` —— 而构建路径**从不读 mcpp.lock**(`prepare` 只写不读),所以删了等于没删,行为影响为零。它现在先强制刷新索引(显式意图 ⇒ 不看 TTL、不看去抖),并报告索引 rev 的变化;工程里没有任何走共享 registry 的依赖时跳过(刷了也没用)。 @@ -5629,7 +5629,7 @@ xlings 作为运行时底座:subos 环境到达程序,以及 self-contained 的 - `[targets.]` 下的不支持键不再被静默丢弃,而是产生 warning(`--strict` 下为 error), 并指引到正确的机制(workspace / features / profile)。 -- 文档 `docs/05-mcpp-toml.md`(及 `docs/zh`)新增"构建配置该放哪"的决策指引。 +- 文档 `docs/04-mcpp-toml.md`(及 `docs/zh`)新增"构建配置该放哪"的决策指引。 设计记录见 `.agents/docs/2026-06-18-per-target-build-config-design.md`。 ## [0.0.54] — 2026-06-10 diff --git a/README.md b/README.md index e1003f73..64de02b1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Module](https://img.shields.io/badge/module-ok-green.svg)](https://en.cppreference.com/w/cpp/language/modules) [![License](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE) -| [Documentation](docs/) · [Getting Started](docs/00-getting-started.md) · [mcpp.toml Guide](docs/05-mcpp-toml.md) · [Examples](docs/01-examples.md) · [Toolchains](docs/03-toolchains.md) | +| [Documentation](docs/) · [Getting Started](docs/01-getting-started.md) · [mcpp.toml Guide](docs/04-mcpp-toml.md) · [Examples](docs/03-examples.md) · [Toolchains](docs/20-toolchains.md) | |:---:| | [Package index mcpp-index](https://mcpplibs.github.io/mcpp-index/) · [Module libraries mcpplibs](https://github.com/mcpplibs) · [Community Forum](https://forum.d2learn.org/category/20) · [Issues](https://github.com/mcpp-community/mcpp/issues) · [Releases](https://github.com/mcpp-community/mcpp/releases) | | [![ci-linux](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml) [![ci-macos](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml) [![ci-windows](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml) | @@ -224,7 +224,7 @@ Then `import` it directly in your code: import mcpplibs.cmdline; ``` -> For more dependency options (version constraints, namespaces, Git references, local paths, etc.), see the [mcpp.toml guide — dependency management](docs/05-mcpp-toml.md). +> For more dependency options (version constraints, namespaces, Git references, local paths, etc.), see the [mcpp.toml guide — dependency management](docs/04-mcpp-toml.md). ## Feature Overview @@ -413,17 +413,17 @@ verified — CI builds **and executes** the artifact end-to-end (qemu/wine inclu > cross-compilers by construction, so any host that can install the LLVM > payload produces these targets. The C library, startup code, memory layout > and emulator travel with a board-support package rather than with mcpp — see -> [docs/13 — Bare-Metal and Freestanding Targets](docs/13-baremetal.md). +> [docs/13 — Bare-Metal and Freestanding Targets](docs/40-baremetal.md). ## Documentation -- [Getting Started](docs/00-getting-started.md) — install → new → build → run in 5 minutes -- [Examples](docs/01-examples.md) -- [Packaging & Release](docs/02-pack-and-release.md) -- [Toolchain Management](docs/03-toolchains.md) -- [Building from Source](docs/04-build-from-source.md) -- [mcpp.toml Guide](docs/05-mcpp-toml.md) -- [Workspaces](docs/06-workspace.md) +- [Getting Started](docs/01-getting-started.md) — install → new → build → run in 5 minutes +- [Examples](docs/03-examples.md) +- [Packaging & Release](docs/10-pack-and-release.md) +- [Toolchain Management](docs/20-toolchains.md) +- [Building from Source](docs/90-build-from-source.md) +- [mcpp.toml Guide](docs/04-mcpp-toml.md) +- [Workspaces](docs/07-workspace.md) Full options for any command are available via `mcpp --help`. diff --git a/README.zh-CN.md b/README.zh-CN.md index 9ed95b31..c2e374f3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,7 +9,7 @@ [![Module](https://img.shields.io/badge/module-ok-green.svg)](https://en.cppreference.com/w/cpp/language/modules) [![License](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](LICENSE) -| [文档](docs/zh/) · [快速开始](docs/zh/00-getting-started.md) · [mcpp.toml 指南](docs/zh/05-mcpp-toml.md) · [示例项目](docs/zh/01-examples.md) · [工具链管理](docs/zh/03-toolchains.md) | +| [文档](docs/zh/) · [快速开始](docs/zh/01-getting-started.md) · [mcpp.toml 指南](docs/zh/04-mcpp-toml.md) · [示例项目](docs/zh/03-examples.md) · [工具链管理](docs/zh/20-toolchains.md) | |:---:| | [包索引 mcpp-index](https://mcpplibs.github.io/mcpp-index/) · [模块化库 mcpplibs](https://github.com/mcpplibs) · [社区论坛](https://forum.d2learn.org/category/20) · [Issues](https://github.com/mcpp-community/mcpp/issues) · [Releases](https://github.com/mcpp-community/mcpp/releases) | | [![ci-linux](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml) [![ci-macos](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml) [![ci-windows](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml) | @@ -221,7 +221,7 @@ cmdline = "0.0.2" import mcpplibs.cmdline; ``` -> 更多依赖配置方式(版本约束、命名空间、Git 引用、本地路径等)参见 [mcpp.toml 指南 — 依赖管理](docs/zh/05-mcpp-toml.md)。 +> 更多依赖配置方式(版本约束、命名空间、Git 引用、本地路径等)参见 [mcpp.toml 指南 — 依赖管理](docs/zh/04-mcpp-toml.md)。 ## 功能概览 @@ -387,13 +387,13 @@ mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family ## 文档 -- [快速开始](docs/zh/00-getting-started.md) — 5 分钟完成 install → new → build → run -- [示例项目](docs/zh/01-examples.md) -- [发布打包](docs/zh/02-pack-and-release.md) -- [工具链管理](docs/zh/03-toolchains.md) -- [从源码构建](docs/zh/04-build-from-source.md) -- [mcpp.toml 指南](docs/zh/05-mcpp-toml.md) -- [工作空间](docs/zh/06-workspace.md) +- [快速开始](docs/zh/01-getting-started.md) — 5 分钟完成 install → new → build → run +- [示例项目](docs/zh/03-examples.md) +- [发布打包](docs/zh/10-pack-and-release.md) +- [工具链管理](docs/zh/20-toolchains.md) +- [从源码构建](docs/zh/90-build-from-source.md) +- [mcpp.toml 指南](docs/zh/04-mcpp-toml.md) +- [工作空间](docs/zh/07-workspace.md) 任意命令的完整选项可通过 `mcpp --help` 查阅。 diff --git a/docs/00-what-mcpp-is.md b/docs/00-what-mcpp-is.md new file mode 100644 index 00000000..8d0228ba --- /dev/null +++ b/docs/00-what-mcpp-is.md @@ -0,0 +1,201 @@ +# 00 — What mcpp Is + +## Background: the state of C++ project tooling + +A C++ project needs four things in place at the same time: a build description, +a set of dependencies, a compiler new enough for the code, and an environment in +which the result runs. No single tool in C++ owns all four. Four unrelated +classes of tool each carry one, and aligning them is the project's own work. + +CMake is the de facto standard for the first of the four. A de facto standard is +a statement about adoption, not about the experience of use. CMake does not +resolve dependencies, does not install a compiler and does not describe the +runtime environment, and those three are what a new contributor meets on the +first day. Supplying them means adding a package manager, the distribution's +packages, and a paragraph of instructions in a README: three further models, +with the alignment between them left to the project. + +The environment is the layer that fails most often, because it is the only one +nothing checks. A build description reports an error at configure time and a +package manager fails to resolve, but a library present on the machine that is +not the library the code was built against produces no signal until the link, +and sometimes not until the program runs. + +Modules add one more constraint to that structure. C++20 modules change what a +translation unit costs: an interface is declared once and imported instead of +being re-parsed out of headers by every file that needs it, and a consumer sees +what the author exported rather than everything a header happened to include. +The price is that a language feature is now implemented by the build system. The +build scans sources for `import`, orders the compiles accordingly, caches the +compiled interfaces and invalidates them correctly, on a compiler new enough to +have the feature at all. `import std` adds a further requirement: the standard +library's own module is built before anything else can use it. + +A project that adopts modules therefore needs all four layers rather than three. +The gap is concrete. On the machine this chapter was written on: + +```console +$ g++ --version +g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 +``` + +That compiler cannot compile `import std`. Nothing about the project is wrong; +the machine is not the machine the project requires. + +## The cost of that state + +**For a person the cost is a setup**, repeated once per machine and once per new +contributor: install a newer compiler, determine which build flags enable +modules, obtain the dependencies, and then determine which of the three is at +fault when the link fails. The cost does not fall as a project matures. It +repeats per person and per machine. + +**For an agent the cost is context**, spent in three places before a line of +code is written: reading the build description to determine what it does, +reconstructing the environment it assumes, and following a header through its +transitive includes to determine what is declared. + +Modules remove the third, because an interface is explicit and `import` states +what is used. mcpp removes the other two, and reduces the first to one command. + +## The composition of mcpp + +``` +mcpp = build system + + build plugins + + package manager + + toolchain management + + the environment and runtime (xlings) +``` + +Most C++ projects assemble those five parts from separate tools, and the setup +cost described above is the cost of that assembly: the build file assumes a +compiler the machine does not have, the package manager assumes a build file it +did not write, and the environment is a paragraph in a README. + +mcpp is one program, and the five parts share one model. + +For a reader who already uses tools for these jobs: + +| component | form in mcpp | comparable tools | +|---|---|---| +| build system | `mcpp.toml`, the module graph, the ninja backend | CMake, Meson | +| build plugins | `build.mcpp`, rule packages | `build.zig`, xmake rules | +| package manager | `[dependencies]`, `mcpp.lock`, the index | Conan, vcpkg | +| toolchain management | the compiler as an installed, pinned payload | Zig's bundled toolchain, rustup, manual GCC / LLVM / MSVC installation | +| environment and runtime | `[xlings]`, payloads, the runtime search path | Nix, conda | + +The closest single-tool analogues are Cargo and Zig, each for a different half +of the same idea. Cargo is one program that is the build, the package manager, +the lock file and the test runner, so a Rust project is cloned and built with no +preliminary step. Zig ships its toolchain with the tool and cross-compiles by +default, so the compiler is not something the machine must already have. + +mcpp is that shape for C++, with one part neither of them has: the environment +layer, through which a project declares the non-compiler tools its build +requires. + +**The table places the parts; it does not claim equivalence.** Each tool listed +does more in its own area than mcpp does, and a project that needs that depth +uses it. + +## The guarantee + +> **Clone any mcpp project and `mcpp build` works** — without installing a +> compiler, configuring an environment, or locating dependencies. + +Two boundaries are stated here so that the claim can be relied on: a project +that targets a device still downloads that device's toolkit on the first build, +and a target this machine cannot serve is refused by name rather than built +incorrectly. + +## A minimal example + +On the same machine, whose only C++ compiler is the GCC 13 above. + +```console +$ mcpp new hello +Created bin package 'hello' at /tmp/zero-demo/hello +Next: cd hello && mcpp build && mcpp run (or `mcpp test`) +``` + +Four files, and the manifest is five lines: + +```toml +[package] +name = "hello" +version = "0.1.0" +description = "A modular C++23 package" +license = "Apache-2.0" +``` + +**No compiler, no language standard and no dependency is declared.** That is the +whole of what a reader has to understand before changing this project. The +source uses the feature the machine's compiler does not have: + +```cpp +import std; + +int main() { + std::println("Hello from hello!"); +} +``` + +```console +$ mcpp run + Inferred target hello (bin from src/main.cpp) + Compiling hello v0.1.0 (.) + Finished dev [unoptimized + debuginfo] in 0.64s + Running `target/x86_64-linux-gnu/0946988e9e4b52ba/bin/hello` + +Hello from hello! +Built with import std + std::println on modular C++23. +``` + +1.25 seconds of wall clock, first run included. The compiler that performed it: + +```console +$ mcpp self env +default toolchain = gcc@16.1.0 +``` + +mcpp installed GCC 16 and used it. Nothing on the host was modified, and a +colleague — or an agent — cloning this project receives the same compiler rather +than the one their machine happens to ship. + +**A dependency is one line and requires no further step:** + +```toml +[dependencies] +"mcpplibs.cmdline" = "^0.0.1" +``` + +`mcpp build` resolves it, fetches it, builds it and links it. + +## Scope + +mcpp is built around **C++20/23 modules and recent language features**, and the +ecosystem it maintains follows from that rather than from a general ambition: + +| | | +|---|---| +| modular C++ | `import std` with no configuration, module scanning, a cross-project BMI cache | +| embedded and bare metal | freestanding targets, board-support packages, one command from source to a running image | +| heterogeneous computing and GPUs | CUDA, HIP, SYCL, Vulkan/SPIR-V and Ascend C, each a rule package rather than an engine feature | +| graphics | shaders compiled as part of the build and reached as modules | +| kernel and low-level work | zero-libc tiers, an explicit link model, no hidden host dependency | + +A project that requires none of these still receives the guarantee above; a +project that requires one of them does not leave the tool to obtain it. + +## Reading path + +| | | +|---|---| +| run a first program | [01 — Getting Started](01-getting-started.md) | +| determine whether mcpp fits the work at hand | [02 — Scenarios](02-scenarios.md) | +| read a project of the same shape | [03 — Examples](03-examples.md) | + +Everything after this chapter is reference: what a manifest may state, how +dependencies resolve, how a target is named. This chapter states no field and no +flag by design. diff --git a/docs/01-examples.md b/docs/01-examples.md deleted file mode 100644 index 33edb693..00000000 --- a/docs/01-examples.md +++ /dev/null @@ -1,64 +0,0 @@ -# 01 — Examples - -> The repository's [`examples/`](../examples) directory provides a set of -> progressively more advanced minimal projects, covering common scenarios from -> a single-file `import std` to a fully static release package. Each example can -> be entered on its own and built with `mcpp build`. - -## How to Run - -```bash -git clone https://github.com/mcpp-community/mcpp -cd mcpp/examples/01-hello -mcpp build && mcpp run -``` - -Each example ships with its own README that only explains the new concepts it -introduces relative to the previous one. Common material such as installation -steps and toolchain initialization lives in -[00 — Getting Started](00-getting-started.md) and is not repeated within the -examples. - -## Example List - -| # | Path | Description | Key Concepts | -|---|---|---|---| -| 01 | [`examples/01-hello`](../examples/01-hello/) | Minimal single-file project with `import std` | The minimal package shape (`mcpp new` also emits `tests/test_smoke.cpp`) | -| 02 | [`examples/02-with-deps`](../examples/02-with-deps/) | Adds the `mcpplibs.cmdline` dependency to parse command-line arguments | `[dependencies]`, SemVer, `mcpp.lock` | -| 03 | [`examples/03-pack-static`](../examples/03-pack-static/) | Produces a fully static release package via `mcpp pack --mode static` | `[target.]` and `[pack]` configuration | -| 04 | [`examples/04-workspace`](../examples/04-workspace/) | A multi-package workspace: two libraries and an application sharing one namespace | `[workspace]`, path dependencies, `mcpp build --workspace` | -| 05 | [`examples/05-lib-distribution`](../examples/05-lib-distribution/) | A prebuilt library and its consumer, interesting only together | `mcpp pack` for a library, a C header and a C++ module from one source, a distribution package | -| 06 | [`examples/06-openkal-cross`](../examples/06-openkal-cross/) | One program asking each machine what it is, built for four targets from any host | `--target`, openkal, cross-compilation without editing the source | -| 07 | [`examples/07-project-subos`](../examples/07-project-subos/) | A build program that finds its tools in the environment the project declared | `[xlings] subos`, `[xlings.workspace]`, a build program whose `PATH` is the environment the project named | -| 08 | [`examples/08-build-rules`](../examples/08-build-rules/) | Two rule packages and a project that uses both | `host-module = true`, `[build-dependencies]`, `mcpp::action` with `role = "check"` | -| 09 | [`examples/09-heterogeneous`](../examples/09-heterogeneous/) | One computation on a device, in several programming models, with a CPU fallback in each; plus one artifact carrying several backends at once | `accel`, constrained source globs, the seam module, rule packages from `mcpp:plugins`, `cfg(accelerator = …)` | -| 09a | [`…/cuda`](../examples/09-heterogeneous/cuda/) | A CUDA kernel behind a seam module, with the `extern "C"` boundary GENERATED | `mcpp.rules.cuda`, `mcpp.tools.island`, `mcpp::action` with `role = "object"`, the driver stated as a fact and a floor | -| 09b | [`…/vulkan`](../examples/09-heterogeneous/vulkan/) | The same computation as a Vulkan compute shader, with the SPIR-V payload reached as a MODULE | `mcpp.rules.spirv`, the module surface, `mcpp::action` with `role = "source"`, a software driver as a payload | -| 09c | [`…/sycl`](../examples/09-heterogeneous/sycl/) | The same computation as a SYCL kernel, compiled by a second compiler | `mcpp.rules.sycl`, `mcpp.tools.island`, the `.sycl` device extension, a chained `mcpp::action` for the device link, `compat:sycl-runtime` | -| 09d | [`…/hip`](../examples/09-heterogeneous/hip/) | The same computation in HIP, with the boundary WRITTEN BY HAND — the contrast against 09a | `mcpp.rules.hip`, HIP as a header layer over the CUDA runtime, a two-chunk `accel` | -| 09e | [`…/multi-backend`](../examples/09-heterogeneous/multi-backend/) | Several backends in ONE artifact, chosen at run time — the library shape, not the program shape | `accel` as a set, `cfg(accelerator = "none")` and its negation, a dispatch chain, a module seam over a C island boundary | -| 09f | [`…/cann`](../examples/09-heterogeneous/cann/) | An Ascend C kernel behind the same seam. **Does not build yet** — its README names the two missing pieces | the `.asc` device extension, `op_kernel`/`op_host` as an island CANN already has, `accelerator = "none"` for the fallback | -| 10 | [`examples/10-graphics`](../examples/10-graphics/) | Graphics rather than compute: a rendering pipeline whose result is pixels | `mcpp.rules.spirv` for the vertex and fragment stages, offscreen rendering as the assertable form | -| 10a | [`…/offscreen`](../examples/10-graphics/offscreen/) | A triangle rasterised by Vulkan into a buffer, and the same triangle by a software rasteriser behind the same seam | two shader stages from one glob, a render pass with no window or swapchain, a pixel as the criterion | - -## Suggested Reading Order - -We recommend reading them in numerical order: - -1. **`01-hello`** shows the minimal package skeleton (`mcpp.toml` and - `src/main.cpp`) and demonstrates the basic usage of `import std`. The current - `mcpp new` scaffold also emits `tests/test_smoke.cpp`. -2. **`02-with-deps`** builds on the previous example by introducing an external - dependency, covering the lock-file mechanism and how the modular package - index works. -3. **`03-pack-static`** demonstrates how to package build artifacts into a - standalone, independently distributable single-file binary; for packaging - details, see [02 — Packaging and Release](02-pack-and-release.md). - -## Adding a New Example - -Example projects follow a consistent directory structure: `mcpp.toml` + `src/` + -`README.md`. To add a new example, create a numbered directory under -`examples/` (e.g. `04-xxx/`), briefly describe the concept it demonstrates in -its README, and then open a PR. For contribution guidelines, see -[04 — Build from Source & Contributing](04-build-from-source.md). diff --git a/docs/00-getting-started.md b/docs/01-getting-started.md similarity index 81% rename from docs/00-getting-started.md rename to docs/01-getting-started.md index 665c171a..cf2e3166 100644 --- a/docs/00-getting-started.md +++ b/docs/01-getting-started.md @@ -1,30 +1,56 @@ -# 00 — Getting Started +# 01 — Getting Started + +**Reader:** a newcomer with nothing installed yet. + +**The question this chapter answers:** how do I get a program compiling and +running, from an empty machine. + +**Not here:** what mcpp's parts are — that is +[00 — What mcpp Is](00-what-mcpp-is.md), which this chapter assumes rather +than repeats — and every field a manifest may hold, which is +[04 — The mcpp.toml Manifest](04-mcpp-toml.md). After: [03 — Examples](03-examples.md). > Go from install → new → build → run → pack in 5 minutes. ## Installation -Supported hosts are Linux x86_64 / aarch64, macOS ARM64, and Windows x86_64. You do not need to install GCC, xlings, or any other build dependency beforehand. -On its first run, mcpp installs a default toolchain into an isolated sandbox (`~/.mcpp/`). The choice is host-aware: Linux x86_64 uses `gcc@16.1.0`; other Linux architectures use `gcc@15.1.0-musl`; macOS uses `llvm@20.1.7`; Windows uses `llvm@20.1.7` when usable MSVC is available and otherwise uses `gcc@16.1.0` for `x86_64-windows-gnu`. +Supported hosts are Linux x86_64 / aarch64, macOS ARM64, and Windows x86_64. +GCC, xlings and every other build dependency are installed by mcpp; none of them +has to be there first. -We recommend installing via [xlings](https://xlings.d2learn.org), which keeps mcpp isolated from your system environment: +**The recommended way is [xlings](https://xlings.d2learn.org)**, which keeps +mcpp isolated from the system environment: ```bash xlings install mcpp -y ``` -Alternatively, on Linux x86_64/aarch64 or macOS ARM64, use the one-line -installer script (xlings is bundled, and everything is installed under -`~/.mcpp/`): +
+Other ways: the standalone script, and what the first run installs + +On Linux x86_64/aarch64 or macOS ARM64 a one-line installer bundles xlings and +puts everything under `~/.mcpp/`. It does not support Windows, where the +PowerShell xlings command in the README is the way in. ```bash curl -fsSL https://github.com/mcpp-community/mcpp/releases/latest/download/install.sh | bash ``` -The script does not support Windows; install through the PowerShell xlings -command in the README instead. +On its first run mcpp installs a default toolchain into `~/.mcpp/`, chosen for +the host: -For full installation instructions (including xlings install commands, Windows support, and more), see the ["Installation" section of the README](../README.md#install). +| host | default | +|---|---| +| Linux x86_64 | `gcc@16.1.0` | +| other Linux architectures | `gcc@15.1.0-musl` | +| macOS | `llvm@20.1.7` | +| Windows with usable MSVC | `llvm@20.1.7` | +| Windows without it | `gcc@16.1.0` for `x86_64-windows-gnu` | + +Full installation instructions, including Windows, are in the +["Installation" section of the README](../README.md#install). + +
Once installation is complete, start a new shell session, then verify: @@ -168,7 +194,7 @@ Declare dependencies in `mcpp.toml`: `mcpp build` automatically resolves SemVer constraints against the [mcpp-index](https://github.com/mcpplibs/mcpp-index), fetches the source, and adds it to the build graph. For a complete example, see `02-with-deps` in -[01 — Examples](01-examples.md). +[03 — Examples](03-examples.md). ## Producing a Release Package @@ -181,13 +207,13 @@ mcpp pack --mode static # fully static musl build mcpp pack --mode self-contained # bundle loader, libc, and dependencies ``` -For the differences between the four modes and their artifact layouts, see [02 — Packaging and Release](02-pack-and-release.md). `bundle-project` and `bundle-all` remain accepted aliases for `vendored` and `self-contained`. +For the differences between the four modes and their artifact layouts, see [10 — Packaging and Release](10-pack-and-release.md). `bundle-project` and `bundle-all` remain accepted aliases for `vendored` and `self-contained`. ## Further Reading -- [01 — Examples](01-examples.md) — a collection of ready-to-run minimal projects -- [02 — Packaging and Release](02-pack-and-release.md) — building distributable artifacts -- [03 — Toolchain Management](03-toolchains.md) — switching compilers and managing multiple versions +- [03 — Examples](03-examples.md) — a collection of ready-to-run minimal projects +- [10 — Packaging and Release](10-pack-and-release.md) — building distributable artifacts +- [20 — Toolchain Management](20-toolchains.md) — switching compilers and managing multiple versions - The full set of options for any command is available via `mcpp --help` @@ -199,3 +225,4 @@ For the differences between the four modes and their artifact layouts, see [02 - Explaining default decisions: `mcpp why [toolchain|runtime|deps]`; host capability checkup: `mcpp self doctor`; machine-readable resolution manifest: the build artifact `target///resolution.json`. - Offline operation: `mcpp --offline` or `MCPP_OFFLINE=1` prevents index refreshes, downloads, and toolchain installation. In a home that has never been used it also skips the first-use sandbox bootstrap (index clone, ninja, patchelf), announces the skip once, and leaves the home un-bootstrapped; commands that need those tools report it. + diff --git a/docs/02-scenarios.md b/docs/02-scenarios.md new file mode 100644 index 00000000..d5d461db --- /dev/null +++ b/docs/02-scenarios.md @@ -0,0 +1,295 @@ +# 02 — Scenarios + +**Reader:** a developer deciding whether mcpp fits the work in front of them, +and which of its features that work will use. + +**The question this chapter answers:** what kinds of project does mcpp serve, +and for one kind, which features are used and in what order. + +**Not here:** the field reference, which is [04](04-mcpp-toml.md); the catalogue +of examples, which is [03](03-examples.md) and is indexed by example rather than +by scenario; and the command lookup, which is +[09](09-commands-by-scenario.md). This chapter is indexed by **the work**. + +Each scenario states the situation, what mcpp contributes to it, the path +through the chapters, one project to run, and the one thing that surprises +people. Read only the scenario that matches; they do not build on each other. + +## The scenarios + +| | scenario | run | +|---|---|---| +| [1](#1-a-command-line-tool-or-service) | a command-line tool or a service | `examples/01-hello` → `03-pack-static` | +| [2](#2-a-library-other-projects-import) | a library other projects import | `examples/11-features`, `05-lib-distribution` | +| [3](#3-a-repository-with-several-packages) | a repository with several packages | `examples/04-workspace` | +| [4](#4-a-graphical-application) | a graphical application | `mcpp new … --template ocornut.imgui` | +| [5](#5-building-for-another-operating-system) | building for another operating system | `examples/06-openkal-cross` | +| [6](#6-a-target-with-no-operating-system) | a target with no operating system | `mcpp new … --template riscv-virt-rt` | +| [7](#7-compute-on-a-gpu-or-an-accelerator) | compute on a GPU or an accelerator | `examples/09-heterogeneous` | +| [8](#8-graphics-rendering) | graphics rendering | `examples/10-graphics/offscreen` | +| [9](#9-a-build-step-the-project-needs) | a build step the project needs, and sharing it | `examples/08-build-rules`, `12-a-new-device-language` | +| [10](#10-packaging-a-tool-a-driver-or-a-board) | packaging a tool, a driver or a board for others | the descriptors in `xim-pkgindex` and `mcpp-index` | + +## 1. A command-line tool or service + +**The situation.** A program built from C++23 modules, with a few dependencies, +that has to run on a machine that does not have mcpp. + +**What mcpp contributes.** `import std` works with no configuration; the +compiler is a pinned payload rather than whatever the machine has; and one +command produces a binary that carries what it needs. + +**The path.** + +1. [00 — What mcpp Is](00-what-mcpp-is.md) — the five nouns. +2. [01 — Getting Started](01-getting-started.md) — a program on the screen. +3. [05 — Dependencies and Resolution](05-dependencies.md) — `[dependencies]`, + the lock file. +4. [08 — Testing](08-testing.md) — `tests/**/*.cpp`. +5. [20 — Toolchain Management](20-toolchains.md) — when the compiler's version + matters, or the project must pin one. +6. [10 — Packaging an Application](10-pack-and-release.md) — `mcpp pack`. + +**Run.** [`examples/01-hello`](../examples/01-hello/), then +[`02-with-deps`](../examples/02-with-deps/), then +[`03-pack-static`](../examples/03-pack-static/). + +**What surprises people.** A debug build and a release build do not invalidate +each other. Each configuration gets its own fingerprinted directory under +`target/`, so alternating between them is not a rebuild. + +## 2. A library other projects import + +**The situation.** Code that other packages will name in their +`[dependencies]`, possibly with optional parts. + +**What mcpp contributes.** A module interface is the published surface; a +feature makes part of it optional without a second package; and a prebuilt +binary can state which toolchains it is compatible with. + +**The path.** + +1. [04 — The mcpp.toml Manifest](04-mcpp-toml.md) — `[lib]`, the library root. +2. [06 — Features and Capabilities](06-features-and-capabilities.md) — optional + parts, and the dependencies they pull. +3. [08 — Testing](08-testing.md) — `[dev-dependencies]`. +4. [11 — Publishing a Library](11-publishing-a-library.md) — the descriptor. +5. [12 — Distributing a Prebuilt Library](12-binary-distribution.md) — if + binaries ship too. + +**Run.** [`examples/11-features`](../examples/11-features/) declares all three +shapes of feature; [`05-lib-distribution`](../examples/05-lib-distribution/) is +the producer and consumer pair. + +**What surprises people.** The criterion for an optional backend is not that the +default build still works — it is that the default build's **resolution does not +name** the optional package. A dependency that is resolved and merely unused +still costs a download. + +## 3. A repository with several packages + +**The situation.** Several packages developed together, depending on each other +by path. + +**What mcpp contributes.** One command builds or tests the set; a member +resolves its siblings without a registry; and each member keeps its own +manifest and identity. + +**The path.** + +1. [07 — Workspaces](07-workspace.md) — `[workspace]`, path dependencies. +2. [05 — Dependencies and Resolution](05-dependencies.md) — what a path + dependency does and does not do. +3. [08 — Testing](08-testing.md) — the fan-out. + +**Run.** [`examples/04-workspace`](../examples/04-workspace/). + +**What surprises people.** A workspace member is not a root. Tooling that +enumerates "every package" has to say which of the two it means, and the +answer changes what gets built. + +## 4. A graphical application + +**The situation.** A desktop program with a window, a renderer and fonts. + +**What mcpp contributes.** The window and rendering stack are ordinary +dependencies; a template scaffolds a working project; and what the artifact +needs at run time is declared rather than discovered. + +**The path.** + +1. [01 — Getting Started](01-getting-started.md) — `mcpp new --template`. +2. [05 — Dependencies and Resolution](05-dependencies.md) — the stack. +3. [10 — Packaging an Application](10-pack-and-release.md) — what ships beside + the executable. + +**Run.** `mcpp new myapp --template ocornut.imgui`. + +**What surprises people.** On Linux an mcpp artifact runs behind a private +loader that does not consult `/usr/lib`. Anything the host must supply — a +graphics driver, a Vulkan ICD — is reached through an adapter package the +project declares, not by being installed on the machine. + +## 5. Building for another operating system + +**The situation.** One source tree that has to produce binaries for Linux, +Windows and macOS. + +**What mcpp contributes.** The target is an argument, not a second checkout; +the cross toolchain is a payload; and a target this host cannot serve is +refused rather than quietly built for the host. + +**The path.** + +1. [21 — The Target Triple](21-the-target-triple.md) — how a target is named, + and which host serves which. +2. [24 — Cross-Compilation Over openkal](24-openkal-cross.md) — the mechanism. +3. [22 — The Target Side](22-target-side.md) — when the manifest must differ + per target. + +**Run.** [`examples/06-openkal-cross`](../examples/06-openkal-cross/) — one +program built for four targets from any host. + +**What surprises people.** `--target` does not require the machine to already +have that toolchain. What it does require is that the target be *servable* from +this host; the support matrix in [21](21-the-target-triple.md) says which are. + +## 6. A target with no operating system + +**The situation.** Firmware for a board or a microcontroller: no OS, no libc by +default, a linker script and a vector table. + +**What mcpp contributes.** A board-support package supplies the whole target +world — the linker script, the startup code, and the **runner** — so `mcpp run` +is the same command on an emulator and on hardware. + +**The path.** + +1. [40 — Bare-Metal and Freestanding Targets](40-baremetal.md) — the target, + the tiers, what of `std` survives. +2. [41 — Reaching a Device](41-devices.md) — runners, named runners. +3. [08 — Testing](08-testing.md) — tests that run on the board. + +**Run.** `mcpp new blinky --template riscv-virt-rt`. + +**What surprises people.** The emulator and the physical board are one package +and one feature apart, not two packages. `mcpp run --features hardware` moves +the default runner; the command a developer types does not change. + +## 7. Compute on a GPU or an accelerator + +**The situation.** Part of the program is a kernel compiled by a vendor's +compiler and linked into an ordinary binary. + +**What mcpp contributes.** The device toolkit is declared by the rule package +and installed by the build; the accelerator is one axis stated once; and the +artifact records which devices it carries code for. + +**The path.** + +1. [42 — Heterogeneous Builds](42-heterogeneous-builds.md) — `accel`, the + island, the seam. +2. [30 — Build Programs](30-build-mcpp.md) — how a rule reaches the graph. +3. [06 — Features and Capabilities](06-features-and-capabilities.md) — the + feature that selects a lane. + +**Run.** [`examples/09-heterogeneous/boundary`](../examples/09-heterogeneous/boundary/) +first — it needs no device — then +[`…/cuda`](../examples/09-heterogeneous/cuda/). + +**What surprises people.** A build that names no accelerator downloads nothing. +Two gates must open before a byte of a multi-gigabyte toolkit is fetched: the +feature that selects the rule, and the `cfg(accelerator = …)` selector that says +this build actually compiles for the device. + +## 8. Graphics rendering + +**The situation.** Shaders compiled to SPIR-V, a pipeline, and pixels that have +to be right. + +**What mcpp contributes.** The shader compiler is a declared payload; compiled +shaders arrive as a **module** rather than as a generated header nobody named; +and a rendering result can be asserted without a GPU. + +**The path.** + +1. [42 — Heterogeneous Builds](42-heterogeneous-builds.md) — the shader lane. +2. [30 — Build Programs](30-build-mcpp.md) — the one line that asks for the + module surface. +3. [10 — Packaging an Application](10-pack-and-release.md) — shipping it. + +**Run.** [`examples/10-graphics/offscreen`](../examples/10-graphics/offscreen/) — +renders a triangle offscreen and compares the pixels against a software +rasteriser. + +**What surprises people.** A software device is not automatically a substitute +for hardware. A framework may reject one on its device *type* even when it +advertises every feature the framework requires, and that is the framework's +policy rather than a packaging defect. + +## 9. A build step the project needs + +**The situation.** Code generation, an embedded asset, a check, or a second +compiler — something the engine has no rule for. + +**What mcpp contributes.** The step becomes edges in the same graph as +everything else: ordered, fingerprinted, incremental, and reported by name when +it fails. It is not a pre-build script. + +**The path.** + +1. [30 — Build Programs](30-build-mcpp.md) — `mcpp::action`, the four roles. +2. [31 — Authoring a Rule Package](31-authoring-a-rule-package.md) — if other + projects should use the step too. +3. [23 — The Project Environment](23-the-project-environment.md) — declaring + the tools the step runs. + +**Run.** [`examples/08-build-rules`](../examples/08-build-rules/) for a rule +that checks and embeds; +[`12-a-new-device-language`](../examples/12-a-new-device-language/) for one that +teaches mcpp a language the engine has never heard of. + +**What surprises people.** The tool the step invokes is an **input** of the +step. Without that, editing the generator leaves every edge clean and the +artifact keeps the bytes the previous generator produced — a green build over a +stale result. + +## 10. Packaging a tool, a driver or a board + +**The situation.** Something other projects should be able to declare and get: +a compiler, a shader compiler, an emulator, a host graphics driver, a board. + +**What mcpp contributes.** A consumer declares it by name and gets a working +program — the payload is installed, its libraries are on the artifact's search +path, and the tier decides whether a build that never runs pays for it at all. + +**The path.** + +1. [32 — Authoring a Payload](32-authoring-a-payload.md) — a tool or a prebuilt + library mcpp installs. +2. [33 — Authoring a Runtime Adapter](33-authoring-an-adapter.md) — when the + library belongs to the host and cannot be redistributed. +3. [34 — Authoring a Board-Support Package](34-authoring-a-bsp.md) — a board, + its memory map, and the way in. +4. [31 — Authoring a Rule Package](31-authoring-a-rule-package.md) — if a build + step drives the tool. + +**Run.** The descriptors themselves: `xim-pkgindex/pkgs/g/glslang.lua` for a +payload, `mcpp-index/pkgs/c/compat.vulkan-runtime.lua` for an adapter, +`mcpplibs/cortex-m-rt` for a board. + +**What surprises people.** Which answer applies is decided by the licence and +the ABI, not by preference. A driver whose userspace is in lockstep with a +kernel module and whose licence forbids redistribution **cannot** be a payload; +it is a host capability, and the adapter is how an artifact reaches it. An open +driver is a payload, and a machine using one needs no adapter at all. + +## Current limitations + +- The scenarios here are the ones with a runnable project or a published + template behind them. A scenario mcpp serves but nothing in this repository + demonstrates is not listed, because a path with no project to run is a claim + rather than a scenario. +- Adopting mcpp inside an existing build system is not a scenario here. mcpp + builds a project it owns; interoperating with another build system's outputs + is not documented and is not covered by any example. diff --git a/docs/03-examples.md b/docs/03-examples.md new file mode 100644 index 00000000..98e26828 --- /dev/null +++ b/docs/03-examples.md @@ -0,0 +1,118 @@ +# 03 — Examples + +**Reader:** someone choosing where to start, or looking for a project shaped +like theirs. + +**The question this chapter answers:** which example teaches what, and in which +order they build on each other. + +**Not here:** the content of any example — each has its own README, which +explains only what it adds. Before: [01 — Getting Started](01-getting-started.md). +After: [04 — The mcpp.toml Manifest](04-mcpp-toml.md). + +The [`examples/`](../examples) directory is a curriculum. Each project is +runnable on its own, and each one teaches **one thing no earlier example +teaches**. This chapter says what that thing is, so you can enter at the level +you need rather than reading from the start. + +## Running an example + +```bash +git clone https://github.com/mcpp-community/mcpp +cd mcpp/examples/01-hello +mcpp build && mcpp run +``` + +Every example ships a README that explains only what it adds. Installation and +toolchain setup live in [01 — Getting Started](01-getting-started.md) and are +not repeated. + +## The curriculum + +### A — The shape of a project + +| example | first to introduce | +|---|---| +| [`01-hello`](../examples/01-hello/) | a package, `import std`, `mcpp build` and `mcpp run` | +| [`02-with-deps`](../examples/02-with-deps/) | `[dependencies]`, the lock file, `mcpp add` | +| [`04-workspace`](../examples/04-workspace/) | `[workspace]`, path dependencies, `mcpp build --workspace` | +| [`11-features`](../examples/11-features/) | `[features]` **declared** rather than consumed, `[feature-deps]`, `[dev-dependencies]`, `[profile.]`, `mcpp::has_feature` | + +### B — Publishing + +| example | first to introduce | +|---|---| +| [`03-pack-static`](../examples/03-pack-static/) | `mcpp pack --mode static`, `[target.]`, `[pack]` | +| [`05-lib-distribution`](../examples/05-lib-distribution/) | a library's interface and its prebuilt binaries; a C header and a C++ module from one source | + +### C — The environment + +| example | first to introduce | +|---|---| +| [`07-project-subos`](../examples/07-project-subos/) | `[xlings]`, `[xlings.workspace]`, a build program whose `PATH` is the environment the project declared | + +### D — Targets + +| example | first to introduce | +|---|---| +| [`06-openkal-cross`](../examples/06-openkal-cross/) | `--target`, one source built for four machines from any host | + +Bare metal is taught by a **template** rather than by a directory here — see +*Lessons that arrive as templates* below. + +### E — Devices and graphics + +Read [`09-heterogeneous`](../examples/09-heterogeneous/) in order. Its README is +the map; the table below is what each sub-example adds. + +| example | first to introduce | +|---|---| +| [`…/boundary`](../examples/09-heterogeneous/boundary/) | the island boundary alone: a generated module the consumer imports, with no seam and no header in the project. Needs no device | +| [`…/cuda`](../examples/09-heterogeneous/cuda/) | a device compiler, a seam over the generated boundary, the driver stated as a fact and a floor | +| [`…/vulkan`](../examples/09-heterogeneous/vulkan/) | a compute shader whose SPIR-V payload arrives as a module | +| [`…/sycl`](../examples/09-heterogeneous/sycl/) | a second compiler with its own standard library | +| [`…/hip`](../examples/09-heterogeneous/hip/) | the boundary written by hand — the contrast against `boundary/` and `cuda/` | +| [`…/cann`](../examples/09-heterogeneous/cann/) | a vendor outside the NVIDIA and Khronos lineages | +| [`…/multi-backend`](../examples/09-heterogeneous/multi-backend/) | several backends in one artifact, chosen when the program runs | +| [`10-graphics/offscreen`](../examples/10-graphics/offscreen/) | a rendering pipeline whose result is pixels, asserted against a software rasteriser | + +### F — Authoring for the ecosystem + +| example | first to introduce | +|---|---| +| [`08-build-rules`](../examples/08-build-rules/) | two rule packages and a project using both; `host-module = true`, `mcpp::action` with `role = "check"` | +| [`12-a-new-device-language`](../examples/12-a-new-device-language/) | `device_extensions` and `rule_module`: a rule package teaching mcpp a language the engine has never heard of, whose compiler is a package built through `tools = [...]` for the build machine | + +[31 — Authoring a Rule Package](31-authoring-a-rule-package.md) is the reference +these two illustrate. + +## Lessons that arrive as templates + +A package may ship `templates//`, which `mcpp new --template` instantiates. +That is a third teaching surface beside this directory and the chapters, and it +is where a lesson belongs when the thing being taught is owned by a package +rather than by mcpp. + +| template | lesson | chapter | +|---|---|---| +| `riscv-virt-rt` | a bare-metal project, its board support and its runner | [40](40-baremetal.md) | +| `riscv-virt-rt:nolibc` | the same with no C library | [40](40-baremetal.md) | +| `ocornut.imgui` | a graphical application with its window and rendering stack | [20](20-toolchains.md) | + +```bash +mcpp new blinky --template riscv-virt-rt +``` + +## Adding an example + +An example directory is `mcpp.toml` + `src/` + `README.md`, numbered after the +last one. A new example is warranted when a capability **changes the shape of a +project** — the files it contains, the manifest it declares, or the commands its +author types. A capability that is one line inside a project an example already +contains belongs in that chapter as a code block; one reached only through a +command belongs in [09 — Commands by Scenario](09-commands-by-scenario.md). + +The README states what the example is the first to teach and the criterion by +which it is judged to work. For contribution mechanics see +[90 — Building from Source & Contributing](90-build-from-source.md). + diff --git a/docs/04-build-from-source.md b/docs/04-build-from-source.md deleted file mode 100644 index df50a451..00000000 --- a/docs/04-build-from-source.md +++ /dev/null @@ -1,116 +0,0 @@ -# 04 — Building from Source & Contributing - -> mcpp is self-hosting — mcpp builds mcpp from source using mcpp itself. -> Any environment that already has a working mcpp binary can build from source. - -## Prerequisites - -Follow [00 — Getting Started](00-getting-started.md) to install a working copy of mcpp, then clone the repository: - -```bash -git clone https://github.com/mcpp-community/mcpp -cd mcpp -``` - -## Building and Testing - -```bash -mcpp build # compile the current source with the existing mcpp → ./target/.../bin/mcpp -mcpp run -- --version # run the artifact you just built -mcpp test # build and run C++ tests discovered under tests/**/*.cpp (including tests/unit) -``` - -`mcpp test` does not run the shell end-to-end suite under `tests/e2e/`; run -those separately against the freshly built binary. - -The first build automatically fetches the default toolchain; see [03 — Toolchain Management](03-toolchains.md) for details. - -To produce a fully static binary identical to a release (the path taken by `release.yml`): - -```bash -mcpp build --target x86_64-linux-musl -# → target/x86_64-linux-musl/.../bin/mcpp is a fully static ELF -``` - -## Source Layout - -``` -src/ -├── main.cpp entry point -├── cli.cppm command dispatch and argument parsing -├── cli/ command implementations -├── manifest/ manifest model, TOML parsing, and xpkg descriptors -├── lockfile.cppm mcpp.lock -├── version_req.cppm SemVer constraints -├── fetcher.cppm fetcher façade -├── fetcher/ package/index download and installation -├── config.cppm ~/.mcpp/config.toml -├── bmi_cache.cppm cross-project BMI cache -├── bmi_cache/ cache storage and invalidation -├── dyndep.cppm ninja dyndep generation -├── ui.cppm progress bars and output formatting -├── build/ build orchestration and ninja backend -├── fallback/ fallback resolution paths -├── modgraph/ P1689 module scanning and dependency graph -├── pm/ dependency resolver and package-management commands -├── platform/ platform and process abstractions -├── scaffold/ `mcpp new` templates and project creation -├── toolchain/ toolchain detection, fingerprinting, and std module -├── pack/ mcpp pack implementation -├── publish/ mcpp publish and xpkg generation -└── libs/ third-party dependencies (toml parsing, etc.) - -tests/ -├── unit/ C++ unit and integration tests, generally grouped by subsystem -└── e2e/ end-to-end shell scripts (run_all.sh is the CI entry point) -``` - -## Test Organization - -Tests are split into two layers: - -- **Unit and integration tests** are C++ files discovered by `mcpp test` under - `tests/**/*.cpp`. They are generally named for the subsystem or module they - exercise (for example, `test_pm_lock_io.cpp` and `test_toolchain_triple.cpp`). -- **E2E tests** live in `tests/e2e/NN_.sh` and exercise a real `mcpp` - binary; `run_all.sh` is the CI entry point. - -Choose focused unit and/or E2E coverage according to the contract changed. E2E -scripts may require the same sandbox, mirror, and capability setup used by CI. - -Run a single e2e script: - -```bash -MCPP= bash tests/e2e/02_new_build_run.sh -``` - -Replace `` with the absolute path to the binary built in the -previous step; on Windows that path names `mcpp.exe`. - -## Issue and PR Guidelines - -### Issues - -File issues at [github.com/mcpp-community/mcpp/issues](https://github.com/mcpp-community/mcpp/issues), ideally including the following: - -- The full output of `mcpp self env` -- The full output of the failing command (`MCPP_LOG_LEVEL=debug` gives more detail) -- Your operating system, distribution, and glibc version (check with `ldd --version`) - -### Pull Requests - -mcpp is in early iteration and its interfaces may change. Before submitting a PR, please note: - -1. For changes touching the CLI or the `mcpp.toml` schema, open an issue first to align on direction. -2. Keep each PR focused on a single change; write commit titles in English imperative form (`fix: ...` / `feat: ...`). -3. For behavior changes or test documentation, run `mcpp test` and the relevant - E2E scripts against a fresh binary before submitting. For documentation-only - changes, recheck the examples and links; use `gh pr checks ` for - the PR's actual required checks. - -## Community Resources - -- [Community forum](https://forum.d2learn.org/category/20) -- Chat group QQ: 1067245099 -- [mcpp-index](https://github.com/mcpplibs/mcpp-index) — the default package index -- [mcpplibs](https://github.com/mcpplibs) — the companion collection of modular C++ libraries diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md new file mode 100644 index 00000000..9f6d5685 --- /dev/null +++ b/docs/04-mcpp-toml.md @@ -0,0 +1,1271 @@ +# 04 — The mcpp.toml Manifest + +**Reader:** an author writing or reading a manifest. + +**The question this chapter answers:** what may an `mcpp.toml` say, field by +field. + +**Not here:** four topics this file's tables belong to but this chapter does not +own — dependencies are [05](05-dependencies.md), features are +[06](06-features-and-capabilities.md), conditioning on a target is +[22](22-target-side.md), and the project's environment is +[23](23-the-project-environment.md). Each is named where its table would be. + +`mcpp.toml` is the project configuration file for the mcpp build tool, analogous to Cargo's `Cargo.toml` or Node's `package.json`. Place it in the project root; `mcpp build` discovers and reads it automatically. + +## 1. Minimal Examples + +mcpp is designed around **convention over configuration** — most fields have sensible defaults, so the simplest `mcpp.toml` is just a few lines: + +### 1.1 Executable (minimal) + +```toml +[package] +name = "hello" +version = "0.1.0" +``` + +mcpp infers automatically: +- Source files: `src/**/*.{cppm,cpp,cc,c,S,s,asm}` +- Entry point: `src/main.cpp` → produces the `hello` binary +- Standard: C++23 +- Modules: scans `export module ...` declarations and builds the dependency graph automatically + +### 1.2 Library project (minimal) + +```toml +[package] +name = "mylib" +version = "0.1.0" + +[targets.mylib] +kind = "lib" +``` + +lib-root convention: the primary module interface defaults to `src/mylib.cppm` (the last segment of the package name). + +## 2. Full Field Reference + +### 2.1 `[package]` — Package Metadata + +```toml +[package] +name = "myapp" # Package name (required) +version = "0.1.0" # Semantic version (required) +standard = "c++23" # C++ standard (default c++23; can be set to c++20 / c++26) +description = "My awesome app" # Description (optional) +license = "MIT" # License (optional) +authors = ["Alice", "Bob"] # Author list (optional) +repo = "https://github.com/user/myapp" # Repository URL (optional) +``` + +`standard` is the first-class setting for the C++ language standard. Recommended values: + +- `c++23`: the default, suited to the current module-based default templates. +- `c++20`: the lowest level mcpp accepts — named modules are a C++20 feature, so nothing below it exists for this build model. Use it when an external constraint (an older in-house rule, a third-party API that stops at C++20) forces the level down. `import std;` still works there: it is a C++23 *library* feature, but GCC (≥ 15), Clang + libc++ (≥ 17) and the MSVC STL (from VS 2022 17.8) all provide the `std` module in C++20 mode as well. Note that C++23 library facilities (`std::print`, `std::expected`, …) are not available, including in the code generated by `mcpp new`. +- `c++26`: for C++26 language features. +- `c++2a` / `c++2c`: compatibility aliases, normalized to `c++20` / `c++26` after parsing. +- `gnu++20` / `gnu++23` / `gnu++26`: GNU dialects; the choice enters the fingerprint and the std BMI cache key. +- `c++latest`: resolves to the newest standard level the resolved toolchain supports. Good for local experimentation, but not recommended for release packages that require reproducibility. +- `c++fly`: `c++latest` **plus every experimental standard feature the resolved toolchain can enable** (language + standard library). On GCC ≥ 16 this turns on C++26 reflection (`-freflection`) and contracts; on Clang/libc++ it adds `-fexperimental-library`; unsupported gates are skipped with a printed summary. Deliberately toolchain-dependent — the bleeding-edge playground mode, never for published packages. + +Two properties worth knowing: + +- **The standard is module-graph-global.** The root package's `standard` applies to every + translation unit in the build, dependencies included — a dependency's own `standard` is not + used when it is being built as a dependency. This is not a simplification: BMIs are not + compatible across levels (GCC rejects them with `language dialect differs`), so a single + graph physically cannot hold two levels. +- **Levels never share caches.** The standard is part of the fingerprint, the `import std` BMI + identity and the dependency build-cache key, so switching between `c++20` and `c++23` gives + each level its own target directory and its own std BMI instead of a corrupt hit. + +If the sources `import std;` at a level the resolved toolchain does not provide the `std` +module for, mcpp fails before compiling and names both the toolchain and the project level. + +Both spellings of the value are accepted: `standard = "c++26"` and `standard = 26`. + +When a **dependency declares a level above the graph's**, mcpp says so before compiling +rather than letting it fail somewhere inside that dependency's sources. See +[workspace §4.2](07-workspace.md). + +#### Dialect flags and the `import std` BMI + +Some flags change what the standard library's headers declare, so the precompiled `import std` +BMI has to be built with them too. That is what `[build] dialect_cxxflags` is for: it is +applied to the std BMI prebuild, the module scan **and** every translation unit in the graph, +including dependencies. + +```toml +[build] +dialect_cxxflags = ["-fno-exceptions"] +``` + +mcpp promotes a few flags into that channel automatically when it finds them in `cxxflags` +(`-freflection`, `-fchar8_t`, `-D_GLIBCXX_USE_CXX11_ABI=…`) — a graph that mixes those is +ill-formed anyway, so no dependency can hold a different opinion about them. + +`-fno-exceptions` and `-fno-rtti` are **not** promoted, because a dependency can legitimately +disagree: they remove a language facility the dependency may use, and the consumer cannot make +that choice on its behalf. Left in `cxxflags` they reach every TU and not the prebuild, so the +build cannot succeed — mcpp refuses it before compiling and names the key: + +``` +error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is + precompiled without it, so every importing translation unit will fail with + "language dialect differs". + Declare it as a dialect flag instead: + + [build] + dialect_cxxflags = ["-fno-exceptions"] +``` + +The check reads the **effective** flags, so it fires for the same flag written in +`[profile.] cxxflags` or in a `[target.…]` block. It does not fire when nothing in the +graph imports `std`, where the flag is an ordinary per-unit option that works. + +### 2.2 `[targets.]` — Build Targets + +```toml +# Executable (default; inferred automatically when src/main.cpp exists) +[targets.myapp] +kind = "bin" +main = "src/main.cpp" # Optional, defaults to src/main.cpp + +# Static library +[targets.mylib] +kind = "lib" + +# Shared library +[targets.mylib] +kind = "shared" +soname = "libmylib.so.1" # Optional: Linux/ELF ABI name; an alias of the same name is generated at runtime +``` + +`soname` is the ABI name for a shared library, analogous to `SOVERSION`/`SONAME` in +Autotools/CMake. On Linux, mcpp passes `-Wl,-soname,` to the linker and +generates a ` -> lib.so` alias in the output directory, so that +downstream programs can load the library via its standard ABI name through +`DT_NEEDED` or `dlopen()`. This field only applies to `kind = "shared"`, and the +value must be a filename basename. + +Shared-library targets work on all three binary formats. ELF gets a `.so` with +its `soname` and a `$ORIGIN` search path; Mach-O gets a `.dylib` whose install +name is `@rpath/`, so it survives being moved; PE gets both the `.dll` +the loader opens and the import library the linker consumes, with the export +list generated from the objects on the MSVC ABI (which exports nothing without +`__declspec(dllexport)` or a `.def`). See `tests/e2e/08`, `257` and `259`. + +#### `exports` — the artifact’s published symbol set (mcpp 2026.9.6.5+) + +```toml +[targets.mydriver] +kind = "shared" +soname = "libmydriver.so.1" +exports = "abi/mydriver.exports" # or inline: exports = ["vk_icd*"] +``` + +**Omitting the key publishes everything, which is what both platforms already +do** — ELF gives symbols default visibility, and PE gets an auto-generated +`.def` listing every symbol. `exports` narrows that. + +Two projects need the narrowing. A **runtime with a stable ABI** publishes a +reviewed set and nothing else, so that what is not in the set stays free to +change. A **plugin loaded beside its rivals** must not collide: a Vulkan ICD is +found by name for `vk_icdGetInstanceProcAddr`, and one that also exports its +internals collides with the loader and with the other ICDs in the process. + +The file lists one symbol pattern per line, `#` starts a comment, and `*` is the +only wildcard. The inline array says the same thing and is for the two or three +entry points where a separate file would be ceremony. + +One statement, three renderings: + +| Platform | Rendered as | +|---|---| +| ELF | a version script, `-Wl,--version-script=` | +| Mach-O | `-Wl,-exported_symbols_list` (the leading underscore is supplied by the engine) | +| PE | the `.def`, replacing the auto-generated all-exports one | + +**It does not change compile-time visibility, and that is deliberate.** The +narrowing is a link-time property on all three formats, so one key has one +effect. `-fvisibility=hidden` remains available through `[build] cxxflags` for +the code-generation benefit it brings, and it is a separate decision because it +also changes how this library's own translation units see each other. + +**Symbol versioning is not this key.** `foo@@LIB_1.0` alongside `foo@LIB_0.9` +is an ELF-only capability that cannot be stated neutrally; a package that needs +it writes the version script itself and passes it through `[build] ldflags`, or +computes it and emits `mcpp:link-flag=` (docs/07). + +A `soname` is meaningful on `kind = "lib"` too — see +[`dependency_linkage`](#dependency_linkage--static-or-shared-is-the-consumers-decision) +below, where the form a library takes becomes the consumer's decision. + +#### Per-target keys + +```toml +[targets.server] +kind = "bin" +main = "src/server.cpp" +defines = ["BUILD_SERVER=1", "PORT=8080"] # -D macros, applied to this target's entry only +cxxflags = ["-Wno-deprecated-declarations"] # extra C++ flags for this target's entry (no -std=...) +cflags = ["-DPURE_C"] # extra C flags for this target's entry + +[targets.gui] +kind = "bin" +main = "src/gui.cpp" +required_features = ["gui"] # only built when feature `gui` is active +``` + +| Key | Meaning | +|---|---| +| `defines` | Preprocessor macros (`name` or `name=value`); desugar to `-D` on both the C and C++ entry compile. | +| `cxxflags` / `cflags` | Extra compile flags for this target. Do **not** put `-std=...` here — use `[package].standard`. | +| `required_features` | The target is emitted only when **every** listed feature is active in the build; otherwise it is silently skipped. A gate only — it does not activate features (use `--features` / `[features].default`). **One exception, and it is not a second rule:** when this target is requested as a host tool (`tools = [...]`, §2.14), the target is what was *asked for*, so its `required_features` become the sub-build's *inputs*. Same field, one meaning — the resolution just runs in the opposite direction. | + +> **Scope (important):** `defines` / `cxxflags` / `cflags` on a target apply **only to that +> target's exclusive entry source** (its `main`) — never to shared module/impl objects, which +> are compiled once and linked into every target (mcpp's compile-once model). They are the right +> tool when the flag only needs to affect a single binary's (or test's) own entry — for example a +> per-test contract evaluation semantic (`-fcontract-evaluation-semantic=observe`) for a test whose +> `main` exercises the violation, a feature macro the entry alone reads, or a local warning +> suppression. If a flag must reach **shared** code, it does not belong here — split into a +> [workspace](07-workspace.md) member or use `[features]`, or for a whole-build mode use a +> `[profile.*]` (`mcpp test --profile ` builds the whole test image, code-under-test +> included, under that profile). +> +> Unsupported keys under `[targets.]` are reported as a warning (an error under `--strict`). + +**Choosing where build configuration goes** — when more than one binary must differ: + +| You want | Use | +|---|---| +| Different macros/flags on a binary's **own entry** | per-target `defines` / `cxxflags` (above) | +| Two products that differ in code they **share** | split into [workspace](07-workspace.md) members, each with its own `[build]` flags over a shared `lib` | +| To **select a variant** of a shared library (e.g. a backend) | `[features]` on that library (§2.8) — additive, reaches the library's own compile | +| A **whole-build mode** (sanitizers, contract semantics, opt level) | `[profile.]` (§2.9) + `--profile`; also honored by `mcpp test --profile ` | + +mcpp deliberately does not compile a shared source two different ways within one build: a source +maps to one object (and one BMI for modules), so divergence that must reach shared code belongs at +the package/feature boundary, not on an individual target. + +### 2.3 `[build]` — Build Configuration + +> **Every entry `sources` matches must produce an object that gets linked.** A +> file mcpp cannot place — an extension outside the built-ins and outside +> `module_extensions` — is refused, naming the file, the extension and the key. +> It is not ignored, because the failure that produced this rule was not "one +> file too many" but *compiled and then linked by nobody*: the scanner reads +> `export module` and gives the edge a BMI while the classifier says the file has +> no role, and what the author sees is `undefined reference` to a module-mangled +> symbol. Headers belong in `include_dirs`; Windows resource scripts in +> `[resources]`. + +> **`sources = []` is not the same as omitting `sources`.** An absent key +> selects the default glob; an explicitly empty list means *compile nothing*, +> which is what a header-only distribution package needs to say. Until +> mcpp 2026.8.18.1 the two were byte-identical, so there was no spelling for +> "nothing" and any file left under `src/` was swept in. + +> **A `sources` entry may carry the accelerator it is for** (2026.9.5.2+): +> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`. The glob +> joins the list like any other; the constraint decides whether it applies to a +> given build. It must match at least one file (an empty match is refused: it +> would leave nothing to compile for that device and say so only at the link). +> Under `--no-accel` the glob is left out, which is how one project yields its +> CPU-only variant. Under an `--accel` that does not cover the constraint the +> build is refused naming both (`accel-mismatch`). Device-kind files the +> effective set matches — CUDA and HIP, the GLSL stages, HLSL, OpenCL C and +> Metal, listed in full in [42 — Heterogeneous Builds](42-heterogeneous-builds.md) — are never +> compiled by the engine; they reach the build program as +> `MCPP_DEVICE_SOURCES`, where the rule package the project imports turns each +> into an `mcpp::action`. + +```toml +[build] +sources = ["src/**/*.cppm", "src/**/*.cpp"] # Source globs (default: src/**/*.{cppm,cpp,cc,c,S,s,asm}) +module_extensions = [".ixx"] # Extra extensions used by module INTERFACES (§ below) +build_program_timeout = 1800 # Seconds a build.mcpp may run; 0 = no limit (§ below) +include_dirs = ["include", "third_party/include"] # Header search paths +include_dirs_after = ["*"] # Header dirs searched AFTER system dirs (-idirafter) +private_include_dirs = ["vendor/src/include"] # Of `include_dirs`, the ones a consumer must NOT get +c_standard = "c11" # Standard for C source files (default c11) +cflags = ["-DFOO=1"] # Extra C compile flags +cxxflags = ["-DBAR=2"] # Extra C++ compile flags (do not put -std=... here) +ldflags = ["-lfoo"] # Extra link flags +defines = ["BIZ=1", "QUX"] # Preprocessor macros for every TU (desugars to -D; reaches module scans) +cxx_runtime = "self-contained" # C++ runtime contract (§ below); static_stdlib is the old spelling +target = "x86_64-linux-musl" # Default build target when no --target is passed + # (≙ cargo build.target; e.g. "ship fully-static") +macos_deployment_target = "14.0" # Minimum supported OS version for macOS artifacts (macOS only) +dependency_linkage = "static" # How dependencies arrive: static (default) | shared (§ below) +cache = "global" # Global dependency cache: global (default) | local | off (§2.10) +jobs = "auto" # Concurrent compiles: a positive number, or "auto" (§ below) +bmi_schedule = "auto" # Module-edge scheduling: auto (= off) | on | off (§ below) +``` + +#### `dependency_linkage` — static or shared is the consumer's decision + +```toml +[build] +dependency_linkage = "shared" # whole-graph default; "static" is the default default + +[profile.dev] +dependency_linkage = "shared" # per profile + +[dependencies] +"compat.zlib" = { version = "1.3.2", linkage = "shared" } # one package +``` + +Until mcpp 2026.8.28.2 a dependency had exactly one shape and the *package +author* chose it: `kind = "lib"` merged its objects into every consumer's link, +`kind = "shared"` produced a real shared library. That is the wrong owner for +the decision. Whether a library should be a separate file at run time is a +property of the **program being built** — how it is shipped, how often it is +relinked, whether something else in the process already provides that library. + +- **`static`** (default) — the dependency's objects are merged into the images + that use it. Byte-for-byte what mcpp has always done; a project that does not + write this key builds exactly as before. +- **`shared`** — mcpp builds the dependency as a shared library beside the + artifact and links against it, with `$ORIGIN` (ELF) / `@loader_path` (Mach-O) + / the executable's own directory (PE) finding it again after the build + directory moves. + +**This is not `[target.].linkage`** (§2.7.1). That key answers the +same-sounding question about the **C library** (a musl `-static` link, MSVC's +`/MT`). The two are not independent, and the direction matters: a fully static +image has no interpreter, so it cannot load a shared object at all. On a target +whose C library is linked statically — which is the **default for musl** — +`dependency_linkage = "shared"` is refused, and says so. + +**A package can say it must be one form**, and only for a real reason: + +| The package writes | mcpp reads it as | +|---|---| +| `[targets.] kind = "shared"` | *must* be shared — something else in the process will `dlopen` it, so there may only be one copy (X11, a Vulkan loader) | +| `ldflags` containing `-L` | *must* be static — the package ships prebuilt archives mcpp did not compile and cannot place inside a shared object it builds | +| a packaged library (`mcpp pack`) | whichever legs it actually ships, from `[[runtime.artifacts]] role` | +| anything else | either form | + +`kind = "lib"` is **not** a constraint: it is the default value, and most +packages write it without choosing anything. Absence of a statement is not a +statement. + +A per-dependency `linkage` is honoured **only in the root project's** +`[dependencies]`. A package deep in the graph does not get to decide how the +final program is laid out; one that genuinely must be a single shared copy says +so on its own target instead. + +#### `soname` on a library target + +A `soname` (§2.2) may be declared on `kind = "lib"` as well as +`kind = "shared"`. It is the name a library is *found* by, and it is the only +way mcpp's build of a package and a third party's copy of the same library can +resolve to **one file** instead of two — which a package cannot state if +declaring it forces the package to stop being consumable as a static library. + +A descriptor that writes `soname` on a non-shared target cannot be read by +mcpp releases before 2026.8.28.2 — the whole manifest fails to load, not just +the key. Publishing one to an index therefore waits for that floor to move. + +#### The symbol-provision check + +After a link, mcpp asks whether every symbol in the image has exactly **one** +provider. On ELF an executable is searched first, so a library statically +merged into the program wins for every symbol it shares with a shared library +loaded beside it — the shared copy is never called, and code inside that +library runs against a build it was not linked against. No linker or loader +diagnostic exists for this. + +The check is a measurement, not a declaration: it reads the produced image's +dynamic symbol table, removes the entries that are copy relocations, and +reports only those a library in the artifact's own closure **also** defines. +An arrangement with one copy in the process is silent. The verdict is recorded +in `target///resolution.json` under `runtime.symbol_provision`, +with the count and its denominator, so CI can read it without `readelf`. + +It is a warning by default and an error under `--strict`. The ways out are +ordered, and the order matters: + +1. **Stop one side from providing it** — usually a package shipping a copy of a + library the graph already builds. Always correct. +2. **Make both resolve to one file** by declaring the library's real `soname` + on its target. +3. **`dependency_linkage`** changes which form mcpp builds. It removes *this* + finding, but on its own it can leave **two** copies loaded instead of one: + measured on a graph staging glib (whose `libgio` needs `libz.so.1`) beside a + statically built `compat.zlib`, switching the form dropped the executable's + 88 exported symbols and then loaded both `libzlib.so` and `libz.so.1`. It + unifies the two providers only when (2) holds as well. + +`private_include_dirs` names the entries **of `include_dirs`** that stop at this +package's own boundary: this package compiles with them, and a consumer never +receives them. + +Almost every package publishes exactly the set it is built from, which is why +`include_dirs` alone was enough for a long time. The shape where the two differ +is a package that vendors a library with an *internal header overlay*. musl +reaches its own declarations through `src/include`, whose headers define +`hidden`, `weak` and `weak_alias` — names that mean something only to musl's own +sources. Publishing that directory hands those macros to every consumer, and a +consumer that uses `hidden` as an ordinary identifier stops compiling for a +reason it has no way to see. + +```toml +[build] +# The relative ORDER of the two kinds is load-bearing: the internal overlay +# must precede the public headers for this package's own build. That is why +# this is a SUBSET of `include_dirs` rather than a second list — two arrays +# cannot express one order. +include_dirs = ["port/include", "musl/src/include", "musl/include"] +private_include_dirs = ["musl/src/include"] +``` + +Entries take the same `*` glob convention as `include_dirs`, and are matched +after expansion — so a glob may name exactly the directories it expands to. An +entry that is not among this package's `include_dirs` withholds nothing and is +reported as such rather than passing in silence. + +**On an older engine the key is ignored, never fatal.** Measured on 2026.8.26.2: +in a dependency's manifest it is accepted silently, and in a root manifest it +warns — `[build] has unsupported key 'private_include_dirs' (ignored)` — and the +build continues. So a package may adopt the key without waiting for its +consumers to upgrade; those on an older engine simply keep receiving the +directory as they did before. The one place this does **not** hold is a +published `xim` descriptor's `target_cfg` block, where an unrecognised sub-key +is a hard error that fails the whole manifest — do not put this key there until +the index floor names an engine that knows it. + +`include_dirs_after` (#249) lists header directories that are searched **after** +the toolchain's system directories (emitted as `-idirafter` on GCC/Clang, as +trailing `/I` under the MSVC dialect, and as plain `-I` for NASM +assembly units — neither has an equivalent, and neither has a system-header +chain to protect). Use it instead of +`include_dirs` when the directory is an extracted source-tarball root that +contains files whose names collide with standard headers — e.g. ffmpeg's +top-level `VERSION` file shadows libc++'s `` on case-insensitive macOS +filesystems when the root is put on `-I`. With `include_dirs_after` the system +header always wins while the package's real headers (``) +remain findable. Entries support the same `*` glob convention as +`include_dirs`, and they propagate to dependent packages along the same edges — +consumers receive them as after-dirs, never upgraded to `-I`. + +`macos_deployment_target` sets the minimum system version in the artifact's +Mach-O header (`LC_BUILD_VERSION minos`), i.e. the oldest macOS the binary can +run on. The precedence follows ecosystem convention: the `MACOSX_DEPLOYMENT_TARGET` +environment variable (an explicit per-invocation override, honored the same way by +cargo/rustc, cc, etc.) > this field (the project default, similar to SwiftPM's +`platforms:`) > the **built-in default `14.0`** (rustc-style — every target has a +baseline, and 14.0 is the floor of LLVM's official static libraries themselves). +This value enters the BMI fingerprint, so switching targets automatically rebuilds +the module cache. + +### Build concurrency (`jobs`) and module scheduling (`bmi_schedule`) + +```toml +[build] +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 +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 +single module interface compile peaks at 0.5–1.0 GB. Precedence is +`--jobs` / `MCPP_JOBS` > this key > the backend's own default. A malformed value +is **reported, never silently treated as the default** — a typo that quietly +restores the default is a build slower than requested, with no indication why. + +`bmi_schedule` decides when importers are unblocked. + +| value | | +|---|---| +| `"auto"` | **the default, and it currently means OFF** | +| `"on"` | split the module edge: importers start when the BMI is published, not when the compiler exits | +| `"off"` | one edge per module | + +Only those three spellings are accepted. `"ON"`, `"true"` and `"yes"` are +**rejected with a diagnostic** rather than quietly meaning off — and they are not +harmless typos: the value enters the build fingerprint, so a rejected spelling +used to select a different build directory (a full rebuild) while changing +nothing about the schedule. + +**Why `auto` is off.** 86% of a module interface compile is code generation that +no importer reads, so publishing the BMI early is worth a lot — measured on mcpp +itself, `cold` 86.7s → 35.7s and `edit-body` 80.9s → 29.8s. But a scheduling +change that is wrong is wrong *silently*: a missed dependency does not fail the +build, it just stops rebuilding something. It stays opt-in until it has been +through CI on every platform. + +**What it does not help.** Where mcpp already skips the cascade — `touch-hub`, +`edit-comment` — there is no owed work to move off the critical path, and the +key buys nothing. See the [benchmark](../README.md#benchmark). + +**The mechanism** differs per compiler and is selected automatically: gcc publishes its +BMI with `rename()`, so code generation is detached and the edge returns at +publication; clang gets two ordinary edges instead, because it writes the BMI to +its final path with `O_TRUNC` and a reader could observe a half-written file. +MSVC is left alone — neither `/ifcOnly`'s cost nor `.ifc` atomicity has been +measured, and guessing either wrong is silent. + +### Module interface extensions (`module_extensions`) + +mcpp treats `.cppm` as a module interface unit. The C++ ecosystem has not +converged on one spelling — Clang also recognizes `.ccm` and `.cxxm`, MSVC uses +`.ixx` — so a project whose interfaces use another extension declares it: + +```toml +[build] +module_extensions = [".ixx", ".ccm"] +``` + +The list is **additive**: `.cppm` is always a module interface and cannot be +removed. To stop a particular file from being built, `!`-exclude it in +`sources`; that is what `sources` is for. + +Declaring an extension does three things at once, which is the point of having +one key rather than several: + +1. the convention default for `sources` grows to match, so the files are + **found** (`src/**/*.ixx` joins the default glob); +2. those units compile with the **module** rule — they emit a BMI and their + objects are linked unconditionally; +3. the freshness fast path watches them, so adding an `import` to one + invalidates the build graph instead of silently reusing a stale one. + +Any extension is accepted **except** ones that already name a non-module role +(`.cpp` `.cc` `.cxx` `.c` `.m` `.mm` `.h` `.hpp` `.hh` `.hxx` `.S` `.s` +`.asm`); claiming one of those is a manifest error rather than a warning, +because it would route (say) C files to the C++ module rule and fail somewhere +that names neither the file nor this key. + +Extensions are matched **literally, without case folding** — `.S` and `.s` are +different languages in this domain, so case is never ignored. + +mcpp always tells the compiler explicitly that a module interface unit is one +(`-x c++-module` on Clang, `-x c++` on GCC, `/interface /TP` on MSVC), so an +extension the compiler driver has never heard of works anyway. This is why any +extension is allowed: mcpp does not need the compiler to recognize it. + +> **Publishing note.** An older mcpp does not know this key: it warns, ignores +> it, and then compiles those files as ordinary translation units — a wrong +> build rather than a clean failure. A published package that uses +> `module_extensions`, declare an mcpp version floor in its index descriptor. + +### Build-program timeout (`build_program_timeout`) + +A `build.mcpp` gets **600 seconds** by default, after which mcpp kills it and +fails the build naming the package. A project whose build program legitimately +runs longer (a large code-generation step) raises its own bound: + +```toml +[build] +build_program_timeout = 1800 # seconds; 0 = no limit +``` + +The value is read from **the manifest of the package that owns the +`build.mcpp`** — a dependency's generator is bounded by the dependency's own +declaration, because its author is the one who knows how long it takes. The +precedence follows the same shape as `macos_deployment_target`: + +``` +MCPP_BUILD_PROGRAM_TIMEOUT= (this invocation; highest) + > [build] build_program_timeout (that package's manifest) + > 600 (built-in default) +``` + +Leaving the key out is not the same as setting `0`: unset means "use the +default bound", `0` means "no bound at all". + +This value is deliberately **not** part of the build fingerprint — it changes +no edge in the graph, and folding it in would mean that raising a timeout +rebuilt the whole project, which is the opposite of what someone raising a +timeout wants. + +The **compile** phase is not bounded, only the build *program*. See +[30-build-mcpp.md](30-build-mcpp.md) for why that asymmetry is deliberate. +### The C++ runtime contract (`cxx_runtime`) + +Moved to [20 — Toolchain Management](20-toolchains.md). + + +### File names outside the host code page + +Globs are narrow strings, and so are compile commands and `build.ninja`. On +Windows those strings are produced in the process's **ANSI code page**, so a +file whose name has no spelling in that code page cannot be matched by a glob, +named on a compile command, or written into a build file. + +Such entries are skipped, and the skip is reported once per directory: + +```text +warning: 'C:/.../pkg/test/www' contains names this system's active code page cannot represent + impact: those files take no part in the build + hint: Windows only: this is the process ANSI code page, which `chcp` does not change. ... +``` + +The reported path is the nearest ancestor whose name the code page *can* spell, +in generic (`/`) spelling. The offending name itself is never printed: rendering +it would throw the same exception the message is reporting. + +`chcp` sets the *console* code page and has no effect here. Names that are only +test data or documentation are harmless — an upstream tarball carrying a +Japanese-named fixture directory builds fine on an en-US host. Sources are not: +they need renaming, or a host whose code page covers them. + +Linux and macOS perform no such conversion, so nothing is skipped there. A +package that builds on one and not the other, with an +`internal: unhandled exception` from a code-page message, was mcpp#516. + +### 2.3.1 `[build] accel` — the accelerator this build targets + +```toml +[build] +accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" +``` + +Which device backends and architectures this build compiles for. Overridden for +one build by `--accel`, the relationship `--target` has with `[toolchain]`; +`--no-accel` requests none explicitly, which is how a CPU-only variant of a +package that also publishes device builds is selected. + +The value is compared against the `accel` field of any prebuilt artifact the +build consumes, and a build asking for none is satisfied by every artifact. See +[42 — Heterogeneous Builds](42-heterogeneous-builds.md). + +### 2.4 `[lib]` — Library Root Module Convention + +```toml +[lib] +path = "src/capi/lua.cppm" # Override the default lib-root location +``` + +Default convention: `src/.cppm` (e.g. package name `mcpplibs.cmdline` → `src/cmdline.cppm`). +### 2.5 `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]` + +Moved to [05 — Dependencies and Resolution](05-dependencies.md). + +### 2.7 `[toolchain]` — Toolchain Configuration + +```toml +[toolchain] +default = "gcc@16.1.0" + +# Cross-compilation target override +[target.x86_64-linux-musl] +toolchain = "gcc@16.1.0" +linkage = "static" +``` +### 2.7.1 `[target.*]` — Platform-Conditional Dependencies & Flags + +Moved to [22 — The Target Side](22-target-side.md). + + +### 2.7.2 Bare metal (`os = none`) — freestanding targets + +`riscv64-none-elf` and `riscv32-none-elf` are targets with no operating system +underneath. They need no per-host cross toolchain: clang and lld are +cross-compilers by construction, so any host that can install the llvm payload +can produce them. + +This section is the manifest reference. The worked examples — scaffolding, +running, testing on the target, the freestanding standard-library subset and +writing a board-support package — are in +[40 — Bare-Metal and Freestanding Targets](40-baremetal.md). + +```bash +mcpp build --target riscv64-none-elf +mcpp run --target riscv64-none-elf # via [target.].runner +``` + +**Starting from a board package** + +Almost nothing below has to be written by hand. A board-support package carries +the C library, the startup code, the memory layout and the emulator, so the +shortest path to a booting image is: + +```bash +mcpp new blinky --template riscv-virt-rt +cd blinky && mcpp run +``` + +The generated manifest names no linker script, load address, libc or emulator — +it has no `[target.*]` section at all. The rest of this section describes what +such a package supplies, which is what to reach for when writing one for a board +that has none. + +**What changes on a freestanding target** + +| | | +|---|---| +| Link line | `-nostdlib -nostartfiles -static`, and nothing hosted — no crt files, no dynamic linker, no C++ runtime. The linker is addressed by **absolute path** (`-fuse-ld=/bin/ld.lld`), because `-fuse-ld=lld` resolves through `PATH` and finds GNU ld on any machine with binutils earlier on it. | +| ISA flags | `-march` / `-mabi` / `-mcmodel` come from the target table, so `--target ` alone is enough to produce a correct object file. | +| C library | **The target's**, resolved by mcpp from the target's own row exactly as the compiler is — a bare-metal project declares no libc, just as a hosted one declares no glibc. Its headers reach every translation unit and its directory is on the link search path, so a board package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). *Which* objects and *which* linker script remain board decisions. | +| Exceptions and RTTI | **Off**, on every translation unit including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise pull in `__cxa_throw` and three more undefined symbols. It belongs to the target rather than to a project's `cxxflags` because a BMI records it — a dependency compiled with exceptions cannot be imported by a unit without them. | +| `import std` | **Unavailable.** `std` is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. Two ordinary dependencies replace it: the **board package** wraps the target's C library, and **`std-freestanding`** carries the parts of the standard library that need no OS (103 of libc++'s 110 headers, measured). | +| Entry point | `int main()` works **as long as something supplies a `crt0`** — a board package normally does, and then a firmware's entry point is an ordinary `main` whose return value reaches the host through semihosting. Only a zero-libc board needs an explicit target whose `main` points at the file carrying `_start`. | + +**A minimal firmware** + +```toml +[package] +name = "fw" +version = "0.1.0" + +[build] +ldflags = ["-T", "/abs/path/to/link.ld"] + +[targets.firmware] +kind = "bin" +main = "src/start.S" # the entry lives in assembly, not in main() + +[target.riscv64-none-elf] +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +**`runner` — how `mcpp run` executes something this machine cannot run** + +A bare-metal image has the wrong ISA, no loader, and expects to own the address +space; exec'ing it directly gives "Exec format error". `runner` is the argv +template that stands in front of it. The artifact path is **appended**, or +substituted for `{}` when the template contains it. + +mcpp ships **no default runner**, deliberately. Which emulator, which machine +model and which firmware mode are board facts — two boards on the same ISA need +different argv (`-bios default` for an OpenSBI boot, `-bios none -semihosting` +for a picolibc image) — and an engine that guesses one is an engine the other +board has to fight. A board-support package normally supplies it. + +### 2.7.3 `runner` on a hosted target (2026.9.2.1+) + +`[target.].runner` applies to every exact triple, not only to bare +metal. A hosted cross artifact — `aarch64-linux-musl` built on an x86_64 +machine — is executable by some hosts (binfmt_misc with qemu-user registered) +and refused by others with `Exec format error`, and which of the two applies is +a property of the machine, not of the triple. mcpp does not predict it. It +either executes the artifact through the runner the project declared, or it +attempts direct execution and reports what the kernel answered. + +```toml +[target.aarch64-linux-musl] +runner = ["qemu-aarch64-static"] +``` + +The rules, for `mcpp run` and `mcpp test` alike: + +- **A declared runner is used.** Its first element is located by mcpp: first in + the `bin/` directory of each payload declared under `[xlings.workspace]` + (§2.13), + then on `PATH`. A bare name on `PATH` resolves to an xvm shim, which answers + for the current SubOS rather than for the package; the payload lookup is what + lets a runner name a program the project declared. +- **A declared runner that cannot be found or started is an error**, with the + program, the directories searched and the errno. There is no fallback to + direct execution: running the artifact under a different interpreter with + different arguments is the failure the key exists to prevent. +- **No runner, and the kernel refuses the artifact:** `mcpp run` reports the + refusal and the key to write, and exits 2. `mcpp test` reports every test as + not run, with the reason once, and exits 2 (§2.7.3.1). +- **`--no-runner`** executes the artifact directly and ignores a declared + runner. It states a fact about this host — the triple is native here — that + the manifest has no axis to carry; a project whose runner was written for + x86_64 developers is still readable on an aarch64 machine. + +Provisioning the emulator through `[xlings.workspace]` is the form for a CI job +or a project built on one host class. `qemu-user-aarch64` in the index is built +for x86_64 Linux only, and the table provisions on every host that builds the +project, so the entry is written per platform (§2.13): + +```toml +[xlings.workspace] +"xim:qemu-user-aarch64" = { linux = "" } # present on Linux, any version + +[target.aarch64-linux-musl] +runner = ["qemu-aarch64-static"] +``` + +A package the host cannot install is a hard build error, so an entry without +the platform form would make the project unbuildable on macOS and Windows. The +Linux/aarch64 host, where the package does not exist either, passes +`--no-runner`. + +#### 2.7.3.1 `mcpp test` and tests that were not run + +A test whose artifact this host cannot execute has neither passed nor failed. +`mcpp test` reports it as **not run**, prints the reason once when it is +established, repeats the first line of the reason in the summary, and exits 2: + +``` +warning: this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); declare [target.aarch64-linux-musl].runner, or pass --no-runner on a host that can +smoke ... not run +error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); ...); finished in 0.41s (build 0.39s + run 0.00s) +``` + +Exit code 1 keeps its meaning — a test ran and failed — and 0 means every test +ran and passed. `--message-format json` carries `"status":"not_run"` and a +`reason` on each record, and `not_run` / `not_run_reason` on the summary +record (see [50 — Machine-Readable Output](50-machine-output.md)). +### 2.8 `[features]` — Features + +Moved to [06 — Features and Capabilities](06-features-and-capabilities.md), +with `provides` / `requires` and `[feature-deps.]`. + + +### 2.8.3 `[scan_overrides.""]` — Author-Asserted Scan Results + +The default module scanner is a text-level pass that (deliberately) rejects +`import` statements inside conditional preprocessor blocks. Some legitimate +module units carry them — e.g. fmt's official `src/fmt.cc` guards +`import std;` behind `#ifdef FMT_IMPORT_STD`. When the file's import set is +known and stable, declare it instead of scanning: + +```toml +[modules] +sources = ["src/**/*.cppm", "vendor/fmt.cc"] + +[scan_overrides."vendor/fmt.cc"] +provides = ["fmt"] # at most one provided module per unit +imports = ["std"] +``` + +Files matched by the glob skip the text scan; the declared unit enters the +module graph directly. The declaration is **audited every build**: the +compiler's own P1689 scan of the file (the `.ddi` dyndep input) is compared +against it, and any divergence fails that compile edge with both sides +printed — a stale declaration cannot silently corrupt the graph. An override +glob that matches no source file is an error. + +The same key exists in xpkg descriptors (index packages): + +```lua +mcpp = { + sources = { "*/src/fmt.cc" }, + cxxflags = { "-DFMT_IMPORT_STD" }, + scan_overrides = { + ["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } }, + }, +} +``` + +To extend the plan-vs-ddi audit to *every* module unit (not just overrides), +set `MCPP_VERIFY_MODGRAPH=1` when generating the build. + +### 2.9 `[profile.]` — Build Profiles + +```toml +[profile.dist] +opt = 3 # -O level (a number, or the string "s"/"z") +debug = false # -g +lto = true # -flto (note: some packaged gcc builds ship without the LTO plugin) +strip = true # -s at link time +# passthrough escape hatch (fixed keys, open values): +cflags = ["-fno-plt"] +cxxflags = ["-fno-plt"] +ldflags = [] +``` + +- Selection & default: a bare `mcpp build` uses the **`dev`** profile (`-O0 -g`) — the + mainstream convention (cf. Cargo/Meson/CMake/Zig/Bazel). **Release is opt-in:** + `mcpp build --release` (shorthand) or `--profile release`. `--dev` is the explicit + shorthand for dev. Same applies to `mcpp test --profile ` (builds the + code-under-test plus the test binaries under that profile). +- **Per-project default** — `[build].default-profile = ""` (alias: `profile`) sets + the project's own default when no flag is passed. The typical use is a tool/library + that should build optimized by default: `[build] default-profile = "release"`. Precedence: + `--profile`/`--release`/`--dev` flag **>** `[build].default-profile` **>** global `dev`. + (A project that defaults to dev should pass `--release` when producing a distributable.) +- Built-in profiles: `release` (-O2) / `dev`, `debug` (-O0 -g) / `dist` (-O3 + strip; + **LTO is not enabled by default**). `[profile.]` can override a + built-in definition wholesale. +- **Each profile owns its own build directory.** The resolved profile knobs + participate in the fingerprint, so `target//` holds one hash directory + per profile and switching between them is incremental instead of a full + rebuild. It also means the disk cost scales with the number of profiles + actually use. + +### 2.10 `[build] cache` — The Global Dependency Cache + +Compiled artifacts for dependencies fetched from an index are cached across +projects under `$MCPP_HOME/build-cache/v1/`. A dependency's artifacts do not +depend on who consumes them, so two projects with the same toolchain, profile and +dependency versions reuse one entry. + +```toml +[build] +cache = "global" # "global" (default) | "local" | "off" +``` + +| Mode | Reads the cache | Writes the cache | Clears the build dir first | +|---|---|---|---| +| `global` (default) | yes | yes | no | +| `local` | no | no | no | +| `off` | no | no | yes | + +`local` builds every dependency inside this project's `target/` — useful to rule +the cache out while diagnosing something, and to give CI a no-sharing baseline. +`off` additionally clears this build's `target///` for a cold +rebuild; `--no-cache` is a deprecated alias for it. + +Precedence: `--cache ` **>** `MCPP_BUILD_CACHE` **>** `[build] cache` **>** +`global`. An unrecognized value is reported (an error under `--strict`) rather +than silently falling back to `global`. + +What is **not** cached: `path` and `git` dependencies, at any depth, and +workspace members. Their sources can change without their `name@version` +changing, so no key over that identity could notice the change. + +Inspection and reclamation: + +``` +mcpp cache dir # where the cache lives +mcpp cache list [--json] # entries, sizes, last use +mcpp cache info @ # one entry, including the key inputs it was built with +mcpp cache verify # every entry's file list against the disk +mcpp cache gc --max-size 5GiB # LRU-collect package entries to a budget +mcpp cache gc --older-than 30d # ...or by how long since they were last used +mcpp cache clean [--deps|--std|--all|--legacy] +``` + +The on-disk entry layout is versioned. An mcpp release that changes it retires +every older entry at once, so the first build after such an upgrade rebuilds +its dependencies and repopulates — nothing to clean by hand. 2026.8.3.4 did +exactly that: an entry's object paths are now addressed relative to the +*package*, never to the build directory of whichever project happened to +populate the entry first. `mcpp cache verify` additionally reports any entry +whose recorded addresses escape it, so a recurrence is auditable offline. + +### 2.11 `[runtime]` — Provider-neutral runtime contract + +```toml +[runtime] +requirements = [ + { kind = "capability", value = "display.present", phase = "run", required = true, + discovery = "rpath-of-dispatch" }, + { kind = "soname", value = "libwidget.so.1", phase = "link", required = false }, +] +provides = ["display.present"] +artifacts = [ + { role = "library", path = "runtime/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:...", host_fingerprint = "host-1" }, +] + +# Platform-neutral LinkIntent. Paths are relative to this package root. +libraries = ["widget"] +link_library_dirs = ["lib"] +transitive_needed_dirs = ["runtime/closure"] +runtime_search_dirs = ["runtime"] +frameworks = ["WindowKit"] +deploy_files = ["bin/widget.dll"] + +# Use an exact canonical identity when multiple providers exist. +[runtime."display.present"] +provider = "acme.widget-runtime@2.0.0" +``` + +`requirements` records a non-empty `kind`/`value`, a `link` or `run` phase, +and whether the requirement is mandatory (`required` defaults to `true`). + +`discovery` is optional and says **how the loader finds** whatever satisfies the +requirement — e.g. `rpath-of-dispatch`, `json-dir`, `glvnd-dispatch`. It is +**declared, never inferred**: which mechanism a capability uses is the +provider's property and changes without mcpp, so mcpp carries the value and +reports an undeclared one as `unknown` rather than guessing. It earns a field +because the mechanisms are not interchangeable — one may be a search path baked +into a dispatch library, another a JSON file holding an *absolute* path, so +"copy the directory across" satisfies one and not the other. `mcpp pack` writes +it into the bundle's `HOST-REQUIREMENTS` and `mcpp publish` projects it into +the descriptor, from one derivation. +Optional requirements remain visible provenance but do not become hard ABI or +doctor inputs. A `libraries` entry that is an explicit relative file path is +resolved against the declaring package root; a bare logical name remains a +platform-spelled library name. +`artifacts` requires `role`, `path`, and `provenance`; `abi`, `digest`, and +`host_fingerprint` are optional evidence. The resolver, not the descriptor, +stamps every requirement with the exact requester PackageId and every artifact +with the exact declaring provider PackageId, including namespace, version, and +source/index provenance. A descriptor therefore cannot spoof another package, +and `alpha.backend` never collapses into `beta.backend`. + +Only `provides` creates a descriptor-owned provider fact. Merely requiring a +capability never makes the requester its own provider. An explicit +`[runtime.] provider=` override accepts a canonical +`namespace.name@version` (or an unambiguous compatibility spelling); missing or +same-short-name ambiguous providers are hard errors. Provider/artifact facts +already selected by the xlings SubOS precede descriptor fallbacks. xlings/xim +owns graphics-stack, driver, ICD, WSL, and host provenance selection; mcpp +records and consumes the generic result and never probes GPU hardware. + +Link intent keeps discovery stages separate: + +| Field | ELF | Mach-O | PE/Windows | +|---|---|---|---| +| `link_library_dirs` | `-L` | `-L` | `-L` or `/LIBPATH:` | +| `transitive_needed_dirs` | `-Wl,-rpath-link` | no flag | no flag | +| `runtime_search_dirs` | RUNPATH/rpath only, never `-L` | rpath only | no flag | +| `frameworks` | no flag | `-framework` | no flag | +| `deploy_files` | copy edge | copy edge | copy beside the output; never a linker flag | + +For one compatibility train, `library_dirs` maps only to runtime search, +`dlopen_libs` maps to required run-phase soname requirements, and +`capabilities` maps to required run-phase capability requirements. None of +these legacy fields creates a provider. + +`target///resolution.json` schema 2 stores the RuntimeBinding, +canonical requirements/providers/artifacts, LinkIntent, platform search +mechanism, and post-link verdict. `mcpp why runtime` is a pure interpreter of +the latest stored file: it neither re-resolves the manifest nor launches a +graphics/hardware probe. Use `xlings doctor` when the selected host provider +itself needs re-diagnosis. + +Each artifact also carries an `identity` verdict, computed from paths alone: + +| `identity` | meaning | +|---|---| +| `ok` | the declared path resolves (through symlinks) into the declared version | +| `mismatch` | it resolves somewhere else — **the binding is stale**, a later install repointed it | +| `missing` | declared, but nothing is at that path | +| `unverified` | declared without a version to check against | + +This is the rule mcpp already applies to the private libc (`glibc@2.44` resolves +that one payload; stale or missing is an error, never "whichever installed +version looks usable"), generalised. It needs no knowledge of what the artifact +does. `unverified` is deliberately not `ok`: a resolved provider with no +artifact behind it has not been checked, and `mcpp why runtime` says +`(not declared by the environment — nothing to verify)` rather than `(none)`. + +Capability names use layered lowercase `domain.sub.role` (for example +`display.present`) and prefix-style `abi:` (for example `abi:glibc`, which +participates in toolchain ABI enforcement). + +### 2.12 `[package] platforms` — Platform Declaration + +```toml +[package] +platforms = ["linux", "macos", "windows"] +``` + +Declares the platforms the package supports (a CI matrix hint, shown via `mcpp why`). +The vocabulary is fixed by mcpp (which owns the target/triple system): +`linux | macos | windows`; unknown values produce a warning, and an error under +`--strict`. + +`mcpp pack` on a library target checks the claim against the legs it actually +produced, because that is the first moment there is evidence to check it against: + +| situation | result | +|---|---| +| a leg was packed for a platform not listed here | warning — the manifest disclaims a platform the package demonstrably serves | +| a listed platform has no leg, **and this host could have built one** | warning — consumers there will resolve the package and find no artifact | +| a listed platform has no leg and this host cannot build for it | **silent** | + +The third row is why the check is usable at all. The normal release flow is one +`mcpp pack` per platform in CI, so a Linux runner never produces a macOS leg — +warning about it would fire on every run of every cross-platform package, and a +warning that always fires hides the one that matters. What "this host could have +built" means is the same question `--target` answers (docs/08 §7.4). + +Both are warnings, never errors: coverage is release discipline, and the person +who can judge it is looking at the release, not at this build. + +### 2.12b `[package] accelerators` — Accelerator Declaration + +```toml +[package] +accelerators = ["cuda", "rocm"] +``` + +Declares the accelerator backends the package supports. Mirrors `platforms`: a +statement of intent and a CI-matrix hint, shown by `mcpp why`, never a gate. + +Distinct from an artifact's `accel` field on purpose. A declaration is written +by hand and may be aspirational; `accel` is measured from the build that +produced a binary and is what a consumer is refused against. See +[42 — Heterogeneous Builds](42-heterogeneous-builds.md). +### 2.13 `[xlings]` — the project's environment + +Moved to [23 — The Project Environment](23-the-project-environment.md). + +### 2.14 Host tools from a dependency + +Moved to [30 — build.mcpp](30-build-mcpp.md). + + +### 2.15 `[resources]` — Metadata and Assets Embedded in the Artifact (2026.8.7.1+) + +An exe icon and the version metadata Windows shows in a file's Properties dialog +are a path in `mcpp.toml`, nothing more: + +```toml +[resources] +icon = "assets/app.ico" +``` + +That is the whole common case. `FILEVERSION`, `ProductName`, `FileDescription`, +`CompanyName` and `LegalCopyright` all default from `[package]`, and mcpp +generates the resource script automatically. + +| Key | Type | Meaning | +|---|---|---| +| `icon` | path | Embedded as the application icon (resource ordinal 1) | +| `files` | list of paths | Your own `.rc` scripts, compiled and **tracked** as build inputs | +| `extra-inputs` | list of paths | Inputs the `.rc` scanner could not see (see below) | +| `version-info` | bool | `false` opts out of the generated version resource | +| `[resources.version-info]` | table | `company`, `product`, `description`, `copyright`, `original-filename`, `internal-name` | + +**Only PE targets *compile* this.** On Linux and macOS the section is +*inapplicable*: no resource units, no diagnostics, byte-identical build. You do +**not** need (and cannot use) a `cfg(windows)` predicate — write it once, +unconditionally. + +**A declared file that does not exist fails the build — on every target.** A +resource is a build input like a source file; mcpp will not quietly ship a +binary without it. Validation is deliberately *not* PE-gated: whether a path +exists is a fact about the working tree, not about the target, so a typo in +`icon = "assets/app.ico"` is caught by the Linux or macOS build (and by their +CI jobs) instead of waiting for the Windows one. To omit the icon, +delete the line. + +**Version fields.** `FILEVERSION` takes the four numeric segments of +`[package].version`, each of which must fit in 16 bits; the string fields keep +the version verbatim, so a form the numeric fields cannot hold (`1.0.0-rc1`) +still shows up in the Properties dialog. + +#### Supplying a hand-written `.rc` + +```toml +[resources] +files = ["res/app.rc"] +``` + +With `files` set, mcpp stops generating a version resource, and the +resource ID space belongs to the project. Set `version-info = true` alongside it for both (and +mind the collision: there can be only one `RT_VERSION` at ordinal 1). + +To start from the generated script instead of a blank file, copy it out of the +build directory (`target///res/.mcpp.rc`) and list it in +`files`. The result is byte-identical, so moving from generated to hand-written +never changes what ships. + +> **`VS_VERSION_INFO` needs ``.** In a hand-written script, +> `VS_VERSION_INFO VERSIONINFO` without `#include ` files the version +> resource under a *string* name instead of ordinal 1. Every tool still reports +> `Type: VERSIONINFO`, but `GetFileVersionInfo` looks up the ordinal, so +> PowerShell's `FileVersionInfo` shows every field as empty. Either include +> `` or write `1 VERSIONINFO`. mcpp warns when it sees this shape; +> the script it generates uses the literal `1`. + +#### Tracked inputs + +mcpp reads the `.rc` for quoted `#include`s and for the files named by resource +statements (`ICON`, `RCDATA`, `MANIFEST`, …), and makes them build inputs, so +editing the icon relinks. Angled includes (``) are the toolchain's +and are covered by the toolchain fingerprint instead. + +A file name reached through a macro (`1 ICON APP_ICON`) is invisible to that +scan. mcpp names what it could not resolve and requires an explicit declaration: + +```toml +extra-inputs = ["assets/app.ico"] +``` + +#### Anything else: `role = "object"` + +For inputs that are not resource scripts — a blob embedded with `objcopy`, a +generated `.def`, a pre-built object — a build program can declare a build-graph +node whose outputs join the link: + +```cpp +mcpp::action o; +o.id = "blob"; o.role = "object"; +o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o") + .input("blob.bin") + .output("${mcpp.out_dir}/blob.o") + .target("myapp") // omit: every image, test binaries included + .submit(); +``` + +See [30 — build.mcpp](30-build-mcpp.md). Naming such a file in +`[build].ldflags` also "works", but ldflags is a flat string in the link +command: nothing tracks it, and editing the file produces `ninja: no work to +do`. +### 2.16 `[hooks]` — Project Build Lifecycle Commands + +Moved to [09 — Commands by Scenario](09-commands-by-scenario.md). + + +## 3. Worked Examples + +Four of these are runnable projects rather than snippets, and the project is +the better answer: it builds, and it is checked by CI. + +| shape | run | +|---|---| +| a hello world | [`examples/01-hello`](../examples/01-hello/) | +| a module library with tests | [`examples/11-features`](../examples/11-features/) | +| an application with dependencies | [`examples/02-with-deps`](../examples/02-with-deps/) | +| a cross-compiled static release | [`examples/03-pack-static`](../examples/03-pack-static/) | + +Two shapes have no example yet and stay here as manifests. + +### 3.4 Pure C Library + +```toml +[package] +name = "myc" +version = "0.1.0" + +[build] +c_standard = "c99" +include_dirs = ["include"] +sources = ["src/**/*.c"] + +[targets.myc] +kind = "lib" +``` + +### 3.5 Mixed C / C++23 Module Project + +```toml +[package] +name = "hybrid" +version = "0.1.0" + +[build] +include_dirs = ["include"] +c_standard = "c11" + +[dependencies] +lua = "5.4.7" # Pure C library; mcpp compiles .c files with the C compiler automatically + +[targets.hybrid] +kind = "bin" +``` + + +## 4. Conventions and Defaults Cheat Sheet + +| Item | Default | Notes | +|---|---|---| +| Source files | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | Scanned recursively and automatically | +| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred | +| Library root | `src/.cppm` | Override with `[lib].path` | +| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly` (experimental playground) | +| C standard | `c11` | `.c` files go through the C compiler automatically | +| Static stdlib | `true` | Portable binary | +| Headers | `include/` (if present) | Added to `-I` automatically | +| Tests | `tests/**/*.cpp` | Discovered automatically by `mcpp test` | +| Dependency namespace | `mcpplibs` (default) | A bare selector means only this exact namespace | + +### 4.1 Legacy `[language]` Compatibility Layer + +The old configuration is still readable: + +```toml +[language] +standard = "c++26" +``` + +New projects should use `[package].standard`. If both locations are present, `[package].standard` is authoritative. diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md new file mode 100644 index 00000000..cc40a69d --- /dev/null +++ b/docs/05-dependencies.md @@ -0,0 +1,306 @@ +# 05 — Dependencies and Resolution + +**Reader:** an author whose build now contains more than their own code. + +**The question this chapter answers:** where does a dependency come from, what +does a version constraint mean, and what happens when two of them disagree. + +**Not here:** what makes two packages the same package — that is +[SPEC-001](specs/package-identity.md), which this chapter applies rather than +restates — and how to publish one, which is +[11 — Publishing a Library](11-publishing-a-library.md). + +Before: [04 — The mcpp.toml Manifest](04-mcpp-toml.md) is where these tables +live among the others. After: [06 — Features and Capabilities](06-features-and-capabilities.md) +is how a dependency becomes optional. + +## `[dependencies]` — Runtime Dependencies + +**The recommended form is a dotted selector with an exact version.** The dotted +form names one identity — everything before the final dot is the namespace, the +final segment is the package name — so what resolves does not depend on which +namespaces happen to be configured. + +```toml +[dependencies] +compat.gtest = "1.15.2" +imgui.core = "0.0.1" +imgui.backend.glfw_opengl3 = "0.0.1" +mcpplibs.capi.lua = "0.0.3" +``` + +A bare name is accepted and resolves through the default namespace +(`mcpplibs`), which is convenient in a project that uses only that namespace and +ambiguous in one that does not: + +```toml +[dependencies] +cmdline = "0.0.2" # resolves to mcpplibs.cmdline +``` + +
+Equivalent spelling: the namespace sub-table + +Groups entries under one namespace. It resolves to exactly the same identities +as the dotted form and is worth using when many dependencies share a namespace. + +```toml +[dependencies.mcpplibs] +cmdline = "0.0.2" +tinyhttps = "0.2.2" +llmapi = "0.2.5" + +[dependencies.compat] +glfw = "3.4" # Explicit namespace; no fallback search +``` + +
+ +```toml +# Path dependency (local development) +[dependencies] +mylib = { path = "../mylib" } +``` + +```toml +# Git dependency — pick exactly one of tag / branch / rev +[dependencies] +mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } +applib = { git = "https://github.com/user/applib.git", branch = "develop" } +``` + +```toml +# Long-form dep spec: features and backend knobs +[dependencies] +imgui = { version = "0.0.3", features = ["docking"] } # Request a feature of this dependency +widget = { version = "1.0", backend = "glfw_opengl3" } # Sugar for: features=["backend-glfw_opengl3"] +``` + +`backend = ""` is **general-purpose convention sugar**: it desugars 1:1 into +requesting the dependency's `backend-` feature (a library that supports this +knob should declare a `backend-*` family in its own `[features]`). If the target +package declares `[features]` but does not include the requested feature (including +the result of backend desugaring), a warning is issued by default, and an error +under `mcpp build --strict`. + +**Git dependencies and `mcpp.lock`**: a `tag` or `rev` already names a fixed point +in history, but a `branch` moves. The first build resolves the branch to a commit +and records it in `mcpp.lock`, and every later build rebuilds **that** commit — the +lock is authoritative, not a cache hint, so deleting `~/.mcpp/git` or moving to +another machine cannot silently move the build to a newer tip. A newer tip must be requested +explicitly: + +```bash +mcpp update mylib # drop the recorded commit; the next build re-resolves it +mcpp update # same, for every dependency +``` + +Because the recorded commit is enough to decide what to build, a rebuild with the +clone already in `~/.mcpp/git` makes no network request at all and works under +`--offline`. Only two things need the network: resolving a branch that has no +commit in the lock, and cloning a commit that is not cached yet. A `git =` value +that names a local directory (or a `file://` URL) needs neither, so it is never +refused offline. + +**SemVer constraints**: + +```toml +[dependencies] +foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret, default) +bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde) +baz = "=1.2.3" # Exact match +qux = ">=1.0, <2.0" # Range combination +``` + +### Namespace resolution rules + +Every package has a two-part identity: a **namespace** and a **name**. Every +selector normalizes to exactly one identity: + +- `cmdline` → `(mcpplibs, cmdline)`; omitting the namespace means the + `mcpplibs` default, and nothing else. +- `compat.gtest` → `(compat, gtest)`. +- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`. + +There is no ordered fallback or fuzzy, index-wide search by short name: + +```toml +# Correct — dotted selector +[dependencies] +chriskohlhoff.asio = "1.38.1" + +# Correct — namespace sub-table (preferred for several packages from one org) +[dependencies.chriskohlhoff] +asio = "1.38.1" + +# Wrong — a bare name never reaches the `chriskohlhoff` namespace +[dependencies] +asio = "1.38.1" +``` + +The third form fails with an error that names the exact `(mcpplibs, asio)` +identity that was tried and, when the short name exists elsewhere, gives a +copyable explicit selector. + +#### Migration window for bare names (`2026.8.10.1` → removed in `2026.9`) + +Every published `compat.*` package and every manifest written before exact +identity spells its dependency bare — `gtest = "1.15.2"`. Failing those +outright on upgrade would break builds against data that is already published +and cannot be edited retroactively, so for one release a bare name that misses +`mcpplibs` still reaches `compat.`, and a descriptor that declares no +namespace at all still answers to its bare name. + +It is not quiet about it: + +``` +warning: dependency 'gtest' resolved to 'compat.gtest' through the deprecated +bare-name search; namespace omission means `mcpplibs` only. Write the exact +package: + [dependencies.compat] + gtest = "1.15.2" + (or run `mcpp add compat.gtest@1.15.2`). This fallback is removed in 2026.9. +``` + +What reaches `mcpp.lock`, the install layer and the cache is the canonical +identity, so the ambiguous spelling lives in exactly one place — the manifest — +until it is rewritten. `mcpp add gtest@1.15.2` performs that rewrite. + +The window does **not** apply to a selector that states a namespace +(`mcpplibs.gtest` misses and stays missed), and a bare name still never reaches +a third-party namespace. + +**Why one identity?** Dependency resolution has to be reproducible. Candidate +search would let two namespaces with the same short name be settled by index +state, and adding an index could silently retarget an existing dependency. + +**For xpkg authors:** in an index descriptor, identity is the pair +`(package.namespace, package.name)`. The namespace is the dotted path; **`name` is +a single atomic segment**: + +```lua +package = { + namespace = "chriskohlhoff", + name = "asio", -- one segment; NOT "chriskohlhoff.asio" +} + +package = { + namespace = "mcpplibs.capi", -- depth belongs here + name = "lua", +} +``` + +The filename is only a hint — a descriptor is found by its declared identity, so +`pkgs/c/chriskohlhoff.asio.lua` and `pkgs/z/anything.lua` resolve identically. +`.lua` or `..lua` are recommended (they hit mcpp's fast +path) but not required. + +The older fully-qualified spelling (`name = "chriskohlhoff.asio"`) is still +accepted, so already-published descriptors keep working. `mcpp xpkg parse` +enforces the descriptor rule; run it in index CI. Descriptor identity +requires mcpp >= 0.0.106; exact selectors require mcpp >= 2026.8.10.1; both use +xlings >= 0.4.69. Full normative text is in `docs/specs/package-identity.md`. + +`mcpp new --template` deliberately reuses this identity model instead of +creating another package grammar: `[ns.]name[@version][:tname]`. A bare name +there also means only `mcpplibs`; version and template may be omitted +independently. Omitted `tname` selects the sole explicit default, or the only +template when no `default = true` is present. Multiple unmarked templates are +an error, never a directory-order choice. See the normative template rows in +`docs/specs/package-identity.md` §4.4. + +### When mcpp refreshes the package index + +`mcpp build` / `run` / `test` refresh the package index **only when a dependency +cannot be resolved from the local copy** — never merely because time has passed. +Concretely, a refresh happens when there is no local index at all, when a +dependency's descriptor is missing from it, or when a SemVer constraint matches +none of the versions it knows. A build whose dependencies all resolve locally +makes no network request, however old the local index is. + +The consequence worth knowing: a constraint like `^1.2` resolves against the +**the versions the local index knows**. A `1.3.0` published upstream after the last refresh +is invisible until it is fetched: + +```bash +mcpp index update # sync the index +mcpp update # sync, then re-resolve dependencies +mcpp index status # local state: state, age and revision +``` + +Controls, in order of precedence: + +| Control | Effect | +|---|---| +| `--offline` (any command) | Never touch the network — no index refresh, no downloads, no toolchain auto-install, no `git ls-remote`/`clone`. Anything already installed still builds, including git deps whose commit is in `mcpp.lock` and whose clone is cached | +| `MCPP_OFFLINE=1` | Same, for a whole shell session or CI job | +| `[index] auto_refresh = false` in `~/.mcpp/config.toml` | Never refresh the index automatically; downloads still work | + +`MCPP_NO_AUTO_INSTALL=1` remains accepted as the older, narrower spelling of +`--offline` (it gates only toolchain auto-install). + +Run any command with `-v` to see the decision for each dependency and why. + +## `[dev-dependencies]` — Test Dependencies + +```toml +[dev-dependencies.compat] +gtest = "1.15.2" +``` + +`mcpp build` ignores these; `mcpp test` resolves and uses them. `mcpp test` automatically discovers `tests/**/*.cpp` and compiles them into test binaries. The runner is framework-agnostic: each file is an independent binary judged by exit code — a bare `main`, gtest (via `[dev-dependencies]` + `gtest_main`), or any other framework all work identically, and `-- args` are forwarded to every test binary (e.g. `-- --gtest_filter=...`). Note: synthesized test target names may contain `/` (`tests/00-a/0.cpp` → `00-a/0`), unlike `[targets.*]` names — the two namespaces are intentionally separate (test targets never enter the manifest or publishing). Tests are named by their `tests/`-relative path (`tests/00-a/0.cpp` → `00-a/0`), each test compiles in isolation (a broken test fails alone; package/dep breakage is reported as a build error instead), and `mcpp test ` / `--message-format json` filter and machine-format the run. + +## `[build-dependencies]` — Build-Time Dependencies (mcpp 2026.8.29.1+) + +```toml +[build-dependencies] +protobuf = { version = "35.1", tools = ["protoc"] } +``` + +The section and the per-edge request answer **different questions**, and +conflating them is a modelling error rather than a matter of spelling. + +- The **section** says whether the package itself reaches the target. + `[dependencies]` means it does; `[build-dependencies]` means it never does, + and neither does anything reachable only through it. +- The **request on the edge** says which build-time product is wanted. + `tools = [...]` asks for a host executable; `host-module = true` asks for a + module the build program can import. + +A package takes a value on both axes at once, and protobuf is the case that +proves the axes must stay separate: a project links `libprotobuf` *and* needs +`protoc` during the build. It is written once, in `[dependencies]`: + +```toml +[dependencies] +protobuf = { version = "35.1", tools = ["protoc"] } +``` + +`[build-dependencies]` is for the combination the first axis cannot otherwise +express — a package whose library must not reach the target while its tool or +its rule is still wanted. Naming one package in both tables is not an error: +the ordinary declaration wins, because a `[build-dependencies]` line must not +quietly drop a library the target needs. + +Unlike `[dev-dependencies]`, these **are** walked transitively: a build +dependency's own dependencies are what make it work, and they inherit its +build-only nature. + +A feature scopes build-time requests without a second declaration site — +`[feature-deps.]` may add `tools` to a dependency already declared +unconditionally, so "only when needed" needs no separate table. + +> The section has been parsed since early versions and, until 2026.8.29.1, read +> by nothing that made a decision: writing it produced a manifest that loaded, +> no diagnostic, and no effect. + +## Current limitations + +- **Two things need the network, and only two:** resolving a branch that has no + commit in the lock, and cloning a commit that is not cached yet. A `git =` + value naming a local directory or a `file://` URL needs neither and is never + refused offline. +- The index-refresh window **does not apply to a selector that states a + namespace**. `mcpplibs.gtest` misses and stays missed until the next refresh. +- `mcpp.lock` records and verifies a resolution; it does not constrain one. See + [51 — Supported Versions and Compatibility](51-supported-versions.md). diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md deleted file mode 100644 index f789ff7a..00000000 --- a/docs/05-mcpp-toml.md +++ /dev/null @@ -1,3129 +0,0 @@ -# The mcpp.toml Manifest Guide - -`mcpp.toml` is the project configuration file for the mcpp build tool, analogous to Cargo's `Cargo.toml` or Node's `package.json`. Place it in the project root; `mcpp build` discovers and reads it automatically. - -## 1. Minimal Examples - -mcpp is designed around **convention over configuration** — most fields have sensible defaults, so the simplest `mcpp.toml` is just a few lines: - -### 1.1 Executable (minimal) - -```toml -[package] -name = "hello" -version = "0.1.0" -``` - -mcpp infers automatically: -- Source files: `src/**/*.{cppm,cpp,cc,c,S,s,asm}` -- Entry point: `src/main.cpp` → produces the `hello` binary -- Standard: C++23 -- Modules: scans `export module ...` declarations and builds the dependency graph automatically - -### 1.2 Library project (minimal) - -```toml -[package] -name = "mylib" -version = "0.1.0" - -[targets.mylib] -kind = "lib" -``` - -lib-root convention: the primary module interface defaults to `src/mylib.cppm` (the last segment of the package name). - -## 2. Full Field Reference - -### 2.1 `[package]` — Package Metadata - -```toml -[package] -name = "myapp" # Package name (required) -version = "0.1.0" # Semantic version (required) -standard = "c++23" # C++ standard (default c++23; can be set to c++20 / c++26) -description = "My awesome app" # Description (optional) -license = "MIT" # License (optional) -authors = ["Alice", "Bob"] # Author list (optional) -repo = "https://github.com/user/myapp" # Repository URL (optional) -``` - -`standard` is the first-class setting for the C++ language standard. Recommended values: - -- `c++23`: the default, suited to the current module-based default templates. -- `c++20`: the lowest level mcpp accepts — named modules are a C++20 feature, so nothing below it exists for this build model. Use it when an external constraint (an older in-house rule, a third-party API that stops at C++20) forces the level down. `import std;` still works there: it is a C++23 *library* feature, but GCC (≥ 15), Clang + libc++ (≥ 17) and the MSVC STL (from VS 2022 17.8) all provide the `std` module in C++20 mode as well. Note that C++23 library facilities (`std::print`, `std::expected`, …) are not available, including in the code generated by `mcpp new`. -- `c++26`: for C++26 language features. -- `c++2a` / `c++2c`: compatibility aliases, normalized to `c++20` / `c++26` after parsing. -- `gnu++20` / `gnu++23` / `gnu++26`: GNU dialects; the choice enters the fingerprint and the std BMI cache key. -- `c++latest`: resolves to the newest standard level the resolved toolchain supports. Good for local experimentation, but not recommended for release packages that require reproducibility. -- `c++fly`: `c++latest` **plus every experimental standard feature the resolved toolchain can enable** (language + standard library). On GCC ≥ 16 this turns on C++26 reflection (`-freflection`) and contracts; on Clang/libc++ it adds `-fexperimental-library`; unsupported gates are skipped with a printed summary. Deliberately toolchain-dependent — the bleeding-edge playground mode, never for published packages. - -Two properties worth knowing: - -- **The standard is module-graph-global.** The root package's `standard` applies to every - translation unit in the build, dependencies included — a dependency's own `standard` is not - used when it is being built as a dependency. This is not a simplification: BMIs are not - compatible across levels (GCC rejects them with `language dialect differs`), so a single - graph physically cannot hold two levels. -- **Levels never share caches.** The standard is part of the fingerprint, the `import std` BMI - identity and the dependency build-cache key, so switching between `c++20` and `c++23` gives - each level its own target directory and its own std BMI instead of a corrupt hit. - -If the sources `import std;` at a level the resolved toolchain does not provide the `std` -module for, mcpp fails before compiling and names both the toolchain and the project level. - -Both spellings of the value are accepted: `standard = "c++26"` and `standard = 26`. - -When a **dependency declares a level above the graph's**, mcpp says so before compiling -rather than letting it fail somewhere inside that dependency's sources. See -[workspace §4.2](06-workspace.md). - -#### Dialect flags and the `import std` BMI - -Some flags change what the standard library's headers declare, so the precompiled `import std` -BMI has to be built with them too. That is what `[build] dialect_cxxflags` is for: it is -applied to the std BMI prebuild, the module scan **and** every translation unit in the graph, -including dependencies. - -```toml -[build] -dialect_cxxflags = ["-fno-exceptions"] -``` - -mcpp promotes a few flags into that channel automatically when it finds them in `cxxflags` -(`-freflection`, `-fchar8_t`, `-D_GLIBCXX_USE_CXX11_ABI=…`) — a graph that mixes those is -ill-formed anyway, so no dependency can hold a different opinion about them. - -`-fno-exceptions` and `-fno-rtti` are **not** promoted, because a dependency can legitimately -disagree: they remove a language facility the dependency may use, and the consumer cannot make -that choice on its behalf. Left in `cxxflags` they reach every TU and not the prebuild, so the -build cannot succeed — mcpp refuses it before compiling and names the key: - -``` -error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is - precompiled without it, so every importing translation unit will fail with - "language dialect differs". - Declare it as a dialect flag instead: - - [build] - dialect_cxxflags = ["-fno-exceptions"] -``` - -The check reads the **effective** flags, so it fires for the same flag written in -`[profile.] cxxflags` or in a `[target.…]` block. It does not fire when nothing in the -graph imports `std`, where the flag is an ordinary per-unit option that works. - -### 2.2 `[targets.]` — Build Targets - -```toml -# Executable (default; inferred automatically when src/main.cpp exists) -[targets.myapp] -kind = "bin" -main = "src/main.cpp" # Optional, defaults to src/main.cpp - -# Static library -[targets.mylib] -kind = "lib" - -# Shared library -[targets.mylib] -kind = "shared" -soname = "libmylib.so.1" # Optional: Linux/ELF ABI name; an alias of the same name is generated at runtime -``` - -`soname` is the ABI name for a shared library, analogous to `SOVERSION`/`SONAME` in -Autotools/CMake. On Linux, mcpp passes `-Wl,-soname,` to the linker and -generates a ` -> lib.so` alias in the output directory, so that -downstream programs can load the library via its standard ABI name through -`DT_NEEDED` or `dlopen()`. This field only applies to `kind = "shared"`, and the -value must be a filename basename. - -Shared-library targets work on all three binary formats. ELF gets a `.so` with -its `soname` and a `$ORIGIN` search path; Mach-O gets a `.dylib` whose install -name is `@rpath/`, so it survives being moved; PE gets both the `.dll` -the loader opens and the import library the linker consumes, with the export -list generated from the objects on the MSVC ABI (which exports nothing without -`__declspec(dllexport)` or a `.def`). See `tests/e2e/08`, `257` and `259`. - -#### `exports` — which symbols the artifact publishes (mcpp 2026.9.6.5+) - -```toml -[targets.mydriver] -kind = "shared" -soname = "libmydriver.so.1" -exports = "abi/mydriver.exports" # or inline: exports = ["vk_icd*"] -``` - -**Omitting the key publishes everything, which is what both platforms already -do** — ELF gives symbols default visibility, and PE gets an auto-generated -`.def` listing every symbol. `exports` narrows that. - -Two projects need the narrowing. A **runtime with a stable ABI** publishes a -reviewed set and nothing else, so that what is not in the set stays free to -change. A **plugin loaded beside its rivals** must not collide: a Vulkan ICD is -found by name for `vk_icdGetInstanceProcAddr`, and one that also exports its -internals collides with the loader and with the other ICDs in the process. - -The file lists one symbol pattern per line, `#` starts a comment, and `*` is the -only wildcard. The inline array says the same thing and is for the two or three -entry points where a separate file would be ceremony. - -One statement, three renderings: - -| Platform | Rendered as | -|---|---| -| ELF | a version script, `-Wl,--version-script=` | -| Mach-O | `-Wl,-exported_symbols_list` (the leading underscore is supplied by the engine) | -| PE | the `.def`, replacing the auto-generated all-exports one | - -**It does not change compile-time visibility, and that is deliberate.** The -narrowing is a link-time property on all three formats, so one key has one -effect. `-fvisibility=hidden` remains available through `[build] cxxflags` for -the code-generation benefit it brings, and it is a separate decision because it -also changes how this library's own translation units see each other. - -**Symbol versioning is not this key.** `foo@@LIB_1.0` alongside `foo@LIB_0.9` -is an ELF-only capability that cannot be stated neutrally; a package that needs -it writes the version script itself and passes it through `[build] ldflags`, or -computes it and emits `mcpp:link-flag=` (docs/07). - -A `soname` is meaningful on `kind = "lib"` too — see -[`dependency_linkage`](#dependency_linkage--static-or-shared-is-the-consumers-decision) -below, where the form a library takes becomes the consumer's decision. - -#### Per-target keys - -```toml -[targets.server] -kind = "bin" -main = "src/server.cpp" -defines = ["BUILD_SERVER=1", "PORT=8080"] # -D macros, applied to this target's entry only -cxxflags = ["-Wno-deprecated-declarations"] # extra C++ flags for this target's entry (no -std=...) -cflags = ["-DPURE_C"] # extra C flags for this target's entry - -[targets.gui] -kind = "bin" -main = "src/gui.cpp" -required_features = ["gui"] # only built when feature `gui` is active -``` - -| Key | Meaning | -|---|---| -| `defines` | Preprocessor macros (`name` or `name=value`); desugar to `-D` on both the C and C++ entry compile. | -| `cxxflags` / `cflags` | Extra compile flags for this target. Do **not** put `-std=...` here — use `[package].standard`. | -| `required_features` | The target is emitted only when **every** listed feature is active in the build; otherwise it is silently skipped. A gate only — it does not activate features (use `--features` / `[features].default`). **One exception, and it is not a second rule:** when this target is requested as a host tool (`tools = [...]`, §2.14), the target is what was *asked for*, so its `required_features` become the sub-build's *inputs*. Same field, one meaning — the resolution just runs in the opposite direction. | - -> **Scope (important):** `defines` / `cxxflags` / `cflags` on a target apply **only to that -> target's exclusive entry source** (its `main`) — never to shared module/impl objects, which -> are compiled once and linked into every target (mcpp's compile-once model). They are the right -> tool when the flag only needs to affect a single binary's (or test's) own entry — for example a -> per-test contract evaluation semantic (`-fcontract-evaluation-semantic=observe`) for a test whose -> `main` exercises the violation, a feature macro the entry alone reads, or a local warning -> suppression. If a flag must reach **shared** code, it does not belong here — split into a -> [workspace](06-workspace.md) member or use `[features]`, or for a whole-build mode use a -> `[profile.*]` (`mcpp test --profile ` builds the whole test image, code-under-test -> included, under that profile). -> -> Unsupported keys under `[targets.]` are reported as a warning (an error under `--strict`). - -**Choosing where build configuration goes** — when more than one binary must differ: - -| You want | Use | -|---|---| -| Different macros/flags on a binary's **own entry** | per-target `defines` / `cxxflags` (above) | -| Two products that differ in code they **share** | split into [workspace](06-workspace.md) members, each with its own `[build]` flags over a shared `lib` | -| To **select a variant** of a shared library (e.g. a backend) | `[features]` on that library (§2.8) — additive, reaches the library's own compile | -| A **whole-build mode** (sanitizers, contract semantics, opt level) | `[profile.]` (§2.9) + `--profile`; also honored by `mcpp test --profile ` | - -mcpp deliberately does not compile a shared source two different ways within one build: a source -maps to one object (and one BMI for modules), so divergence that must reach shared code belongs at -the package/feature boundary, not on an individual target. - -### 2.3 `[build]` — Build Configuration - -> **Every entry `sources` matches must produce an object that gets linked.** A -> file mcpp cannot place — an extension outside the built-ins and outside -> `module_extensions` — is refused, naming the file, the extension and the key. -> It is not ignored, because the failure that produced this rule was not "one -> file too many" but *compiled and then linked by nobody*: the scanner reads -> `export module` and gives the edge a BMI while the classifier says the file has -> no role, and what the author sees is `undefined reference` to a module-mangled -> symbol. Headers belong in `include_dirs`; Windows resource scripts in -> `[resources]`. - -> **`sources = []` is not the same as omitting `sources`.** An absent key -> selects the default glob; an explicitly empty list means *compile nothing*, -> which is what a header-only distribution package needs to say. Until -> mcpp 2026.8.18.1 the two were byte-identical, so there was no spelling for -> "nothing" and any file left under `src/` was swept in. - -> **A `sources` entry may carry the accelerator it is for** (2026.9.5.2+): -> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`. The glob -> joins the list like any other; the constraint decides whether it applies to a -> given build. It must match at least one file (an empty match is refused: it -> would leave nothing to compile for that device and say so only at the link). -> Under `--no-accel` the glob is left out, which is how one project yields its -> CPU-only variant. Under an `--accel` that does not cover the constraint the -> build is refused naming both (`accel-mismatch`). Device-kind files the -> effective set matches — CUDA and HIP, the GLSL stages, HLSL, OpenCL C and -> Metal, listed in full in [20 — Heterogeneous Builds](20-heterogeneous-builds.md) — are never -> compiled by the engine; they reach the build program as -> `MCPP_DEVICE_SOURCES`, where the rule package the project imports turns each -> into an `mcpp::action`. - -```toml -[build] -sources = ["src/**/*.cppm", "src/**/*.cpp"] # Source globs (default: src/**/*.{cppm,cpp,cc,c,S,s,asm}) -module_extensions = [".ixx"] # Extra extensions used by module INTERFACES (§ below) -build_program_timeout = 1800 # Seconds a build.mcpp may run; 0 = no limit (§ below) -include_dirs = ["include", "third_party/include"] # Header search paths -include_dirs_after = ["*"] # Header dirs searched AFTER system dirs (-idirafter) -private_include_dirs = ["vendor/src/include"] # Of `include_dirs`, the ones a consumer must NOT get -c_standard = "c11" # Standard for C source files (default c11) -cflags = ["-DFOO=1"] # Extra C compile flags -cxxflags = ["-DBAR=2"] # Extra C++ compile flags (do not put -std=... here) -ldflags = ["-lfoo"] # Extra link flags -defines = ["BIZ=1", "QUX"] # Preprocessor macros for every TU (desugars to -D; reaches module scans) -cxx_runtime = "self-contained" # C++ runtime contract (§ below); static_stdlib is the old spelling -target = "x86_64-linux-musl" # Default build target when no --target is passed - # (≙ cargo build.target; e.g. "ship fully-static") -macos_deployment_target = "14.0" # Minimum supported OS version for macOS artifacts (macOS only) -dependency_linkage = "static" # How dependencies arrive: static (default) | shared (§ below) -cache = "global" # Global dependency cache: global (default) | local | off (§2.10) -jobs = "auto" # Concurrent compiles: a positive number, or "auto" (§ below) -bmi_schedule = "auto" # Module-edge scheduling: auto (= off) | on | off (§ below) -``` - -#### `dependency_linkage` — static or shared is the consumer's decision - -```toml -[build] -dependency_linkage = "shared" # whole-graph default; "static" is the default default - -[profile.dev] -dependency_linkage = "shared" # per profile - -[dependencies] -"compat.zlib" = { version = "1.3.2", linkage = "shared" } # one package -``` - -Until mcpp 2026.8.28.2 a dependency had exactly one shape and the *package -author* chose it: `kind = "lib"` merged its objects into every consumer's link, -`kind = "shared"` produced a real shared library. That is the wrong owner for -the decision. Whether a library should be a separate file at run time is a -property of the **program being built** — how it is shipped, how often it is -relinked, whether something else in the process already provides that library. - -- **`static`** (default) — the dependency's objects are merged into the images - that use it. Byte-for-byte what mcpp has always done; a project that does not - write this key builds exactly as before. -- **`shared`** — mcpp builds the dependency as a shared library beside the - artifact and links against it, with `$ORIGIN` (ELF) / `@loader_path` (Mach-O) - / the executable's own directory (PE) finding it again after the build - directory moves. - -**This is not `[target.].linkage`** (§2.7.1). That key answers the -same-sounding question about the **C library** (a musl `-static` link, MSVC's -`/MT`). The two are not independent, and the direction matters: a fully static -image has no interpreter, so it cannot load a shared object at all. On a target -whose C library is linked statically — which is the **default for musl** — -`dependency_linkage = "shared"` is refused, and says so. - -**A package can say it must be one form**, and only for a real reason: - -| The package writes | mcpp reads it as | -|---|---| -| `[targets.] kind = "shared"` | *must* be shared — something else in the process will `dlopen` it, so there may only be one copy (X11, a Vulkan loader) | -| `ldflags` containing `-L` | *must* be static — the package ships prebuilt archives mcpp did not compile and cannot place inside a shared object it builds | -| a packaged library (`mcpp pack`) | whichever legs it actually ships, from `[[runtime.artifacts]] role` | -| anything else | either form | - -`kind = "lib"` is **not** a constraint: it is the default value, and most -packages write it without choosing anything. Absence of a statement is not a -statement. - -A per-dependency `linkage` is honoured **only in the root project's** -`[dependencies]`. A package deep in the graph does not get to decide how the -final program is laid out; one that genuinely must be a single shared copy says -so on its own target instead. - -#### `soname` on a library target - -A `soname` (§2.2) may be declared on `kind = "lib"` as well as -`kind = "shared"`. It is the name a library is *found* by, and it is the only -way mcpp's build of a package and a third party's copy of the same library can -resolve to **one file** instead of two — which a package cannot state if -declaring it forces the package to stop being consumable as a static library. - -A descriptor that writes `soname` on a non-shared target cannot be read by -mcpp releases before 2026.8.28.2 — the whole manifest fails to load, not just -the key. Publishing one to an index therefore waits for that floor to move. - -#### The symbol-provision check - -After a link, mcpp asks whether every symbol in the image has exactly **one** -provider. On ELF an executable is searched first, so a library statically -merged into the program wins for every symbol it shares with a shared library -loaded beside it — the shared copy is never called, and code inside that -library runs against a build it was not linked against. No linker or loader -diagnostic exists for this. - -The check is a measurement, not a declaration: it reads the produced image's -dynamic symbol table, removes the entries that are copy relocations, and -reports only those a library in the artifact's own closure **also** defines. -An arrangement with one copy in the process is silent. The verdict is recorded -in `target///resolution.json` under `runtime.symbol_provision`, -with the count and its denominator, so CI can read it without `readelf`. - -It is a warning by default and an error under `--strict`. The ways out are -ordered, and the order matters: - -1. **Stop one side from providing it** — usually a package shipping a copy of a - library the graph already builds. Always correct. -2. **Make both resolve to one file** by declaring the library's real `soname` - on its target. -3. **`dependency_linkage`** changes which form mcpp builds. It removes *this* - finding, but on its own it can leave **two** copies loaded instead of one: - measured on a graph staging glib (whose `libgio` needs `libz.so.1`) beside a - statically built `compat.zlib`, switching the form dropped the executable's - 88 exported symbols and then loaded both `libzlib.so` and `libz.so.1`. It - unifies the two providers only when (2) holds as well. - -`private_include_dirs` names the entries **of `include_dirs`** that stop at this -package's own boundary: this package compiles with them, and a consumer never -receives them. - -Almost every package publishes exactly the set it is built from, which is why -`include_dirs` alone was enough for a long time. The shape where the two differ -is a package that vendors a library with an *internal header overlay*. musl -reaches its own declarations through `src/include`, whose headers define -`hidden`, `weak` and `weak_alias` — names that mean something only to musl's own -sources. Publishing that directory hands those macros to every consumer, and a -consumer that uses `hidden` as an ordinary identifier stops compiling for a -reason it has no way to see. - -```toml -[build] -# The relative ORDER of the two kinds is load-bearing: the internal overlay -# must precede the public headers for this package's own build. That is why -# this is a SUBSET of `include_dirs` rather than a second list — two arrays -# cannot express one order. -include_dirs = ["port/include", "musl/src/include", "musl/include"] -private_include_dirs = ["musl/src/include"] -``` - -Entries take the same `*` glob convention as `include_dirs`, and are matched -after expansion — so a glob may name exactly the directories it expands to. An -entry that is not among this package's `include_dirs` withholds nothing and is -reported as such rather than passing in silence. - -**On an older engine the key is ignored, never fatal.** Measured on 2026.8.26.2: -in a dependency's manifest it is accepted silently, and in a root manifest it -warns — `[build] has unsupported key 'private_include_dirs' (ignored)` — and the -build continues. So a package may adopt the key without waiting for its -consumers to upgrade; those on an older engine simply keep receiving the -directory as they did before. The one place this does **not** hold is a -published `xim` descriptor's `target_cfg` block, where an unrecognised sub-key -is a hard error that fails the whole manifest — do not put this key there until -the index floor names an engine that knows it. - -`include_dirs_after` (#249) lists header directories that are searched **after** -the toolchain's system directories (emitted as `-idirafter` on GCC/Clang, as -trailing `/I` under the MSVC dialect, and as plain `-I` for NASM -assembly units — neither has an equivalent, and neither has a system-header -chain to protect). Use it instead of -`include_dirs` when the directory is an extracted source-tarball root that -contains files whose names collide with standard headers — e.g. ffmpeg's -top-level `VERSION` file shadows libc++'s `` on case-insensitive macOS -filesystems when the root is put on `-I`. With `include_dirs_after` the system -header always wins while the package's real headers (``) -remain findable. Entries support the same `*` glob convention as -`include_dirs`, and they propagate to dependent packages along the same edges — -consumers receive them as after-dirs, never upgraded to `-I`. - -`macos_deployment_target` sets the minimum system version in the artifact's -Mach-O header (`LC_BUILD_VERSION minos`), i.e. the oldest macOS the binary can -run on. The precedence follows ecosystem convention: the `MACOSX_DEPLOYMENT_TARGET` -environment variable (an explicit per-invocation override, honored the same way by -cargo/rustc, cc, etc.) > this field (the project default, similar to SwiftPM's -`platforms:`) > the **built-in default `14.0`** (rustc-style — every target has a -baseline, and 14.0 is the floor of LLVM's official static libraries themselves). -This value enters the BMI fingerprint, so switching targets automatically rebuilds -the module cache. - -### Build concurrency (`jobs`) and module scheduling (`bmi_schedule`) - -```toml -[build] -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 -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 -single module interface compile peaks at 0.5–1.0 GB. Precedence is -`--jobs` / `MCPP_JOBS` > this key > the backend's own default. A malformed value -is **reported, never silently treated as the default** — a typo that quietly -restores the default is a build slower than requested, with no indication why. - -`bmi_schedule` decides when importers are unblocked. - -| value | | -|---|---| -| `"auto"` | **the default, and it currently means OFF** | -| `"on"` | split the module edge: importers start when the BMI is published, not when the compiler exits | -| `"off"` | one edge per module | - -Only those three spellings are accepted. `"ON"`, `"true"` and `"yes"` are -**rejected with a diagnostic** rather than quietly meaning off — and they are not -harmless typos: the value enters the build fingerprint, so a rejected spelling -used to select a different build directory (a full rebuild) while changing -nothing about the schedule. - -**Why `auto` is off.** 86% of a module interface compile is code generation that -no importer reads, so publishing the BMI early is worth a lot — measured on mcpp -itself, `cold` 86.7s → 35.7s and `edit-body` 80.9s → 29.8s. But a scheduling -change that is wrong is wrong *silently*: a missed dependency does not fail the -build, it just stops rebuilding something. It stays opt-in until it has been -through CI on every platform. - -**What it does not help.** Where mcpp already skips the cascade — `touch-hub`, -`edit-comment` — there is no owed work to move off the critical path, and the -key buys nothing. See the [benchmark](../README.md#benchmark). - -**The mechanism** differs per compiler and is selected automatically: gcc publishes its -BMI with `rename()`, so code generation is detached and the edge returns at -publication; clang gets two ordinary edges instead, because it writes the BMI to -its final path with `O_TRUNC` and a reader could observe a half-written file. -MSVC is left alone — neither `/ifcOnly`'s cost nor `.ifc` atomicity has been -measured, and guessing either wrong is silent. - -### Module interface extensions (`module_extensions`) - -mcpp treats `.cppm` as a module interface unit. The C++ ecosystem has not -converged on one spelling — Clang also recognizes `.ccm` and `.cxxm`, MSVC uses -`.ixx` — so a project whose interfaces use another extension declares it: - -```toml -[build] -module_extensions = [".ixx", ".ccm"] -``` - -The list is **additive**: `.cppm` is always a module interface and cannot be -removed. To stop a particular file from being built, `!`-exclude it in -`sources`; that is what `sources` is for. - -Declaring an extension does three things at once, which is the point of having -one key rather than several: - -1. the convention default for `sources` grows to match, so the files are - **found** (`src/**/*.ixx` joins the default glob); -2. those units compile with the **module** rule — they emit a BMI and their - objects are linked unconditionally; -3. the freshness fast path watches them, so adding an `import` to one - invalidates the build graph instead of silently reusing a stale one. - -Any extension is accepted **except** ones that already name a non-module role -(`.cpp` `.cc` `.cxx` `.c` `.m` `.mm` `.h` `.hpp` `.hh` `.hxx` `.S` `.s` -`.asm`); claiming one of those is a manifest error rather than a warning, -because it would route (say) C files to the C++ module rule and fail somewhere -that names neither the file nor this key. - -Extensions are matched **literally, without case folding** — `.S` and `.s` are -different languages in this domain, so case is never ignored. - -mcpp always tells the compiler explicitly that a module interface unit is one -(`-x c++-module` on Clang, `-x c++` on GCC, `/interface /TP` on MSVC), so an -extension the compiler driver has never heard of works anyway. This is why any -extension is allowed: mcpp does not need the compiler to recognize it. - -> **Publishing note.** An older mcpp does not know this key: it warns, ignores -> it, and then compiles those files as ordinary translation units — a wrong -> build rather than a clean failure. A published package that uses -> `module_extensions`, declare an mcpp version floor in its index descriptor. - -### Build-program timeout (`build_program_timeout`) - -A `build.mcpp` gets **600 seconds** by default, after which mcpp kills it and -fails the build naming the package. A project whose build program legitimately -runs longer (a large code-generation step) raises its own bound: - -```toml -[build] -build_program_timeout = 1800 # seconds; 0 = no limit -``` - -The value is read from **the manifest of the package that owns the -`build.mcpp`** — a dependency's generator is bounded by the dependency's own -declaration, because its author is the one who knows how long it takes. The -precedence follows the same shape as `macos_deployment_target`: - -``` -MCPP_BUILD_PROGRAM_TIMEOUT= (this invocation; highest) - > [build] build_program_timeout (that package's manifest) - > 600 (built-in default) -``` - -Leaving the key out is not the same as setting `0`: unset means "use the -default bound", `0` means "no bound at all". - -This value is deliberately **not** part of the build fingerprint — it changes -no edge in the graph, and folding it in would mean that raising a timeout -rebuilt the whole project, which is the opposite of what someone raising a -timeout wants. - -The **compile** phase is not bounded, only the build *program*. See -[07-build-mcpp.md](07-build-mcpp.md) for why that asymmetry is deliberate. - -### The C++ runtime contract (`cxx_runtime`) - -`cxx_runtime` states what the produced artifact promises about the machine that -runs it. It is a **distribution** property, not a build one — it describes the -runtime dependency set, and the flags that deliver it differ per platform. - -> **A target with no C++ in it has no C++ runtime contract to honour.** mcpp -> links such a target with the C driver and leaves the C++ runtime flags off the -> line entirely, so a pure-C shared library does not acquire a dependency on -> `libstdc++`/`libc++` it has no use for. One C++ translation unit anywhere in -> the target puts the whole target back on the C++ driver. This is derived from -> the sources — there is no key for it. - -```toml -[build] -cxx_runtime = "self-contained" # applies to every target (the default) - -# or, per role: -[build.cxx_runtime] -default = "self-contained" # executables -tests = "host-coupled" # test binaries never leave this machine -shared = "self-contained" # shared libraries (see below — the - # default differs by target format) - -# or, per target triple — beside `linkage`, which is the same axis: -[target.x86_64-linux-gnu] -cxx_runtime = "host-coupled" # e.g. this build is for a distro package -``` - -| value | the artifact needs, at run time | typical use | -|---|---|---| -| `self-contained` (default) | no C++ runtime outside itself | shipping a binary | -| `toolchain-coupled` | the C++ runtime of the toolchain mcpp installed | local iteration | -| `host-coupled` | whatever the driver resolves by default (the system runtime) | distro packaging, `dlopen` plugins that must share a runtime with their host | - -**Self-contained by default (portable by default)**: on macOS this statically -links LLVM's bundled libc++/libc++abi — the system libc++ would otherwise pin the -runnable version to the build machine's OS (older systems lack newer symbols, e.g. -the support symbols behind `std::print`), and only static linking can truly deliver -the `macos_deployment_target` floor. On Linux/MinGW it is `-static-libstdc++` (GCC) -or the whole-link `-static` (MinGW); on a Linux clang/libc++ toolchain it links -libc++.a/libc++abi.a/libunwind.a explicitly. A lower macOS floor (11–13) requires a -self-built libc++ archive (already verified to work, a data-level switch, available -on request). - -**Shared libraries are the one role whose default depends on the target format**, -because the hazard does. A `.so`/`.dylib`/`.dll` is not a small executable — it is -loaded *into* a process that already has a C++ runtime. - -| target | default for `kind = "shared"` | why | -|---|---|---| -| ELF (Linux, …) | `toolchain-coupled` | ELF has one global symbol namespace and the first definition loaded wins. A `.so` that statically embedded libstdc++ **exports** it, and the executable linking that library binds *its* `std::` references there — its own `self-contained` contract silently becomes a no-op, and its C++ runtime is whichever build of that library happens to load. | -| Mach-O | `self-contained` | the mechanism there is already `-load_hidden`, i.e. hidden visibility, so dyld never unifies those symbols; and toolchain-coupled is not available on macOS at all (see the note below). | -| PE (Windows) | `self-contained` | PE has no global symbol namespace — imports resolve per-DLL by name, so a DLL's private runtime cannot be picked up by anything else. | - -Setting `shared = "self-contained"` on ELF is supported and does exactly what it -says: the library embeds the runtime. mcpp additionally passes -`-Wl,--exclude-libs` for the standard-library archives, so the embedded copy stays -out of the library's dynamic symbol table and cannot be picked up by anything that -links it. Template instantiations emitted by the consuming code (weak/COMDAT `std::string` -symbols and the like) are still exported — that is the intended C++ ABI behaviour -and is not the leak this guards against. - -A project-wide `cxx_runtime = "…"` (or `static_stdlib = false`) applies to shared -libraries too: a human said what the whole project promises. The format-specific -default applies only when nobody said anything. - -`static_stdlib` is the older spelling and still works: `true` means -`self-contained`, `false` means `host-coupled`. An explicit `cxx_runtime` wins. - -**A contract that cannot be honored is reported, never silently downgraded.** If a -toolchain ships no `libc++.a`, or a contract has no mechanism on that platform, -the build prints what it fell back to instead of quietly producing a different -artifact than the manifest asked for. - -#### On the MSVC runtime - -The CRT model is the mechanism here, and it is a **whole-project** switch: cl -bakes `_MSVC_MT`/`_MSVC_MD` into the one `std` module a project builds, so a -per-role contract that disagrees with the project's cannot be honoured and is -reported rather than ignored. - -| value | what it is on MSVC | -|---|---| -| `self-contained` | `/MT` — the static CRT. `linkage = "static"` selects the same thing from the libc axis. | -| `host-coupled` (default under `/MD`) | the target provides `vcruntime140.dll` / `msvcp140.dll` — i.e. Visual Studio or the redistributable is installed there. | -| `toolchain-coupled` | the toolset's **own** copy of those DLLs travels with the artifact. | - -`toolchain-coupled` is worth spelling out, because the obvious reading is -wrong. `ucrtbase.dll` *is* a Windows component (since Windows 10) and mcpp -never ships it. `vcruntime140.dll` and `msvcp140.dll` are **not**: every MSVC -toolset carries them under `VC\Redist\MSVC\\\`, exactly the -way a gcc payload carries `libstdc++.so`. Under this contract mcpp stages them -beside the artifact — which is what makes a default `/MD` build runnable on a -machine that has only the pinned toolset and no Visual Studio at all. - -The debug CRT (`vcruntime140d.dll` and friends, under `debug_nonredist\`) is -never staged: it may not be redistributed. - -> **Upgrading from 2026.8.15 or earlier?** This key used to be **inert** on the -> MSVC ABI — it reported `not implemented for the MSVC runtime yet` and every -> value fell back to `/MD`. Since 2026.8.16 it is honoured, so a manifest that -> carries `cxx_runtime = "self-contained"` from that era **changes CRT model on -> upgrade**, from `/MD` to `/MT`. It is not a stricter version of the same -> model, and the switch is silent because the value was always valid. A project -> that set it while the key did nothing should re-confirm the intended value. - -Combining it with `/MT` is a contradiction rather than a missing feature — a -static CRT leaves no DLL to couple to — so it is reported and resolved to -`self-contained`. `mcpp pack` enforces the other half: a mode that bundles -nothing (`--mode system`, `--mode static`) cannot deliver `toolchain-coupled` -and refuses. - -**Scope.** The contract governs the C++ runtime only. Static **libc** is a separate -axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment -floor is a third (`macos_deployment_target`). Also, `host-coupled` means mcpp adds -nothing to embed a C++ runtime; it does not strip the toolchain rpath the link -carries for other reasons, so on ELF such an artifact may still find the -toolchain's libraries first. - -> **macOS + `self-contained` and static initialization order.** Mach-O has no -> priority-ordered initializer section and libc++'s `` carries no -> `ios_base::Init` guard of its own (unlike libstdc++ and the MSVC STL), so a -> stream initializer pulled out of `libc++.a` would otherwise run *after* the -> program's own global constructors — a global whose constructor touches -> `std::cout` would read an unconstructed stream and crash at process start. mcpp -> links a tiny generated object first to force the streams up; nothing is required -> of the calling code. See mcpp-community/mcpp#336. - -`defines` takes **bare** macro names (no `-D`) and desugars each entry to `-D` on -both the C and C++ compile channels. It reaches every TU in the package — module -interface units included — so it also reaches the compiler's own P1689 module scan. - -> **It does not make a macro-guarded `import` acceptable.** mcpp runs its own -> lexical pre-scan before the compiler ever sees the file, and that scanner -> rejects an `import` inside **any** `#if` / `#ifdef` block without evaluating the -> condition: -> -> ``` -> error: import statement inside conditional preprocessor block (forbidden in M1) -> ``` -> -> So a `#ifdef FOO` / `import bar;` pair fails even when `FOO` is in `defines`. -> Put the conditional around an `#include` in the global module fragment instead. -> Tracked as mcpp-community/mcpp#421. Assembly units pick it up too. It is a -build input like any other, so `[target.'cfg(...)'.build]` can carry it: - -```toml -[build] -defines = ["APP_NAME=\"demo\""] - -[target.'cfg(windows)'.build] -defines = ["USE_WIN32", "WINVER=0x0A00"] -``` - -Picking the right axis: - -| You want the macro on… | Use | -|---|---| -| every TU of this package | `[build].defines` (here) | -| one binary's own entry source only | `[targets.].defines` | -| a specific set of files | `[build].flags` with a `glob` + `defines` | -| every TU **and** every consumer's TUs | `[features.].defines` (an interface contribution) | - -`[build].defines` is private to the package: it does not propagate to consumers. - -Unsupported keys under `[build]` are reported as a warning (an error under -`--strict`) rather than silently ignored. - -Do not configure the C++ standard via `build.cxxflags = ["-std=..."]`. Instead use: - -```toml -[package] -standard = "c++26" -``` - -mcpp applies the same standard to ordinary C++ compilation, module scanning, -`compile_commands.json`, and the standard library BMI build for `import std`. - -**glob exclusion** (`!` prefix, mcpp 0.0.4+): - -```toml -[build] -sources = [ - "src/**/*.cpp", - "!src/**/*_test.cpp", # Exclude test files - "!src/**/*_fuzzer.cpp", # Exclude fuzzers -] -``` - -**Per-glob flags** (mcpp 0.0.95+): `[build] flags` is an ordered array of -inline tables attaching extra compile flags to exactly the sources a glob -matches — the tool for SIMD dispatch TUs and vendored-code warning isolation: - -```toml -[build] -flags = [ - { glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] }, - { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] }, - { glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] }, -] -``` - -Keys per entry: `glob` (package-root-relative, required) plus `cflags` / -`cxxflags` / `asmflags` / `defines` (no `ldflags` — linking has no per-TU -scope). Declaration order is application order: a later entry's flags land -later on the command line, so with GNU "last flag wins" a narrower glob -placed after a broader one overrides it. All matching entries apply. These -are private build flags — they never propagate to consumers. A glob that -matches no source file prints a warning (a typo'd glob must not silently do -nothing). - -**Generated files** (mcpp 0.0.95+): `[generated_files]` maps a relative path -to file contents (TOML multiline strings supported). Entries are written -into the project tree before source globs expand — the same mechanism index -descriptors use to synthesize module wrappers — and the content enters the -fingerprint, so editing it rebuilds: - -```toml -[generated_files] -"src/gen/wrap.cppm" = """ -module; -#include -export module wrap; -""" -``` - -Paths must stay inside the project root (`..` / absolute paths are parse -errors). - -**Assembly sources** (mcpp 0.0.95+): `.S`/`.s` (GAS — preprocessed by the C -driver, covers ARM and AT&T-syntax x86) and `.asm` (NASM — Intel-syntax x86) -are first-class sources: default-globbed, fingerprinted, built incrementally -in parallel, and linked like any other object. The NASM output format is -derived from the target triple (`elf64`/`win64`/`macho64`/... — cross builds -just work), and `nasm` itself is resolved lazily only when `.asm` units exist: -`PATH` first, then the mcpp sandbox, then `xlings install nasm`; if none -yields nasm ≥ 2.16 the build **fails hard** (assembly is never silently -skipped). Limits: `.asm` targets x86 only (hard error elsewhere — gate the -files off other targets), `.S` is unavailable on the MSVC toolchain, and -`.asm` means NASM syntax (MASM sources should be `!`-excluded). - -### File names outside the host code page - -Globs are narrow strings, and so are compile commands and `build.ninja`. On -Windows those strings are produced in the process's **ANSI code page**, so a -file whose name has no spelling in that code page cannot be matched by a glob, -named on a compile command, or written into a build file. - -Such entries are skipped, and the skip is reported once per directory: - -```text -warning: 'C:/.../pkg/test/www' contains names this system's active code page cannot represent - impact: those files take no part in the build - hint: Windows only: this is the process ANSI code page, which `chcp` does not change. ... -``` - -The reported path is the nearest ancestor whose name the code page *can* spell, -in generic (`/`) spelling. The offending name itself is never printed: rendering -it would throw the same exception the message is reporting. - -`chcp` sets the *console* code page and has no effect here. Names that are only -test data or documentation are harmless — an upstream tarball carrying a -Japanese-named fixture directory builds fine on an en-US host. Sources are not: -they need renaming, or a host whose code page covers them. - -Linux and macOS perform no such conversion, so nothing is skipped there. A -package that builds on one and not the other, with an -`internal: unhandled exception` from a code-page message, was mcpp#516. - -### 2.3.1 `[build] accel` — the accelerator this build targets - -```toml -[build] -accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" -``` - -Which device backends and architectures this build compiles for. Overridden for -one build by `--accel`, the relationship `--target` has with `[toolchain]`; -`--no-accel` requests none explicitly, which is how a CPU-only variant of a -package that also publishes device builds is selected. - -The value is compared against the `accel` field of any prebuilt artifact the -build consumes, and a build asking for none is satisfied by every artifact. See -[20 — Heterogeneous Builds](20-heterogeneous-builds.md). - -### 2.4 `[lib]` — Library Root Module Convention - -```toml -[lib] -path = "src/capi/lua.cppm" # Override the default lib-root location -``` - -Default convention: `src/.cppm` (e.g. package name `mcpplibs.cmdline` → `src/cmdline.cppm`). - -### 2.5 `[dependencies]` — Runtime Dependencies - -```toml -# Packages under the default package namespace (mcpplibs) -[dependencies] -cmdline = "0.0.2" # Exact version -templates = "0.0.1" - -# Dotted selector: one exact identity. Everything before the final dot is the -# namespace; the final segment is the package name. -compat.gtest = "1.15.2" -imgui.core = "0.0.1" -imgui.backend.glfw_opengl3 = "0.0.1" -mcpplibs.capi.lua = "0.0.3" -``` - -```toml -# Namespace sub-table form -[dependencies.mcpplibs] -cmdline = "0.0.2" -tinyhttps = "0.2.2" -llmapi = "0.2.5" - -[dependencies.compat] -glfw = "3.4" # Explicit namespace; no fallback search -``` - -```toml -# Path dependency (local development) -[dependencies] -mylib = { path = "../mylib" } -``` - -```toml -# Git dependency — pick exactly one of tag / branch / rev -[dependencies] -mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } -applib = { git = "https://github.com/user/applib.git", branch = "develop" } -``` - -```toml -# Long-form dep spec: features and backend knobs -[dependencies] -imgui = { version = "0.0.3", features = ["docking"] } # Request a feature of this dependency -widget = { version = "1.0", backend = "glfw_opengl3" } # Sugar for: features=["backend-glfw_opengl3"] -``` - -`backend = ""` is **general-purpose convention sugar**: it desugars 1:1 into -requesting the dependency's `backend-` feature (a library that supports this -knob should declare a `backend-*` family in its own `[features]`). If the target -package declares `[features]` but does not include the requested feature (including -the result of backend desugaring), a warning is issued by default, and an error -under `mcpp build --strict`. - -**Git dependencies and `mcpp.lock`**: a `tag` or `rev` already names a fixed point -in history, but a `branch` moves. The first build resolves the branch to a commit -and records it in `mcpp.lock`, and every later build rebuilds **that** commit — the -lock is authoritative, not a cache hint, so deleting `~/.mcpp/git` or moving to -another machine cannot silently move the build to a newer tip. A newer tip must be requested -explicitly: - -```bash -mcpp update mylib # drop the recorded commit; the next build re-resolves it -mcpp update # same, for every dependency -``` - -Because the recorded commit is enough to decide what to build, a rebuild with the -clone already in `~/.mcpp/git` makes no network request at all and works under -`--offline`. Only two things need the network: resolving a branch that has no -commit in the lock, and cloning a commit that is not cached yet. A `git =` value -that names a local directory (or a `file://` URL) needs neither, so it is never -refused offline. - -**SemVer constraints**: - -```toml -[dependencies] -foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret, default) -bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde) -baz = "=1.2.3" # Exact match -qux = ">=1.0, <2.0" # Range combination -``` - -#### Namespace resolution rules - -Every package has a two-part identity: a **namespace** and a **name**. Every -selector normalizes to exactly one identity: - -- `cmdline` → `(mcpplibs, cmdline)`; omitting the namespace means the - `mcpplibs` default, and nothing else. -- `compat.gtest` → `(compat, gtest)`. -- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`. - -There is no ordered fallback or fuzzy, index-wide search by short name: - -```toml -# Correct — dotted selector -[dependencies] -chriskohlhoff.asio = "1.38.1" - -# Correct — namespace sub-table (preferred for several packages from one org) -[dependencies.chriskohlhoff] -asio = "1.38.1" - -# Wrong — a bare name never reaches the `chriskohlhoff` namespace -[dependencies] -asio = "1.38.1" -``` - -The third form fails with an error that names the exact `(mcpplibs, asio)` -identity that was tried and, when the short name exists elsewhere, gives a -copyable explicit selector. - -##### Migration window for bare names (`2026.8.10.1` → removed in `2026.9`) - -Every published `compat.*` package and every manifest written before exact -identity spells its dependency bare — `gtest = "1.15.2"`. Failing those -outright on upgrade would break builds against data that is already published -and cannot be edited retroactively, so for one release a bare name that misses -`mcpplibs` still reaches `compat.`, and a descriptor that declares no -namespace at all still answers to its bare name. - -It is not quiet about it: - -``` -warning: dependency 'gtest' resolved to 'compat.gtest' through the deprecated -bare-name search; namespace omission means `mcpplibs` only. Write the exact -package: - [dependencies.compat] - gtest = "1.15.2" - (or run `mcpp add compat.gtest@1.15.2`). This fallback is removed in 2026.9. -``` - -What reaches `mcpp.lock`, the install layer and the cache is the canonical -identity, so the ambiguous spelling lives in exactly one place — the manifest — -until it is rewritten. `mcpp add gtest@1.15.2` performs that rewrite. - -The window does **not** apply to a selector that states a namespace -(`mcpplibs.gtest` misses and stays missed), and a bare name still never reaches -a third-party namespace. - -**Why one identity?** Dependency resolution has to be reproducible. Candidate -search would let two namespaces with the same short name be settled by index -state, and adding an index could silently retarget an existing dependency. - -**For xpkg authors:** in an index descriptor, identity is the pair -`(package.namespace, package.name)`. The namespace is the dotted path; **`name` is -a single atomic segment**: - -```lua -package = { - namespace = "chriskohlhoff", - name = "asio", -- one segment; NOT "chriskohlhoff.asio" -} - -package = { - namespace = "mcpplibs.capi", -- depth belongs here - name = "lua", -} -``` - -The filename is only a hint — a descriptor is found by its declared identity, so -`pkgs/c/chriskohlhoff.asio.lua` and `pkgs/z/anything.lua` resolve identically. -`.lua` or `..lua` are recommended (they hit mcpp's fast -path) but not required. - -The older fully-qualified spelling (`name = "chriskohlhoff.asio"`) is still -accepted, so already-published descriptors keep working. `mcpp xpkg parse` -enforces the descriptor rule; run it in index CI. Descriptor identity -requires mcpp >= 0.0.106; exact selectors require mcpp >= 2026.8.10.1; both use -xlings >= 0.4.69. Full normative text is in `docs/specs/package-identity.md`. - -`mcpp new --template` deliberately reuses this identity model instead of -creating another package grammar: `[ns.]name[@version][:tname]`. A bare name -there also means only `mcpplibs`; version and template may be omitted -independently. Omitted `tname` selects the sole explicit default, or the only -template when no `default = true` is present. Multiple unmarked templates are -an error, never a directory-order choice. See the normative template rows in -`docs/specs/package-identity.md` §4.4. - -#### When mcpp refreshes the package index - -`mcpp build` / `run` / `test` refresh the package index **only when a dependency -cannot be resolved from the local copy** — never merely because time has passed. -Concretely, a refresh happens when there is no local index at all, when a -dependency's descriptor is missing from it, or when a SemVer constraint matches -none of the versions it knows. A build whose dependencies all resolve locally -makes no network request, however old the local index is. - -The consequence worth knowing: a constraint like `^1.2` resolves against the -**the versions the local index knows**. A `1.3.0` published upstream after the last refresh -is invisible until it is fetched: - -```bash -mcpp index update # sync the index -mcpp update # sync, then re-resolve dependencies -mcpp index status # local state: state, age and revision -``` - -Controls, in order of precedence: - -| Control | Effect | -|---|---| -| `--offline` (any command) | Never touch the network — no index refresh, no downloads, no toolchain auto-install, no `git ls-remote`/`clone`. Anything already installed still builds, including git deps whose commit is in `mcpp.lock` and whose clone is cached | -| `MCPP_OFFLINE=1` | Same, for a whole shell session or CI job | -| `[index] auto_refresh = false` in `~/.mcpp/config.toml` | Never refresh the index automatically; downloads still work | - -`MCPP_NO_AUTO_INSTALL=1` remains accepted as the older, narrower spelling of -`--offline` (it gates only toolchain auto-install). - -Run any command with `-v` to see the decision for each dependency and why. - -### 2.6 `[dev-dependencies]` — Test Dependencies - -```toml -[dev-dependencies.compat] -gtest = "1.15.2" -``` - -`mcpp build` ignores these; `mcpp test` resolves and uses them. `mcpp test` automatically discovers `tests/**/*.cpp` and compiles them into test binaries. The runner is framework-agnostic: each file is an independent binary judged by exit code — a bare `main`, gtest (via `[dev-dependencies]` + `gtest_main`), or any other framework all work identically, and `-- args` are forwarded to every test binary (e.g. `-- --gtest_filter=...`). Note: synthesized test target names may contain `/` (`tests/00-a/0.cpp` → `00-a/0`), unlike `[targets.*]` names — the two namespaces are intentionally separate (test targets never enter the manifest or publishing). Tests are named by their `tests/`-relative path (`tests/00-a/0.cpp` → `00-a/0`), each test compiles in isolation (a broken test fails alone; package/dep breakage is reported as a build error instead), and `mcpp test ` / `--message-format json` filter and machine-format the run. - -### 2.6.1 `[build-dependencies]` — Build-Time Dependencies (mcpp 2026.8.29.1+) - -```toml -[build-dependencies] -protobuf = { version = "35.1", tools = ["protoc"] } -``` - -The section and the per-edge request answer **different questions**, and -conflating them is a modelling error rather than a matter of spelling. - -- The **section** says whether the package itself reaches the target. - `[dependencies]` means it does; `[build-dependencies]` means it never does, - and neither does anything reachable only through it. -- The **request on the edge** says which build-time product is wanted. - `tools = [...]` asks for a host executable; `host-module = true` asks for a - module the build program can import. - -A package takes a value on both axes at once, and protobuf is the case that -proves the axes must stay separate: a project links `libprotobuf` *and* needs -`protoc` during the build. It is written once, in `[dependencies]`: - -```toml -[dependencies] -protobuf = { version = "35.1", tools = ["protoc"] } -``` - -`[build-dependencies]` is for the combination the first axis cannot otherwise -express — a package whose library must not reach the target while its tool or -its rule is still wanted. Naming one package in both tables is not an error: -the ordinary declaration wins, because a `[build-dependencies]` line must not -quietly drop a library the target needs. - -Unlike `[dev-dependencies]`, these **are** walked transitively: a build -dependency's own dependencies are what make it work, and they inherit its -build-only nature. - -A feature scopes build-time requests without a second declaration site — -`[feature-deps.]` may add `tools` to a dependency already declared -unconditionally, so "only when needed" needs no separate table. - -> The section has been parsed since early versions and, until 2026.8.29.1, read -> by nothing that made a decision: writing it produced a manifest that loaded, -> no diagnostic, and no effect. - -### 2.7 `[toolchain]` — Toolchain Configuration - -```toml -[toolchain] -default = "gcc@16.1.0" - -# Cross-compilation target override -[target.x86_64-linux-musl] -toolchain = "gcc@16.1.0" -linkage = "static" -``` - -### 2.7.1 `[target.*]` — Platform-Conditional Dependencies & Flags - -Scope dependencies and build flags to a platform with a `[target.]` table. -The selector `` has three forms: - -| Selector | Meaning | Example | -|---|---|---| -| **bare OS alias** | a single OS / family — the concise, common form | `[target.windows]`, `[target.unix]` | -| **`cfg(...)` predicate** | a compound condition (arch / env / combinators) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | -| **exact triple** | one specific target (also carries `toolchain` / `linkage` / `sysroot` / `runner`; see §2.7.3) | `[target.x86_64-linux-musl]` | - -A selector may carry platform-conditional **dependencies** and **build flags**: - -```toml -# Concise bare-alias form — pull OpenBLAS and link it only on Windows. -[target.windows.dependencies.compat] -openblas = "0.3.33" -[target.windows.build] -ldflags = ["-Llib", "-llibopenblas"] - -# cfg(...) for compound predicates (grammar: all/any/not over os/arch/family/env, -# plus the bare aliases windows/unix/linux/macos). -[target.'cfg(all(linux, not(arch = "aarch64")))'.build] -cxxflags = ["-march=x86-64-v2"] -``` - -`[target.windows]` is exactly equivalent to `[target.'cfg(windows)']` — the bare -aliases `windows` / `linux` / `macos` / `unix` are never valid target triples, so -there is no ambiguity. Use the bare form for a single OS/family; use `cfg(...)` -for arch/env conditions and combinators. - -- **Keys**: `dependencies` / `dev-dependencies` / `build-dependencies` / - `feature-deps.` (mcpp 2026.8.6.2+ — see §2.14; the feature is - registered unconditionally, only its dependency set is scoped), and - `build` with `cflags` / `cxxflags` / `ldflags` / `sources` (mcpp 0.0.95+ — - conditional source globs, e.g. gating `src/x86/**/*.asm` behind - `cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and - `include_dirs` / `include_dirs_after` (mcpp 0.0.102+), plus - `private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+). -- **What `build` accepts is exactly the set of *additive build inputs*** — the - things that combine by appending and are consumed after the predicate is - evaluated, which is the member list of `BuildInputs`. `linkage`, `target`, - and the profile knobs are deliberately not among them: they are *inputs to* - target selection (conditioning `target` on a predicate evaluated against - `target` is circular), or they need override-rather-than-append semantics. - A key outside the set is reported and ignored; the message lists the set it - checked against, so it cannot drift from the check. -- **Evaluated against the resolved target** — the `--target` triple for a cross - build, otherwise the host. So a native Linux build never even *downloads* a - `[target.windows]` dependency. -- **Predicate keys**: `os`, `arch`, `family`, `env` — the triple's coordinates — - and, from mcpp 2026.9.1.1, the five target-side layer names `compiler`, - `compiler-runtime`, `kernel-abi`, `c-abi`, `c++-abi` - ([14 — The Target Side](14-target-side.md)). `accelerator` is a key here too - and is answered from this build's own `accel` — the backend names in - `--accel` or `[build] accel` — so it is a membership test over a set, and - `accelerator = "none"` is how a section says "this build named no backend" - without enumerating the ones it is not. Barewords `linux` / `macos` / - `windows` / `unix` are sugar for the matching `os` / `family` test. A key - outside this set is reported as a schema warning and the section does not - apply — it used to answer false in silence, which is indistinguishable from - a section that correctly did not match. -- **A resolved-layer predicate cannot select dependencies.** A layer is - resolved *from* the dependency graph, so a dependency chosen by one would - decide the answer it is asking for. `[target.'cfg(c-abi = "musl")'.dependencies]` - is reported and ignored; the `build` inputs under the same predicate do apply. - `accelerator` is not one of these (mcpp 2026.9.6.5): it is an input to the - build rather than an answer from the graph, so - `[target.'cfg(accelerator = "cuda")'.dependencies]` applies. -- **Precedence**: an exact-triple table wins over a `cfg`/alias table; multiple - matching predicate tables have their flags concatenated. Conditional entries - are appended **after** the unconditional `[build]` ones, so under GNU - "last flag wins" a conditional rule overrides a broader unconditional one. - That is what makes a per-OS **removal** expressible: - - ```toml - [build] - flags = [{ glob = "third_party/zlib/**", defines = ["HAVE_UNISTD_H=1"] }] - - # clang-MSVC has no : undo the base define, add the windows one. - [target.'cfg(windows)'.build] - flags = [{ glob = "third_party/zlib/**", - defines = ["NO_FSEEKO"], cflags = ["-UHAVE_UNISTD_H"] }] - ``` - -- **A conditional `flags` entry that does not match the current target does not - exist at all**, so it cannot produce a "glob matched no source file" warning. - One manifest can therefore carry all three OSes' flag tables without any of - them generating noise on the other two — the same way an inactive feature's - entries simply are not there. A zero-hit glob in the *unconditional* table - still warns, because there it is a real defect. -- **`toolchain` / `linkage` / `sysroot` are exact-triple only** — they describe - one specific cross target, so put them under `[target.]` (above), not - under a bare alias or `cfg(...)`. - -#### `sysroot` — the target's C library - -`sysroot` (mcpp 2026.8.20.2+) overrides the C library the target table binds to -a triple, on the same axis as `toolchain` overriding the compiler pin: one names -the compiler a target resolves, the other names its C library, and both were -engine-only until a project had a reason to disagree. - -```toml -[target.riscv64-none-elf] -sysroot = "xim:newlib-riscv@4.4" # a different C library -``` - -```toml -[target.riscv64-none-elf] -sysroot = "" # no C library at all -``` - -**An absent key and an empty one are different answers.** Absent inherits the -target table's C library. Present-and-empty is the **zero-libc tier**: no C -library is resolved, no include or library path is added, and the link carries -only what the project and its dependencies supply. `#include ` stops -resolving. A kernel or a bootloader wants exactly that, and collapsing the two -cases would silently hand such a project the target's C library back. - -The value is an xpkg reference or the empty string; a bare name is rejected when -the manifest is parsed, because accepting it would install nothing and then fail -much later naming a missing libc. - -A build program can ask which C library **payload** supplies the sysroot: -`mcpp::target_libc()` returns that package's name and -`mcpp::target_libc_profile()` the sub-directory for the target's ISA profile. -Both are empty on the zero-libc tier. See -[13 — Bare-Metal and Freestanding Targets](13-baremetal.md). - -**That is not the same question as "which C library did the target side -resolve to".** `target_libc()` names the payload mcpp installed, and that value -is an *input* to target-side resolution — a package in the dependency graph can -supply the C library instead, in which case the resolved `c-abi` is not what -this returns. To branch on the resolved layer, use a layer predicate: -`[target.'cfg(c-abi = "musl")'.build]` ([14 — The Target -Side](14-target-side.md)). This paragraph said "which C library was resolved" -until 2026.9.1.1, which was the wrong one of the two. - -### 2.7.2 Bare metal (`os = none`) — freestanding targets - -`riscv64-none-elf` and `riscv32-none-elf` are targets with no operating system -underneath. They need no per-host cross toolchain: clang and lld are -cross-compilers by construction, so any host that can install the llvm payload -can produce them. - -This section is the manifest reference. The worked examples — scaffolding, -running, testing on the target, the freestanding standard-library subset and -writing a board-support package — are in -[13 — Bare-Metal and Freestanding Targets](13-baremetal.md). - -```bash -mcpp build --target riscv64-none-elf -mcpp run --target riscv64-none-elf # via [target.].runner -``` - -**Starting from a board package** - -Almost nothing below has to be written by hand. A board-support package carries -the C library, the startup code, the memory layout and the emulator, so the -shortest path to a booting image is: - -```bash -mcpp new blinky --template riscv-virt-rt -cd blinky && mcpp run -``` - -The generated manifest names no linker script, load address, libc or emulator — -it has no `[target.*]` section at all. The rest of this section describes what -such a package supplies, which is what to reach for when writing one for a board -that has none. - -**What changes on a freestanding target** - -| | | -|---|---| -| Link line | `-nostdlib -nostartfiles -static`, and nothing hosted — no crt files, no dynamic linker, no C++ runtime. The linker is addressed by **absolute path** (`-fuse-ld=/bin/ld.lld`), because `-fuse-ld=lld` resolves through `PATH` and finds GNU ld on any machine with binutils earlier on it. | -| ISA flags | `-march` / `-mabi` / `-mcmodel` come from the target table, so `--target ` alone is enough to produce a correct object file. | -| C library | **The target's**, resolved by mcpp from the target's own row exactly as the compiler is — a bare-metal project declares no libc, just as a hosted one declares no glibc. Its headers reach every translation unit and its directory is on the link search path, so a board package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). *Which* objects and *which* linker script remain board decisions. | -| Exceptions and RTTI | **Off**, on every translation unit including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise pull in `__cxa_throw` and three more undefined symbols. It belongs to the target rather than to a project's `cxxflags` because a BMI records it — a dependency compiled with exceptions cannot be imported by a unit without them. | -| `import std` | **Unavailable.** `std` is one module over the entire library — threads, filesystem and iostreams included — so there is no subset of it to build without an OS. Two ordinary dependencies replace it: the **board package** wraps the target's C library, and **`std-freestanding`** carries the parts of the standard library that need no OS (103 of libc++'s 110 headers, measured). | -| Entry point | `int main()` works **as long as something supplies a `crt0`** — a board package normally does, and then a firmware's entry point is an ordinary `main` whose return value reaches the host through semihosting. Only a zero-libc board needs an explicit target whose `main` points at the file carrying `_start`. | - -**A minimal firmware** - -```toml -[package] -name = "fw" -version = "0.1.0" - -[build] -ldflags = ["-T", "/abs/path/to/link.ld"] - -[targets.firmware] -kind = "bin" -main = "src/start.S" # the entry lives in assembly, not in main() - -[target.riscv64-none-elf] -runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", - "-no-reboot", "-bios", "default", "-kernel"] -``` - -**`runner` — how `mcpp run` executes something this machine cannot run** - -A bare-metal image has the wrong ISA, no loader, and expects to own the address -space; exec'ing it directly gives "Exec format error". `runner` is the argv -template that stands in front of it. The artifact path is **appended**, or -substituted for `{}` when the template contains it. - -mcpp ships **no default runner**, deliberately. Which emulator, which machine -model and which firmware mode are board facts — two boards on the same ISA need -different argv (`-bios default` for an OpenSBI boot, `-bios none -semihosting` -for a picolibc image) — and an engine that guesses one is an engine the other -board has to fight. A board-support package normally supplies it. - -### 2.7.3 `runner` on a hosted target (2026.9.2.1+) - -`[target.].runner` applies to every exact triple, not only to bare -metal. A hosted cross artifact — `aarch64-linux-musl` built on an x86_64 -machine — is executable by some hosts (binfmt_misc with qemu-user registered) -and refused by others with `Exec format error`, and which of the two applies is -a property of the machine, not of the triple. mcpp does not predict it. It -either executes the artifact through the runner the project declared, or it -attempts direct execution and reports what the kernel answered. - -```toml -[target.aarch64-linux-musl] -runner = ["qemu-aarch64-static"] -``` - -The rules, for `mcpp run` and `mcpp test` alike: - -- **A declared runner is used.** Its first element is located by mcpp: first in - the `bin/` directory of each payload declared under `[xlings.workspace]` - (§2.13), - then on `PATH`. A bare name on `PATH` resolves to an xvm shim, which answers - for the current SubOS rather than for the package; the payload lookup is what - lets a runner name a program the project declared. -- **A declared runner that cannot be found or started is an error**, with the - program, the directories searched and the errno. There is no fallback to - direct execution: running the artifact under a different interpreter with - different arguments is the failure the key exists to prevent. -- **No runner, and the kernel refuses the artifact:** `mcpp run` reports the - refusal and the key to write, and exits 2. `mcpp test` reports every test as - not run, with the reason once, and exits 2 (§2.7.3.1). -- **`--no-runner`** executes the artifact directly and ignores a declared - runner. It states a fact about this host — the triple is native here — that - the manifest has no axis to carry; a project whose runner was written for - x86_64 developers is still readable on an aarch64 machine. - -Provisioning the emulator through `[xlings.workspace]` is the form for a CI job -or a project built on one host class. `qemu-user-aarch64` in the index is built -for x86_64 Linux only, and the table provisions on every host that builds the -project, so the entry is written per platform (§2.13): - -```toml -[xlings.workspace] -"xim:qemu-user-aarch64" = { linux = "" } # present on Linux, any version - -[target.aarch64-linux-musl] -runner = ["qemu-aarch64-static"] -``` - -A package the host cannot install is a hard build error, so an entry without -the platform form would make the project unbuildable on macOS and Windows. The -Linux/aarch64 host, where the package does not exist either, passes -`--no-runner`. - -#### 2.7.3.1 `mcpp test` and tests that were not run - -A test whose artifact this host cannot execute has neither passed nor failed. -`mcpp test` reports it as **not run**, prints the reason once when it is -established, repeats the first line of the reason in the summary, and exits 2: - -``` -warning: this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); declare [target.aarch64-linux-musl].runner, or pass --no-runner on a host that can -smoke ... not run -error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); ...); finished in 0.41s (build 0.39s + run 0.00s) -``` - -Exit code 1 keeps its meaning — a test ran and failed — and 0 means every test -ran and passed. `--message-format json` carries `"status":"not_run"` and a -`reason` on each record, and `not_run` / `not_run_reason` on the summary -record (see [11 — Machine-Readable Output](11-machine-output.md)). - -### 2.8 `[features]` — Features (Cargo-style, additive) - -```toml -[features] -default = ["base"] # Default activation set -base = [] -docking = ["extra"] # Activating docking implies activating extra (transitive closure) -extra = [] -``` - -- Activation sources: the package's own `default` set ∪ explicit requests (the root - package via `mcpp build --features a,b`; dependencies via the long-form dep spec's - `features = [...]` / `backend = "..."` sugar). -- Each activated feature gets the macro `-DMCPP_FEATURE_` during that package's - compilation (the name is uppercased and non-alphanumerics become `_`, e.g. - `backend-a` → `MCPP_FEATURE_BACKEND_A`). -- **strict validation**: when the target package declares a `[features]` table, - requesting an undeclared feature produces a warning; an error under `--strict`. A - package that does not declare `[features]` accepts any request (pure macro usage). - -#### Table form — a feature that contributes more than implied features - -A `[features]` entry may be written as a **table** instead of an array, letting the -feature carry package-owned preprocessor `defines`, feature-gated source globs -(`sources`, mcpp 0.0.95+ — the globs leave the default build and compile only when -the feature is active, exactly like an index descriptor's `features..sources`; -the highest-frequency shape for vendored libraries: *feature = a source set + a -define*), feature-gated per-glob compile flags (`flags`, mcpp 0.0.101+), and/or -capability `requires` / `provides` (see §2.8.1) alongside its implied features: - -```toml -[features] -default = [] -# Array shorthand: just implied features. -docking = ["extra"] -extra = [] -# Table form: contribute a package-owned define when active. -mpl2only = { defines = ["EIGEN_MPL2_ONLY"] } -# Table form: a define + an implied feature. -fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] } -# Table form: feature-gated sources + per-glob flags that co-locate with them. -simd = { sources = ["src/simd/**"], flags = [ - { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] } -``` - -- **The table form accepts exactly** `implies`, `forward`, `defines`, `sources`, - `flags`, `requires`, `provides`. Anything else is reported as a schema warning - and ignored (mcpp 2026.9.1.1+); `deps` is reported separately as reserved and - points at `[feature-deps.]`. Before that release `[features]` was - the one structured section with no schema check at all, so a misplaced - `include_dirs` inside a feature built successfully with no diagnostic while - the identical mistake in `[build]` was reported. -- `defines` are **bare** macro names (no `-D`); each desugars to `-D` on the - package's own compile when the feature is active — exactly like `[targets.*] - defines`. They are restricted by convention to the package's **own** namespaced - macros: a feature does **not** inject free-form package-wide `cflags`/`ldflags`, - which would break the additive feature-union model. Link flags come from a - provider dependency (§2.8.1), not from a feature. -- The automatic `-DMCPP_FEATURE_` is still defined for every active feature, - so `defines` are additive to it. -- `flags` (mcpp 0.0.101+) is the same ordered array-of-inline-tables grammar as - `[build].flags` (§2.3: `glob` required, plus `cflags`/`cxxflags`/`asmflags`/ - `defines`; the `[[features..flags]]` array-of-tables spelling is accepted - too, like `[[build.flags]]`). When the feature is active the entries - are appended **after** the base `[build].flags`, features in name order, so a - feature rule wins over a broader base rule via "last flag wins"; when it is - inactive the entries do not exist (no dead-glob warning). This is how a - feature's group-specific flags co-locate with its `sources` instead of living - as base rules whose globs go dead on feature-off builds. Unlike `defines`, - feature `flags` are **private per-TU build flags** — they never propagate to - consumers (same contract as `[build].flags`), so they stay inside the additive - model: scoped by glob, deterministic order, no cross-package effect. - - -#### A feature that is a build rule (mcpp 2026.9.7.1+) - -Two keys turn a feature into a build rule other packages can use. They are what -lets a consumer write one dependency edge and no build program. - -```toml -[features.rules-spirv] -sources = ["rules/spirv.cppm"] -rule_module = "mcpp.rules.spirv" -device_extensions = [".comp", ".vert", ".frag", ".glsl"] -``` - -`device_extensions` states which **device source** extensions this rule -compiles. A consumer that activates the feature gets them classified as device -sources -- never scanned for imports, never a BMI, compiled by something mcpp -does not drive. This is the same shape as `[build] module_extensions`: mcpp -knows what a device source *is* and does not know that `.cu` is CUDA, so a NEW -device language costs no engine change. It is what makes -[20 — Heterogeneous Builds](20-heterogeneous-builds.md)' claim that "a sixth -backend is a package rather than an engine change" true rather than -aspirational; `.slang` was removed from mcpp's built-in table and now arrives -this way. - -`rule_module` names the module a consumer's build program imports to reach the -rule, and whose `compile()` it calls. Declared rather than scanned out of the -source, because the program has to be **written** before anything is compiled -and a build that scanned a dependency to decide what to write would order the -two the wrong way round. - -Two things follow, and neither puts a package name inside mcpp: - -- **`host-module = true` is implied.** A feature naming a rule module has - already said that is the only way to use it, so the edge does not repeat it. -- **A package with no `build.mcpp` gets one.** mcpp writes the program those - rules describe into the build directory and compiles that. A package with its - own program keeps it: the synthesis fills an absence and never overrides, and - the generated file is the program a project would have written, so taking it - over is a copy and an edit. - -The feature is still requested **by name**: - -```toml -[build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"] } -``` - -An earlier design derived the set from the extensions a project's sources -carried. It was withdrawn because two packages may claim one extension -- a -third-party CUDA rule is a thing someone will write -- and because a manifest's -job is to describe the build, which a derived feature set no longer does. - -Both keys must appear together. One without the other is a declaration nothing -can act on, and it is refused at parse time rather than in a consumer's build. - - -### 2.8.1 `provides` / `requires` — Capabilities (backend selection) - -A **capability** is a shared abstract name (e.g. `blas`). A package can *provide* -one; a feature can *require* one instead of naming a concrete package, and the -resolver binds exactly one provider from the dependency graph. This is how a build selects -one of several interchangeable backends (OpenBLAS / MKL / …) without baking a choice -into the library. - -```toml -# A provider package satisfies a capability for any dependent that requires it. -[package] -name = "compat.openblas" -version = "0.3.0" -provides = ["blas", "lapack"] -``` - -```toml -# A consumer requires the abstract capability via one of its features. -[features] -use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } - -# When >1 provider is in the graph, pick one (else the build errors and lists them). -[capabilities] -blas = "compat.openblas" # equivalently: mcpp build --cap blas=compat.openblas - -[dependencies] -compat.openblas = "0.3.0" # the provider must be a real dependency in the graph -``` - -The reserved prefix `mcpp:` names the target-side layers this engine resolves, -and those names are validated against a closed set. A package-level `requires` -array carries the symmetric statement — what a target-side layer must resolve to -for this package to be usable. - -```toml -[package] -name = "acme.llvm-runtime" -version = "0.1.0" -provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"] -requires = ["mcpp:compiler=llvm"] -``` - -A package that is a standard library states its `std` module source under -`[build]`, where the flags it needs become conditional like any other build -input. - -```toml -[build] -std-module = "llvm-generated/std.cppm" -std-compat-module = "llvm-generated/std.compat.cppm" -std-module-flags = ["--no-default-config", "-nostdinc++"] - -[target.'cfg(c-abi = "musl")'.build] -std-module-flags = ["-D_GNU_SOURCE"] -``` - -See [14 - The Target Side](14-target-side.md) for the five layers, the rules -that govern them, and the diagnostics. - -Binding is **deterministic**: - -| Providers of a required capability in the graph | Result | -|---|---| -| exactly one | bound automatically (no config needed) | -| a `[capabilities]` pin / `--cap` names one | the pin wins | -| zero | **error**: no package provides `` | -| two or more, unpinned | **error**, listing the candidates — never a silent guess | - -The bound provider's link/include flags reach the consumer through normal -dependency mechanics; the capability layer is the *selection-and-validation* step -that turns a silently-wrong or missing backend into a loud configure-time error. - -**Binding selects a provider; it does not prune the link line.** A dependency -package contributes its object files to the consumer's link regardless of whether -its capability was the one bound. Measured with two packages that both provide -one capability and both define `cap_probe`: unpinned, resolution fails as the -table says; after pinning one with `[capabilities]`, the build reaches the linker -and fails there instead — - -``` -ld: obj/mcpplibs_pa/src/impl.o: in function `cap_probe': - multiple definition of `cap_probe'; obj/mcpplibs_pb/src/impl.o: first defined here -``` - -This matters for a capability whose providers define the **same symbols** — a -whole-program singleton such as `operator new`, or a C API with one fixed name -set. For those, two providers in the graph is a defect to fix rather than an -ambiguity to pin: pinning replaces an error that names both candidates with one -that names a mangled symbol. Interchangeable *libraries* (BLAS implementations, -which export distinct symbol sets and are selected per link) are unaffected. - -#### `exclusive` — a package declaring it is the only provider - -The paragraph above describes a defect the engine cannot detect. Seeing that two -providers define the same symbols requires their object files, which do not -exist when capabilities are bound; and refusing every duplicate provider as a -rule would break the BLAS case in the same paragraph, which is legitimate. - -So the package says it: - -```toml -[package] -name = "compat.cublas" -provides = ["gpu-blas"] -exclusive = ["gpu-blas"] -``` - -Two packages that both provide `gpu-blas`, where at least one declares it -exclusive, are refused when capabilities are bound — before anything is -compiled, naming the capability and both providers: - -``` -error: capability 'gpu-blas' is provided by more than one package, and they - declare it EXCLUSIVE. - providers: [compat.cublas, compat.rocblas] - exclusive: [compat.cublas, compat.rocblas] - Two implementations of one interface define the same symbols, so the - link would resolve every call to whichever archive it reached first. - Keep one of them — a `[capabilities]` pin selects a provider for a - REQUIREMENT and cannot make two definitions of one symbol safe. -``` - -The refusal reports `exclusive-capability` in `--format json` (chapter 11). - -#### `version-floor` — needing more of the machine than it has - -Some facts about a machine bound what may be built for it, and the failure when -they are ignored arrives late: a program built against a runtime newer than the -driver it will meet links cleanly and fails at first use, naming neither side. - -A package states what it needs: - -```toml -[[runtime.requirements]] -kind = "version-floor" -value = "cuda.driver >= 12.0" -``` - -and a package that established a fact about this machine — at install time, -which is where probing belongs — states it: - -```toml -[runtime] -provides = ["cuda.driver=12.4"] -``` - -mcpp compares them when capabilities are bound and refuses before anything is -compiled, reporting `version-floor-unmet`: - -``` -error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4. - stated by: driverfact -``` - -**No vendor vocabulary reaches the engine.** It reads a name, a relation and a -version; `cuda.driver` is data passing through, and a backend mcpp has never -heard of compares the same way. - -**A floor nobody answered is silent.** A machine that never declared what it -has is not a machine that fails the floor — it is one nobody asked. Turning -"we do not know" into "no" is the failure mode this exists to avoid, and it is -asserted directly: `tests/e2e/603_version_floor.sh` builds a project whose floor -names something no package provides. - -**It is a claim about this package's own symbols**, so an entry that names a -capability the package does not provide is reported as a schema warning: there -is nothing to be exclusive about. And a capability nobody declares exclusive -behaves exactly as before — two BLAS implementations still coexist, and the -existing "two or more, unpinned" error still applies only when something -*requires* the capability. - -### 2.8.2 `[feature-deps.]` — dependencies a feature pulls in - -A dependency declared under `[feature-deps.]` is **optional**: it is -resolved only when that feature is active (root `--features`, or a dependency -spec's `features = [...]`). A dependency in `[dependencies]` is always resolved; -optionality is expressed by *where* the entry is declared, not by a flag. - -```toml -[features] -use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } -backend-openblas = { implies = ["use_blas"] } - -# Pulled ONLY when `backend-openblas` is active. Each entry is a full dependency -# spec (version/path/git + its own features). -[feature-deps.backend-openblas] -compat.openblas = "0.3" -``` - -**`"^0.3.0"` and not `"0.3.x"` or `"0.3"`.** Measured against a package the -index certainly carries, with a **build** as the criterion: - -| Written | Result | -|---|---| -| `cmdline = "0.0.1"` | builds | -| `cmdline = "^0.0.1"` | builds | -| `cmdline = "0.0"` | resolves, then `install path missing after fetch` | -| `cmdline = "0.0.x"` | `E_NOT_FOUND`, naming the package — which exists | - -The three outcomes are worth distinguishing, because two weaker criteria each -admit a form that does not work: "no `E_NOT_FOUND`" admits the two-segment -prefix, and "resolves" admits it as well. Only building against the real index -settles it. - -This matters more here than in `[dependencies]`. A feature whose -implementation cannot be fetched is a feature that does not exist, and a project -using a **path** dependency during development never consults the index — so the -failure appears only after publication, to somebody else. - -This composes with capabilities (§2.8.1): a single `backend-openblas` feature -both **pulls** the provider (`compat.openblas`, which `provides = ["blas"]`) and -**turns on** the consumer switch (`implies = ["use_blas"]`, which -`requires = ["blas"]`). With one provider in the graph the capability binds -automatically — `features = ["backend-openblas"]` is all the consumer writes. - -In an index package's Lua descriptor the same is written inline: - -```lua -features = { - use_blas = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } }, - ["backend-openblas"] = { - implies = { "use_blas" }, - deps = { ["compat.openblas"] = "0.3.x" }, - }, -} -``` - -#### A default implementation that stays replaceable - -The same three pieces cover the case where a library wants to *offer* an -implementation without *imposing* one — a whole-program singleton such as -`operator new`, a logging sink, or a panic handler: - -```toml -[features] -default = [] -# The consumer-side switch: "I use the part of this library that needs an allocator". -alloc = { requires = ["freestanding-allocator"] } -# The built-in default: activating this one is enough. -alloc-kal = { implies = ["alloc"] } - -# Resolved only when `alloc-kal` is active, so the library itself carries no -# dependency on the implementation. -[feature-deps.alloc-kal] -std-freestanding-alloc-kal = "0.1.x" -``` - -Three usages, one line each: - -| Consumer needs | What the manifest says | -|---|---| -| none of the allocating parts | `std-freestanding = "0.2.0"` — no allocator enters the graph | -| the default | `features = ["alloc-kal"]` — the implementation arrives with it, and its package name never has to be known | -| its own or a third party's | `features = ["alloc"]` plus a package that `provides = ["freestanding-allocator"]` | - -Two properties make this preferable to shipping the implementation -unconditionally. A library that ships one takes a decision belonging to the -program, and it cannot be undone: features are **additive**, so there is no way -for a consumer to switch a default *off*. And because a dependency package's -objects link unconditionally (§2.8.1), a shipped default plus a program-supplied -one is a duplicate definition rather than a replacement — the archive semantics -that let a C++ standard library offer a replaceable `operator new` do not apply -to a package dependency. Keeping the implementation behind a switch means the -two never coexist. - -### 2.8.3 `[scan_overrides.""]` — Author-Asserted Scan Results - -The default module scanner is a text-level pass that (deliberately) rejects -`import` statements inside conditional preprocessor blocks. Some legitimate -module units carry them — e.g. fmt's official `src/fmt.cc` guards -`import std;` behind `#ifdef FMT_IMPORT_STD`. When the file's import set is -known and stable, declare it instead of scanning: - -```toml -[modules] -sources = ["src/**/*.cppm", "vendor/fmt.cc"] - -[scan_overrides."vendor/fmt.cc"] -provides = ["fmt"] # at most one provided module per unit -imports = ["std"] -``` - -Files matched by the glob skip the text scan; the declared unit enters the -module graph directly. The declaration is **audited every build**: the -compiler's own P1689 scan of the file (the `.ddi` dyndep input) is compared -against it, and any divergence fails that compile edge with both sides -printed — a stale declaration cannot silently corrupt the graph. An override -glob that matches no source file is an error. - -The same key exists in xpkg descriptors (index packages): - -```lua -mcpp = { - sources = { "*/src/fmt.cc" }, - cxxflags = { "-DFMT_IMPORT_STD" }, - scan_overrides = { - ["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } }, - }, -} -``` - -To extend the plan-vs-ddi audit to *every* module unit (not just overrides), -set `MCPP_VERIFY_MODGRAPH=1` when generating the build. - -### 2.9 `[profile.]` — Build Profiles - -```toml -[profile.dist] -opt = 3 # -O level (a number, or the string "s"/"z") -debug = false # -g -lto = true # -flto (note: some packaged gcc builds ship without the LTO plugin) -strip = true # -s at link time -# passthrough escape hatch (fixed keys, open values): -cflags = ["-fno-plt"] -cxxflags = ["-fno-plt"] -ldflags = [] -``` - -- Selection & default: a bare `mcpp build` uses the **`dev`** profile (`-O0 -g`) — the - mainstream convention (cf. Cargo/Meson/CMake/Zig/Bazel). **Release is opt-in:** - `mcpp build --release` (shorthand) or `--profile release`. `--dev` is the explicit - shorthand for dev. Same applies to `mcpp test --profile ` (builds the - code-under-test plus the test binaries under that profile). -- **Per-project default** — `[build].default-profile = ""` (alias: `profile`) sets - the project's own default when no flag is passed. The typical use is a tool/library - that should build optimized by default: `[build] default-profile = "release"`. Precedence: - `--profile`/`--release`/`--dev` flag **>** `[build].default-profile` **>** global `dev`. - (A project that defaults to dev should pass `--release` when producing a distributable.) -- Built-in profiles: `release` (-O2) / `dev`, `debug` (-O0 -g) / `dist` (-O3 + strip; - **LTO is not enabled by default**). `[profile.]` can override a - built-in definition wholesale. -- **Each profile owns its own build directory.** The resolved profile knobs - participate in the fingerprint, so `target//` holds one hash directory - per profile and switching between them is incremental instead of a full - rebuild. It also means the disk cost scales with the number of profiles - actually use. - -### 2.10 `[build] cache` — The Global Dependency Cache - -Compiled artifacts for dependencies fetched from an index are cached across -projects under `$MCPP_HOME/build-cache/v1/`. A dependency's artifacts do not -depend on who consumes them, so two projects with the same toolchain, profile and -dependency versions reuse one entry. - -```toml -[build] -cache = "global" # "global" (default) | "local" | "off" -``` - -| Mode | Reads the cache | Writes the cache | Clears the build dir first | -|---|---|---|---| -| `global` (default) | yes | yes | no | -| `local` | no | no | no | -| `off` | no | no | yes | - -`local` builds every dependency inside this project's `target/` — useful to rule -the cache out while diagnosing something, and to give CI a no-sharing baseline. -`off` additionally clears this build's `target///` for a cold -rebuild; `--no-cache` is a deprecated alias for it. - -Precedence: `--cache ` **>** `MCPP_BUILD_CACHE` **>** `[build] cache` **>** -`global`. An unrecognized value is reported (an error under `--strict`) rather -than silently falling back to `global`. - -What is **not** cached: `path` and `git` dependencies, at any depth, and -workspace members. Their sources can change without their `name@version` -changing, so no key over that identity could notice the change. - -Inspection and reclamation: - -``` -mcpp cache dir # where the cache lives -mcpp cache list [--json] # entries, sizes, last use -mcpp cache info @ # one entry, including the key inputs it was built with -mcpp cache verify # every entry's file list against the disk -mcpp cache gc --max-size 5GiB # LRU-collect package entries to a budget -mcpp cache gc --older-than 30d # ...or by how long since they were last used -mcpp cache clean [--deps|--std|--all|--legacy] -``` - -The on-disk entry layout is versioned. An mcpp release that changes it retires -every older entry at once, so the first build after such an upgrade rebuilds -its dependencies and repopulates — nothing to clean by hand. 2026.8.3.4 did -exactly that: an entry's object paths are now addressed relative to the -*package*, never to the build directory of whichever project happened to -populate the entry first. `mcpp cache verify` additionally reports any entry -whose recorded addresses escape it, so a recurrence is auditable offline. - -### 2.11 `[runtime]` — Provider-neutral runtime contract - -```toml -[runtime] -requirements = [ - { kind = "capability", value = "display.present", phase = "run", required = true, - discovery = "rpath-of-dispatch" }, - { kind = "soname", value = "libwidget.so.1", phase = "link", required = false }, -] -provides = ["display.present"] -artifacts = [ - { role = "library", path = "runtime/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:...", host_fingerprint = "host-1" }, -] - -# Platform-neutral LinkIntent. Paths are relative to this package root. -libraries = ["widget"] -link_library_dirs = ["lib"] -transitive_needed_dirs = ["runtime/closure"] -runtime_search_dirs = ["runtime"] -frameworks = ["WindowKit"] -deploy_files = ["bin/widget.dll"] - -# Use an exact canonical identity when multiple providers exist. -[runtime."display.present"] -provider = "acme.widget-runtime@2.0.0" -``` - -`requirements` records a non-empty `kind`/`value`, a `link` or `run` phase, -and whether the requirement is mandatory (`required` defaults to `true`). - -`discovery` is optional and says **how the loader finds** whatever satisfies the -requirement — e.g. `rpath-of-dispatch`, `json-dir`, `glvnd-dispatch`. It is -**declared, never inferred**: which mechanism a capability uses is the -provider's property and changes without mcpp, so mcpp carries the value and -reports an undeclared one as `unknown` rather than guessing. It earns a field -because the mechanisms are not interchangeable — one may be a search path baked -into a dispatch library, another a JSON file holding an *absolute* path, so -"copy the directory across" satisfies one and not the other. `mcpp pack` writes -it into the bundle's `HOST-REQUIREMENTS` and `mcpp publish` projects it into -the descriptor, from one derivation. -Optional requirements remain visible provenance but do not become hard ABI or -doctor inputs. A `libraries` entry that is an explicit relative file path is -resolved against the declaring package root; a bare logical name remains a -platform-spelled library name. -`artifacts` requires `role`, `path`, and `provenance`; `abi`, `digest`, and -`host_fingerprint` are optional evidence. The resolver, not the descriptor, -stamps every requirement with the exact requester PackageId and every artifact -with the exact declaring provider PackageId, including namespace, version, and -source/index provenance. A descriptor therefore cannot spoof another package, -and `alpha.backend` never collapses into `beta.backend`. - -Only `provides` creates a descriptor-owned provider fact. Merely requiring a -capability never makes the requester its own provider. An explicit -`[runtime.] provider=` override accepts a canonical -`namespace.name@version` (or an unambiguous compatibility spelling); missing or -same-short-name ambiguous providers are hard errors. Provider/artifact facts -already selected by the xlings SubOS precede descriptor fallbacks. xlings/xim -owns graphics-stack, driver, ICD, WSL, and host provenance selection; mcpp -records and consumes the generic result and never probes GPU hardware. - -Link intent keeps discovery stages separate: - -| Field | ELF | Mach-O | PE/Windows | -|---|---|---|---| -| `link_library_dirs` | `-L` | `-L` | `-L` or `/LIBPATH:` | -| `transitive_needed_dirs` | `-Wl,-rpath-link` | no flag | no flag | -| `runtime_search_dirs` | RUNPATH/rpath only, never `-L` | rpath only | no flag | -| `frameworks` | no flag | `-framework` | no flag | -| `deploy_files` | copy edge | copy edge | copy beside the output; never a linker flag | - -For one compatibility train, `library_dirs` maps only to runtime search, -`dlopen_libs` maps to required run-phase soname requirements, and -`capabilities` maps to required run-phase capability requirements. None of -these legacy fields creates a provider. - -`target///resolution.json` schema 2 stores the RuntimeBinding, -canonical requirements/providers/artifacts, LinkIntent, platform search -mechanism, and post-link verdict. `mcpp why runtime` is a pure interpreter of -the latest stored file: it neither re-resolves the manifest nor launches a -graphics/hardware probe. Use `xlings doctor` when the selected host provider -itself needs re-diagnosis. - -Each artifact also carries an `identity` verdict, computed from paths alone: - -| `identity` | meaning | -|---|---| -| `ok` | the declared path resolves (through symlinks) into the declared version | -| `mismatch` | it resolves somewhere else — **the binding is stale**, a later install repointed it | -| `missing` | declared, but nothing is at that path | -| `unverified` | declared without a version to check against | - -This is the rule mcpp already applies to the private libc (`glibc@2.44` resolves -that one payload; stale or missing is an error, never "whichever installed -version looks usable"), generalised. It needs no knowledge of what the artifact -does. `unverified` is deliberately not `ok`: a resolved provider with no -artifact behind it has not been checked, and `mcpp why runtime` says -`(not declared by the environment — nothing to verify)` rather than `(none)`. - -Capability names use layered lowercase `domain.sub.role` (for example -`display.present`) and prefix-style `abi:` (for example `abi:glibc`, which -participates in toolchain ABI enforcement). - -### 2.12 `[package] platforms` — Platform Declaration - -```toml -[package] -platforms = ["linux", "macos", "windows"] -``` - -Declares the platforms the package supports (a CI matrix hint, shown via `mcpp why`). -The vocabulary is fixed by mcpp (which owns the target/triple system): -`linux | macos | windows`; unknown values produce a warning, and an error under -`--strict`. - -`mcpp pack` on a library target checks the claim against the legs it actually -produced, because that is the first moment there is evidence to check it against: - -| situation | result | -|---|---| -| a leg was packed for a platform not listed here | warning — the manifest disclaims a platform the package demonstrably serves | -| a listed platform has no leg, **and this host could have built one** | warning — consumers there will resolve the package and find no artifact | -| a listed platform has no leg and this host cannot build for it | **silent** | - -The third row is why the check is usable at all. The normal release flow is one -`mcpp pack` per platform in CI, so a Linux runner never produces a macOS leg — -warning about it would fire on every run of every cross-platform package, and a -warning that always fires hides the one that matters. What "this host could have -built" means is the same question `--target` answers (docs/08 §7.4). - -Both are warnings, never errors: coverage is release discipline, and the person -who can judge it is looking at the release, not at this build. - -### 2.12b `[package] accelerators` — Accelerator Declaration - -```toml -[package] -accelerators = ["cuda", "rocm"] -``` - -Declares the accelerator backends the package supports. Mirrors `platforms`: a -statement of intent and a CI-matrix hint, shown by `mcpp why`, never a gate. - -Distinct from an artifact's `accel` field on purpose. A declaration is written -by hand and may be aspirational; `accel` is measured from the build that -produced a binary and is what a consumer is refused against. See -[20 — Heterogeneous Builds](20-heterogeneous-builds.md). - -### 2.13 `[xlings]` — the project's environment - -```toml -[xlings.workspace] # what this project's environment contains -cmake = "3.28" -"xim:picolibc-riscv" = "1.8.12" # a namespaced package - quotes required -code = "" # present; version unconstrained -llvm = { macosx = "20", default = "22" } -``` - -```toml -[xlings] -subos = "dev" # a named, isolated environment -``` - -`[xlings]` is mcpp's surface for **xlings' local project mechanism**: the -project `.xlings.json` that gives a directory its own environment. The -subsection names and their meanings are that file's, and mcpp materializes them -into `/.mcpp/.xlings.json` with no translation layer. - -**`[xlings.workspace]` is the one table.** An entry names a package and the -version this project uses it at. mcpp provisions it — installing it when the -machine does not have it, mapping it when it does — and materializes it as a -resolution pin, so the version the project named is the one its tools resolve -to. - -#### Writing an entry - -| Form | Means | -|---|---| -| `cmake = "3.28"` | that version | -| `llvm = "22"` | the highest installed `22.*`; a version prefix resolves | -| `code = ""` | present, version unconstrained | -| `"xim:picolibc-riscv" = "1.8.12"` | a package from the `xim` index | -| `llvm = { macosx = "20", default = "22" }` | per host platform | - -**A namespaced package is written `":" = ""`, and the -quotes are required** — a TOML bare key cannot contain a colon. That is the -recommended form and the one every official package uses: an entry names a -package and then says which version of it, so the namespace belongs to the -name. - -The namespace is also accepted on the version (`picolibc-riscv = "xim:1.8.12"`), -because that is what the materialised `.xlings.json` carries — a key there is an -xvm target and the scope qualifies the version. Two vocabularies, one entry. -Writing it on both halves with different values is an error, and so is naming -one package twice under two spellings. - -Platform keys are xlings' own — `linux`, `macosx`, `windows` — plus `default`. -`macos` and `macosx` are the same platform written in two vocabularies (mcpp's -triples say one, descriptors and xlings' project file say the other) and both -are accepted wherever a platform is named. A table with no key for this host -and no `default` declares nothing here. - -#### Two resolution axes — host and target (mcpp 2026.9.6.4+) - -A tool entry answers one of two different questions, and the table it is written -in decides which: - -| Written | Axis | Resolved against | -|---|---|---| -| `[xlings.workspace]`, platform keys in the value | host | the machine running the build | -| `[target..xlings.workspace]` | target | the resolved target (`--target`, else the host) | - -Both are correct spellings and neither supersedes the other. A tool that -executes on the build machine belongs on the host axis; a payload the produced -code is compiled or linked against belongs on the target axis. - -```toml -[xlings.workspace] -"xim:dpcpp" = "7.1.0" # a compiler, and it runs here - -[target.'cfg(os = "linux")'.xlings.workspace] -"xim:glibc" = "" # what the device units are compiled against -"xim:linux-headers" = "" -``` - -On a native build the two axes name the same platform, so a project that states -target facts on the host axis is right by accident and keeps working. It stops -being right the first time that project is cross-compiled. **For anything the -produced code is compiled or linked against, the target axis is the recommended -form.** - -`[target..feature-xlings.]` composes the condition with the -gate, exactly as `[target..feature-deps.]` does: the selector -says which targets, the feature says whether at all. - -```toml -[target.'cfg(os = "linux")'.feature-xlings.backend-vulkan] -"xim:shaderc" = "2026.3" -``` - -**A selector here must not name a RESOLVED layer.** `c-abi`, `c++-abi`, -`compiler`, `compiler-runtime` and `kernel-abi` are answered by dependency -resolution, which happens after tools are installed and after build programs -run. A tool conditioned on one would be declared and never installed — a build -that succeeds with the tool simply absent — so such a manifest is refused, -naming both the tool and the predicate. Condition it on the target, or gate it -on a feature: `[feature-xlings.]` is known before anything is -provisioned. - -**`accelerator` is the exception, and is admitted** (mcpp 2026.9.6.5). It is -not resolved from anything: it is `--accel`, or `[build] accel`, read before the -first package is looked up. A payload predicated on it is merged in the same -pass as a triple predicate and installed like any other. - -```toml -[target.'cfg(accelerator = "cuda")'.xlings.workspace] -"xim:cuda-nvcc" = "12.9.86" -"xim:cuda-cudart" = "12.9.79" -``` - -This is the form a project with a device island should use. Without it the -vendor toolkit is declared unconditionally or not at all, so `mcpp build` with -no accelerator — the cheapest build, and the one CI usually runs — downloaded -gigabytes for a device it was not compiling for. - -The same rule governs dependencies: `[target.'cfg(accelerator = "cuda")'.dependencies]` -is honoured, while a dependency conditioned on a resolved layer is not, because -that one would decide the answer it is asking for. Nothing about the -accelerator is circular. - -The selector is the only place the condition is written. A value under a -selector that also carries platform keys states one fact twice, and is refused -naming both halves: - -``` -[target.cfg(os = "linux").xlings.workspace] xim:tool: the value carries platform -keys (linux, macosx), but [target.cfg(os = "linux")] already says which targets -this applies to. -``` - -`subos` is not conditional on a target: a project has one environment, so -`[target..xlings]` refuses the key rather than dropping it. - -**A published descriptor carries no edge for a target-axis entry**, and -`mcpp publish` says so. A descriptor has one block per platform, and a selector -is not a platform — `cfg(target_arch = "aarch64")` names no block that file has. -What a CONSUMER of the package gets installed comes from the top-level -`[xlings.workspace]`; the target axis stays correct for what the package's own -build compiles against. - -See [SPEC-004](specs/manifest-semantics.md) for the general rule these two axes -are an instance of. - -#### `when` — which verbs need this tool (mcpp 2026.9.4.2+) - -```toml -[xlings.workspace] -"xim:qemu-arm" = "9.2.4-1" # every build, as before -"xim:codegen" = { version = "1.0", when = "build" } -"xim:probe-rs" = { version = "0.24.0", when = "run" } -"xim:clang-tidy"= { version = "20", when = "dev" } -``` - -Package dependencies have had this axis since the beginning — -`[dependencies]`, `[build-dependencies]`, `[dev-dependencies]`. Tools had one -list, so a board-support package that named both an emulator and a debug probe -installed both for every consumer, including one that only wanted the library -to compile. - -| `when` | Installed by | Reaches a consumer | -|---|---|---| -| *(omitted)* | every verb that builds | yes | -| `build` | every verb that builds | yes | -| `run` | `mcpp run`, `mcpp test` | yes | -| `dev` | only the package that declared it, as the root | **no** | - -**Omitting `when` is the pre-2026.9.4.2 behaviour exactly**, so no manifest has -to change. Narrowing is optional; it is not a question an author has to answer. - -`dev` is the only tier that does not propagate. It means *"while the package -that declared this is itself being developed"*, so a dependency's `dev` entry is -never installed for a consumer. Every other tier does reach one, which is the -point of a board package knowing its own machine: it declares the emulator once -and every consumer gets it. - -The tier is written on the entry rather than as a second table, on the same -reasoning that makes `[dependencies]` accept both `dep = "1.0"` and -`dep = { version = "1.0", features = [...] }`. A scoped entry must name -`version` even to leave it empty (`version = ""` means *present, any version*), -because `{ when = "run" }` and a misspelt `version` key would otherwise be -indistinguishable. - -#### `[feature-xlings.]` — a tool a feature needs - -```toml -[features] -default = ["emulator"] -emulator = {} -hardware = {} - -[feature-xlings.hardware] -"xim:probe-rs" = "0.24.0" -``` - -The same table, gated on a feature, spelled the way `[feature-deps.]` -is. A consumer who never asks for `hardware` never downloads a probe driver. -Entries here accept `when` exactly as the unconditional ones do. - -A feature name no `[features]` table declares is reported as a schema warning: -it activates for nobody and installs nothing, and a tool whose absence is only -visible as *"the device is never reachable"* is the hardest kind to diagnose. - -#### A rule package brings its own environment (2026.9.6.6+) - -The table above is what a project writes when it has an opinion. Most projects -have none, and nothing is what they write: - -```toml -[build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda"], host-module = true } -``` - -That one edge is the whole declaration. The rule package names the packages its -rule needs and the version it needs them from, under the feature that selects -it and the accelerator it is for: - -```toml -# in the rule package, not in your project -[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] -"xim:cuda-nvcc" = ">=12.9.86" -"xim:cuda-cudart" = ">=12.9.79" -``` - -Two gates, and both must open. The feature says *whether this rule is wanted*; -the selector says *which builds actually download it*. A CPU-only build of the -same project opens neither and installs nothing. - -Which package, and how old it may be, is the rule author's knowledge. Repeating -it in every project that uses the rule is a copy that goes stale silently — the -rule moves and the projects do not. - -#### One package, one version (2026.9.6.6+) - -A tool address is `[:][@]`, and its **identity is the -`(namespace, name)` pair**. The version is a constraint on that package, never -part of its name, so `xim:glibc`, `xim:glibc@2.40` and `xim:glibc@>=2.38` all -name one package. One version of it is installed per build. - -Which one is decided in two steps. - -**Adjudication — the declaration nearer the artifact wins.** Your project -outranks a package it depends on, so a pin overrides a rule's requirement: - -```toml -# the project, when it does have an opinion -[target.'cfg(accelerator = "cuda")'.xlings.workspace] -"xim:cuda-nvcc" = "13.3.33" -``` - -A declaration that names no version abstains: it says the package is wanted and -nothing about which version, so it cannot outrank a floor merely by being -nearer. When two declarations disagree and both name a version, mcpp reports -which was used — an override visible only as *"two versions were declared and -one directory exists"* is a fact the reader has to reconstruct from the -filesystem. - -**Validation — the winner must satisfy every requirement that lost.** `>=`, -`^`, `~` and comma-combined forms are requirements. A pin that fails one is -refused, naming both sides: - -``` -error: `xim:cuda-nvcc` is pinned to 12.0.0 by this project, and mcpp:plugins - requires >=12.9.86. - One version of a package is installed, so the two cannot both hold. - fix: pin a version satisfying >=12.9.86, or drop the pin and let the - requirement decide. -``` - -A bare version is a *choice*, not a requirement: two exact pins that differ are -adjudicated and reported, not refused. Only a stated requirement can be -violated. - -**This is a comparison, not a search.** The version is chosen by adjudication -and then checked, so mcpp never has to ask the index which versions exist and -carries no constraint solver. The cost is stated rather than hidden: a -combination a solver could satisfy — a project's `>=8.0`, a rule's `8.5.0`, and -8.3 as the newest in the index — is refused instead, and the refusal says how to -proceed. - -Ranges are resolved in both directions. `>=2026.1` installs the highest -published version satisfying it, `>=2099.1` is refused as unsatisfiable, and -`mcpp::xpkg_dir` answers with the highest **installed** version satisfying the -range — a rule that declares a floor can find what the floor brought in. - -#### Which version a tool the project did not name resolves to - -| The project declares | The version comes from | -|---|---| -| `[xlings.workspace]`, no `subos` | the machine's environment, with the project's own entries laid over it | -| `[xlings.workspace]` and `subos = ""` | that environment's own workspace; the machine's does not apply | -| neither | the machine's environment | - -The middle row is not an omission. A named environment has its own installed -set, and carrying the machine's versions into it would name versions that are -not there. Naming one is how a project asks for isolation; leaving it out is -how it asks for the machine's environment with its own entries on top. - -An `xlings use` performed inside the project outranks this table until mcpp -rewrites the environment, because it is the layer merged last. - -#### `deps`, superseded - -`deps = ["xim:qemu-riscv@9.2.4-1"]` is the pre-2026.9.3 spelling of the same -statement. It is still honoured and is reported once, with the -`[xlings.workspace]` line to write instead. It is not refused, because a -refusal would reach a *dependency's* manifest, which a project that pinned an -exact version of that package cannot edit. - -#### `envs`, removed - -`[xlings.envs]` was materialized into `.xlings.json` and read by nothing: a -program's environment is declared by its own package, and an environment's by -that environment. The key is now an error naming both. Nothing in the index -used it. - -### 2.14 Host tools from a dependency (mcpp 2026.8.5.1+) - -A package can build a binary its consumers need *at build time* — `protoc`, a -`grpc_cpp_plugin`, `flatc`, `moc`, a transpiler. Ask for it on the dependency: - -```toml -[dependencies] -protobuf = { version = "35.1", tools = ["protoc"] } -grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] } -``` - -Each name must be a `kind = "bin"` target of that package. mcpp builds it **for -the build machine** and hands `build.mcpp` its absolute path as -`MCPP_DEP__BIN_` — read it with `mcpp::dep_bin("protobuf", "protoc")` -(see [07 — build.mcpp](07-build-mcpp.md)). - -Four properties worth knowing: - -- **Always a host binary.** Under `mcpp build --target ` the tool is - still built for *this* machine, because a code generator has to run here. It - is a separate, host-targeted sub-build — the tool package's own `[toolchain]` - and its own dependency resolution apply, and none of it has to agree with - the consuming build. That is safe precisely because an executable has no ABI - contact with the consuming code. -- **One version axis.** The tool's version *is* the dependency's version, so - a `protoc` that does not match its runtime is not expressible. (This is the - problem with packaging the tool separately, and it is the failure mode that - bites at run time rather than compile time.) -- **Default off.** Nothing is built unless someone asks; the cost is the - consumer's to pay. A package gates the expensive part with - `[features]` + `required_features` (protobuf's `protoc` needs libprotoc's - ~157 extra TUs, which the runtime's users must not compile). -- **Cached globally**, keyed on package version × host toolchain × features × - its own dependency closure — built once per machine, not once per project. - -#### `[tools.overrides]` — use an existing binary - -```toml -[tools.overrides] -"compat.protobuf:protoc" = "/usr/bin/protoc" -``` - -or, without editing the manifest (CI, distro packaging): - -```bash -MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build -``` - -An override **skips the build entirely**. Every comparable system provides this -escape hatch (vcpkg's `VCPKG_HOST_TRIPLET`, CMake's `LLVM_NATIVE_TOOL_DIR`, -Qt's `QT_HOST_PATH`), and for the same reason: a tool that cannot be built from -source on this machine must not be a dead end. It is deliberately **not** part -of the cache key — an override is an escape hatch, not a reproducible input. - -#### `host-module = true` — reusable build rules as packages - -A rule (say "run protoc over these `.proto` files") should be written once, not -copy-pasted into every consumer's `build.mcpp`. Ship it as an ordinary mcpp -library package and import it: - -```toml -[dependencies] -protobufgen = { version = "0.1.0", host-module = true } -``` - -```cpp -// build.mcpp -import mcpp; -import protobufgen; -int main() { return protobufgen::generate({"schema"}) ? 0 : 1; } -``` - -mcpp compiles that package's lib-root module **for the host, in the same -command as `build.mcpp`** — which is what makes the BMI usable at all, since a -module interface is only importable by a compile that agrees with it on -standard, dialect and compiler identity. - -Rules are therefore versioned, testable and distributable through the package -manager already in use, written in **C++** — no second language, which is the -whole point of `build.mcpp` existing. - -**The module name is what the rule's source declares** (mcpp 2026.8.29.1+). -`export module acme.rules.protobuf;` is imported as `acme.rules.protobuf`, -whatever the package is called. Module names are authored API and do not mirror -package identity — the rule ordinary library packages have always followed. - -Until 2026.8.29.1 the host-module path registered the bare `package.name` -instead, which made a divergent name build under GCC and fail under Clang and -MSVC: GCC's BMIs are implicit under `gcm.cache` and keyed by the declared name, -while the other two are handed an explicit `=` mapping. Package names -carry no C++ naming constraint as a result, and `grpc-rules` is a legal package -name again. - -**Two rules may not declare one module name.** `import` addresses the module, -so two such packages are indistinguishable to the compiler, and their BMIs and -objects share a filename — the second overwrites the first and the surviving -object reaches the link twice. mcpp refuses this, naming both packages and both -interface paths. The check covers the rules one `build.mcpp` can see; it is not -an index-wide uniqueness rule, which `path` dependencies and private registries -would escape anyway. - -**`mcpp.` is reserved for rules maintained by the mcpp project.** A module name -under that prefix from a package outside the `mcpp` namespace produces a -warning naming both, and the build proceeds. It is a warning because the engine -cannot decide who is official: a `path` dependency, a private mirror and an -internal fork are all legitimate and indistinguishable from here. - -The lib root must be at `src/.cppm` (or wherever `[lib] path` points); a -missing one is reported as *"host module 'x': no interface unit at …"*. - -**A package may offer several rules, selected by features** (mcpp 2026.9.5.3+). -Every module interface unit among the package's resolved `[build] sources` — -including the sources a feature adds — is compiled as a host module under the -name it declares, the lib root first. A feature unit may import the lib root; -units are otherwise compiled alone, so they import `std`, `mcpp` and nothing -else. Only listed sources take part: the inferred `src/**` of a package that -declares no `sources` is not consulted, so a rule package published before this -release exposes exactly what it exposed then. - -```toml -# the collection's manifest -[build] -sources = ["src/plugins.cppm"] # export module mcpp.plugins; - -[features] -rules-cuda = { sources = ["rules/cuda.cppm"] } # export module mcpp.rules.cuda; -rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spirv; -``` - -```toml -# a consumer -[build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } -``` - -**`[build-dependencies]`, not `[dependencies]`** — a rule package is the case -§2.6.1 describes exactly: its library must never reach the target while its -rule is still wanted. The two axes are separate, so `host-module = true` says -*which build-time product* is wanted and the section says *whether the package -reaches the target*; a rule package answers "no" on the second axis, and the -section is where that is said. Written in `[dependencies]` it still works, and -that is precisely why the distinction has to be stated rather than enforced by -a failure. - -The module set is the feature set: a unit whose feature is not active is not -compiled, and importing it fails as an unknown module. `mcpp:plugins` is the -collection the mcpp project maintains (repository `mcpp-community/mcpp-plugins`); -its members are named `mcpp.rules.` for rule packages and `mcpp.tools.` -for build-time utilities. - -*Build-time only:* a `host-module = true` dependency is **not** compiled into -or linked with the target, and neither is anything it depends on. It exists to -run during `build.mcpp` and nowhere else. (Before 2026.8.5.2 it was also built -as an ordinary library, which made `import mcpp;` inside a rule fail: the -bundled module does not exist in that second compile. Until 2026.8.29.1 the -rule itself was excluded but its own `[dependencies]` were not, so they were -compiled and linked into the consumer's binary while the rule could not import -them.) - -#### A rule that depends on another rule (mcpp 2026.8.29.1+) - -A rule declares what it needs in its own `[build-dependencies]`, and may import -any entry there marked `host-module = true`: - -```toml -# inside the rule package's manifest -[build-dependencies] -globbing = { path = "../globbing", host-module = true } -``` - -```cpp -// the rule's own interface -export module tidyrule; -import std; -import mcpp; -import globbing; -``` - -mcpp compiles the inner rule first, in the same command and with the same -flags, so BMI agreement stays structural rather than checked. - -The consumer may **not** import `globbing`: build-time provisions cross one -further edge only on a `reexport = true` edge, and mcpp enforces that rather -than leaving it to the compiler, which on GCC would allow the import and then -fail on someone else's machine. - -*Limit:* one interface unit per host module. A library with implementation -units or several modules cannot yet be a rule's build dependency. - -#### `reexport = true` — a library standing up a toolchain for its user (2026.8.6.2+) - -Everything above is declared by whoever *uses* the tool. That is the wrong -place when the knowledge belongs to a library: gRPC's code generation needs -protobuf's `protoc`, and no user of a gRPC package should have to know that. - -`reexport = true` hands an edge's build-time provisions — its `tools`, its -`host-module`, and the dependency's directory — to **this package's own -consumers**: - -```toml -# inside the grpc package's manifest -[feature-deps.codegen] -"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } -grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"], reexport = true } -grpcgen = { version = "1.83.0", host-module = true, reexport = true } -``` - -Its user then writes one line, and imports the rule: - -```toml -[dependencies] -grpc = { version = "1.83.0", features = ["codegen"] } -``` - -```cpp -// build.mcpp -import mcpp; -import grpcgen; -int main() { return grpcgen::generate_all() ? 0 : 1; } -``` - -- **Off by default, and deliberately not the edge's `visibility`.** `visibility` - already defaults to `"public"`, so riding it would let any dependency at any - depth put entries into the build program's tool namespace without saying so. - Handing something to consumers is a supply-chain statement; it has to be - written down. -- **One hop per declaration.** A re-exported provision reaches the consumers of - the package that declared it. For it to travel further, the next package must - re-export in turn — each package decides only what *it* hands on. -- **A feature may add a request to an already-declared dependency.** gRPC - depends on protobuf unconditionally and its `codegen` feature adds - `tools = ["protoc"], reexport = true` to that same edge. `tools` and - `features` union, `host-module` and `reexport` OR together; `version` / - `path` / `git` do not merge, so a feature still cannot silently override the - unconditional entry's identity. -- **Visibility, not execution.** `dep_bin()` returns a path; whether anything - runs is still the consumer's `build.mcpp`'s decision. Nothing changes about - who builds the tool or how the tool store is keyed. -- **Unqualified names are resolved by a ladder, not by luck.** Once two - libraries can re-export, both may offer the tail `protobuf`. The - fully-qualified `MCPP_DEP___BIN_` is always published; the - bare spelling is bound to `mcpplibs.`, else `compat.`, else an - unnamespaced ``, else the single remaining candidate — and when it is - contested mcpp says so instead of picking silently. - -##### Older mcpp reading a manifest that uses this - -An unrecognized dependency key is reported as a **degradation** and ignored -(mcpp 2026.8.6.2+), so a package written for a newer mcpp still loads and the -parts this reader understands still apply. Before that release it was a hard -load failure with a misleading message, which is why a published package could -not adopt a new key at all — the same property the index floor establishes: -data must not decide whether the program works. - -Consequently a package that *relies* on `reexport` for its ergonomics still -needs a client new enough to implement it; what changed is that everything else -about that package keeps working on an older one. - -##### Scoping a provision per platform - -A package may declare a `bin` target on some platforms only. Because the -*library* now decides what is requested, an unconditional request turns an -unsupported platform into an error its user cannot edit away. Scope it: - -```toml -[target.'cfg(not(windows))'.feature-deps.codegen] -"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } -``` - -`[target..feature-deps.]` (2026.8.6.2+) follows the same rules as -the other conditional dependency tables (§2.7.1). The **feature itself is -registered on every platform** — only what it pulls in is conditional — so -requesting it where no predicate matches is not an unknown-feature error. - -### 2.15 `[resources]` — Metadata and Assets Embedded in the Artifact (2026.8.7.1+) - -An exe icon and the version metadata Windows shows in a file's Properties dialog -are a path in `mcpp.toml`, nothing more: - -```toml -[resources] -icon = "assets/app.ico" -``` - -That is the whole common case. `FILEVERSION`, `ProductName`, `FileDescription`, -`CompanyName` and `LegalCopyright` all default from `[package]`, and mcpp -generates the resource script automatically. - -| Key | Type | Meaning | -|---|---|---| -| `icon` | path | Embedded as the application icon (resource ordinal 1) | -| `files` | list of paths | Your own `.rc` scripts, compiled and **tracked** as build inputs | -| `extra-inputs` | list of paths | Inputs the `.rc` scanner could not see (see below) | -| `version-info` | bool | `false` opts out of the generated version resource | -| `[resources.version-info]` | table | `company`, `product`, `description`, `copyright`, `original-filename`, `internal-name` | - -**Only PE targets *compile* this.** On Linux and macOS the section is -*inapplicable*: no resource units, no diagnostics, byte-identical build. You do -**not** need (and cannot use) a `cfg(windows)` predicate — write it once, -unconditionally. - -**A declared file that does not exist fails the build — on every target.** A -resource is a build input like a source file; mcpp will not quietly ship a -binary without it. Validation is deliberately *not* PE-gated: whether a path -exists is a fact about the working tree, not about the target, so a typo in -`icon = "assets/app.ico"` is caught by the Linux or macOS build (and by their -CI jobs) instead of waiting for the Windows one. To omit the icon, -delete the line. - -**Version fields.** `FILEVERSION` takes the four numeric segments of -`[package].version`, each of which must fit in 16 bits; the string fields keep -the version verbatim, so a form the numeric fields cannot hold (`1.0.0-rc1`) -still shows up in the Properties dialog. - -#### Supplying a hand-written `.rc` - -```toml -[resources] -files = ["res/app.rc"] -``` - -With `files` set, mcpp stops generating a version resource, and the -resource ID space belongs to the project. Set `version-info = true` alongside it for both (and -mind the collision: there can be only one `RT_VERSION` at ordinal 1). - -To start from the generated script instead of a blank file, copy it out of the -build directory (`target///res/.mcpp.rc`) and list it in -`files`. The result is byte-identical, so moving from generated to hand-written -never changes what ships. - -> **`VS_VERSION_INFO` needs ``.** In a hand-written script, -> `VS_VERSION_INFO VERSIONINFO` without `#include ` files the version -> resource under a *string* name instead of ordinal 1. Every tool still reports -> `Type: VERSIONINFO`, but `GetFileVersionInfo` looks up the ordinal, so -> PowerShell's `FileVersionInfo` shows every field as empty. Either include -> `` or write `1 VERSIONINFO`. mcpp warns when it sees this shape; -> the script it generates uses the literal `1`. - -#### Tracked inputs - -mcpp reads the `.rc` for quoted `#include`s and for the files named by resource -statements (`ICON`, `RCDATA`, `MANIFEST`, …), and makes them build inputs, so -editing the icon relinks. Angled includes (``) are the toolchain's -and are covered by the toolchain fingerprint instead. - -A file name reached through a macro (`1 ICON APP_ICON`) is invisible to that -scan. mcpp names what it could not resolve and requires an explicit declaration: - -```toml -extra-inputs = ["assets/app.ico"] -``` - -#### Anything else: `role = "object"` - -For inputs that are not resource scripts — a blob embedded with `objcopy`, a -generated `.def`, a pre-built object — a build program can declare a build-graph -node whose outputs join the link: - -```cpp -mcpp::action o; -o.id = "blob"; o.role = "object"; -o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o") - .input("blob.bin") - .output("${mcpp.out_dir}/blob.o") - .target("myapp") // omit: every image, test binaries included - .submit(); -``` - -See [07 — build.mcpp](07-build-mcpp.md). Naming such a file in -`[build].ldflags` also "works", but ldflags is a flat string in the link -command: nothing tracks it, and editing the file produces `ninja: no work to -do`. - -### 2.16 `[hooks]` — Project Build Lifecycle Commands (experimental) - -> **Experimental.** A hook cannot currently decide whether a build succeeded. -> Every hook failure is reported as a **warning** and `mcpp build` keeps the -> result it earned on its own; `side_effect = true` is refused with an error -> rather than honoured. The key stays in the schema so that manifests written -> today do not have to change when the feature is promoted. Two further limits -> are permanent rather than provisional: only the root project's hooks run, and -> only `mcpp build` runs them. - -A hook is a command `mcpp build` **owns for an interval**, and the event names -the interval: - -```toml -[hooks] -build_start = "echo build started" -build_failed = "notify-send 'build failed'" -build_finished = "notify-send 'build finished'" - -# Optional; these are the defaults. -timeout_seconds = 10 -enabled = true -side_effect = false # `true` is refused while this is experimental -``` - -| Key | Type | Default | The interval it names | -|---|---|---:|---| -| `build_start` | command | — | Opens after project preparation, closes when the command exits | -| `build_finished` | command | — | Opens after a build that succeeded, closes when the command exits | -| `build_failed` | command | — | Opens after a build that failed, closes when the command exits | -| `during_build` | command | — | Opens before the build, closes after it | -| `timeout_seconds` | integer, 1–86400 | `10` | Bounds one run of a command | -| `enabled` | bool | `true` | Enables all commands in this table | -| `side_effect` | bool | `false` | Whether a hook failure makes the build fail. **Reserved** — only `false` is accepted while this is experimental | - -The first three intervals are **self-closing** — they end when the command -does. "Synchronous" is not a separate mode here; it is what a self-closing -interval looks like. `during_build` is the one interval closed by something -else, and the two keys that only make sense for one shape follow from that -rather than being exceptions. - -A command is a string, or a table when it needs options: - -| Table key | Applies to | Meaning | -|---|---|---| -| `cmd` | every event | The command. Required. | -| `timeout_seconds` | self-closing events | Overrides the table default for this event | -| `loop` | `during_build` | Restart the command if it exits before the build ends | - -`loop` on a self-closing event and `timeout_seconds` on `during_build` are both -**errors**, not ignored keys: a self-closing interval ends when its command -exits, so there is nothing to restart, and `during_build` is already bounded by -the build. A key that is accepted and does nothing reads as a broken feature. - -Commands run through the host shell (`/bin/sh` or `cmd.exe`), with the -**project root** as their working directory — not the directory `mcpp build` -was typed in, so a relative path in a hook means the same thing wherever the -build was started. A self-closing command keeps ordinary terminal -input/output. Missing event commands are skipped. - -The lifecycle is: - -```text -during_build opens -build_start - ├─ build succeeds → during_build closes → build_finished - └─ build fails → during_build closes → build_failed -``` - -`during_build` closes **before** the terminal hook, so the two commands never -overlap. - -`build_failed` and `build_finished` are mutually exclusive, and both are -reachable only after `build_start` has run. A project that cannot be *prepared* -— an invalid manifest, an unresolvable dependency, no usable toolchain — fires -nothing: it has not started building, and its hook program may be exactly what -preparation would have installed. - -A hook command that cannot start, returns non-zero, or exceeds its timeout is a -hook failure. For `during_build` there is one more: a looped command that -**fails to stay up** — five consecutive runs ending unsuccessfully within a -second — stops being restarted and is reported. (A command that finishes -quickly and *successfully* is doing exactly what `loop` was asked to repeat, -and is not a failure.) Every one of those is reported as a **warning**, and the -build keeps the result it earned on its own — while `[hooks]` is experimental -it does not get a vote. A hook's own failure does not trigger another hook. - -`side_effect = true` is what will change that, and asking for it today is an -error: - -```text -error: mcpp.toml: error: [hooks].side_effect = true is not available yet: -[hooks] is experimental and cannot decide whether a build succeeded. … -``` - -Refused rather than quietly downgraded, because both silent options are worse: -honouring it would give an experimental feature a veto over every build, and -ignoring it would leave a project believing its build is gated on a notifier -when nothing is. When the feature is promoted, `true` will mean "a hook failure -fails the build" — and a build that failed on its own will still keep its own -exit code, so `mcpp build` never reports a compile error as a notifier problem. - -Two things are worth knowing about a `during_build` command specifically: - -- **Its output is discarded**, because it writes concurrently with the build - and would otherwise land in the middle of a compiler diagnostic. Run - `mcpp build --verbose` to see it. -- **It is stopped as a process tree**, not as a process. `player & wait` makes - the player a grandchild of the command mcpp started, and stopping only the - latter would leave the audio device held after the build. mcpp puts the - command in its own process group (a job object on Windows) and stops that, - including when the build is interrupted with Ctrl-C. - -Scope, precisely: - -- Only `mcpp build` runs hooks. `mcpp run`, `mcpp test` and - `mcpp build --configure-only` build too, and deliberately do not. -- Hooks belong to the **package being built**. In a workspace fan-out that is - each member in turn — its own `[hooks]`, around its own build, in its own - root. A *virtual* workspace root (`[workspace]` with no `[package]`) builds - nothing, so a `[hooks]` table there never fires. -- A dependency's `[hooks]` is **skipped**, always. Only the root project's run. - Every manifest mcpp parses carries the section, a dependency's included, and - nothing reads it — which is what keeps `mcpp add` from meaning "run this - author's shell command on my next build". This is a property of the design, - not a default awaiting a switch. -- Declaring an active hook opts the project out of the no-op fast path, because - `build_start` is specified to run after preparation. Expect `mcpp build` on an - already-current hooked project to cost a preparation pass rather than - milliseconds. - -An unrecognised key in `[hooks]`, or inside one event's table, is a warning (an -error under `--strict`), so a manifest written for a newer mcpp still loads. An -unrecognised *value* — a missing or non-string `cmd`, a `timeout_seconds` -outside 1–86400, a key offered to the wrong interval — is a manifest error. - -> **A hook is code, and `mcpp.toml` is part of the repository.** Building a -> freshly cloned project runs whatever its `[hooks]` say, with the privileges -> of whoever invoked `mcpp build`. This is the same trust `build.mcpp` already -> asks for ([07 — build.mcpp](07-build-mcpp.md)); `[hooks]` widens its reach -> rather than introducing it. - -Hook programs can be installed as ordinary xlings dependencies. For example, -an audio notifier can keep its sound files inside its own executable rather -than adding media handling to mcpp: - -```toml -[hooks] -build_finished = "mcpp-hooks-audioplayer niulai-mm" -build_failed = "mcpp-hooks-audioplayer niulai-niulai" -side_effect = false - -[xlings.workspace] -"xim:mcpp-hooks-audioplayer" = "0.0.1" -``` - -A different sound for a successful or failed build. `side_effect = false` is -written out rather than left to the default: it is the value this manifest -wants on its own terms — a missing audio device should never fail a build — so -it will still say so once the key has more than one accepted value. - -## Appendix A. Schema Ownership Principle (admission criteria for new fields) - -> **Closed syntax, open vocabulary**: whoever owns the parsing semantics defines the keys; whoever owns the domain knowledge defines the values. - -- mcpp only defines **mechanisms** (feature union/closure, capability - require/provide/override, profile→compiler flags, platform→triple); the keys and - shapes are fixed. Domain vocabulary such as feature names, capability names, and - backend names **appears only in values**, never in mcpp's code. -- **Package-custom toml keys are not supported**: key legitimacy must not depend on - "first parsing the target package," otherwise the manifest loses static - parseability (a prerequisite for lockfiles/LSP/auditing). A package's extension - point = open value domains within fixed mechanisms. -- Package-level knobs all converge into features; for sugar keys (such as `backend=`) - to enter the core syntax, they must satisfy: ① domain-neutral (a cross-ecosystem - general pattern) ② 1:1 desugaring with zero new parsing semantics. -- **A key that duplicates an answer another section already gives is not admitted.** - Two places to state one fact is two places that can disagree, and the failure - is silent — whichever reader loses the race is simply wrong. Library packaging - ([12](12-binary-distribution.md)) is the worked example: it added **zero** - manifest keys, because what to pack is `[targets.].kind`, which interface - to publish is `[lib]` plus the module graph, which headers are public is - `[build].include_dirs`, and the per-artifact evidence is `[[runtime.artifacts]]`. -- A field that describes what a *generated* package IS (rather than what a build - should DO) belongs on `[[runtime.artifacts]]` — see §2.11. `provenance` - beginning with `mcpp-pack` is what marks a directory as one, and mcpp refuses - to `build` inside it. -- See `.agents/docs/2026-06-04-manifest-schema-ownership.md` for the full field-ownership - table and the finalized decisions. - -## 3. Worked Examples - -### 3.1 Simple Hello World - -```toml -[package] -name = "hello" -version = "0.1.0" -``` - -```cpp -// src/main.cpp -import std; -int main() { std::println("Hello, mcpp!"); } -``` - -```bash -mcpp build && mcpp run -``` - -### 3.2 Module-based Library + Tests - -```toml -[package] -name = "mymath" -version = "1.0.0" - -[targets.mymath] -kind = "lib" - -[dev-dependencies.compat] -gtest = "1.15.2" -``` - -```cpp -// src/mymath.cppm -export module mymath; -export int add(int a, int b) { return a + b; } -``` - -```cpp -// tests/test_add.cpp -#include -import mymath; -TEST(Math, Add) { EXPECT_EQ(add(1, 2), 3); } -``` - -```bash -mcpp build # Compile the library -mcpp test # Compile + run tests -``` - -### 3.3 An Application Depending on Other Packages - -```toml -[package] -name = "myapp" -version = "0.1.0" - -[dependencies] -ftxui = "6.1.9" - -[dependencies.mcpplibs] -cmdline = "0.0.2" -llmapi = "0.2.5" -``` - -mcpp automatically: -1. Downloads source tarballs from mcpp-index -2. Propagates header search paths per `[build].include_dirs` -3. Pulls transitive dependencies into the graph (llmapi → tinyhttps → mbedtls, fully automatic) - -### 3.4 Pure C Library - -```toml -[package] -name = "myc" -version = "0.1.0" - -[build] -c_standard = "c99" -include_dirs = ["include"] -sources = ["src/**/*.c"] - -[targets.myc] -kind = "lib" -``` - -### 3.5 Mixed C / C++23 Module Project - -```toml -[package] -name = "hybrid" -version = "0.1.0" - -[build] -include_dirs = ["include"] -c_standard = "c11" - -[dependencies] -lua = "5.4.7" # Pure C library; mcpp compiles .c files with the C compiler automatically - -[targets.hybrid] -kind = "bin" -``` - -### 3.6 Cross-Compiled Static Release - -```toml -[package] -name = "mytool" -version = "1.0.0" - -[toolchain] -default = "gcc@16.1.0" - -[target.x86_64-linux-musl] -toolchain = "gcc@16.1.0" -linkage = "static" -``` - -```bash -mcpp build --target x86_64-linux-musl -# → Produces a fully statically linked binary that can be scp'd directly to any Linux x86_64 machine and run -``` - -## 4. Conventions and Defaults Cheat Sheet - -| Item | Default | Notes | -|---|---|---| -| Source files | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | Scanned recursively and automatically | -| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred | -| Library root | `src/.cppm` | Override with `[lib].path` | -| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly` (experimental playground) | -| C standard | `c11` | `.c` files go through the C compiler automatically | -| Static stdlib | `true` | Portable binary | -| Headers | `include/` (if present) | Added to `-I` automatically | -| Tests | `tests/**/*.cpp` | Discovered automatically by `mcpp test` | -| Dependency namespace | `mcpplibs` (default) | A bare selector means only this exact namespace | - -### 4.1 Legacy `[language]` Compatibility Layer - -The old configuration is still readable: - -```toml -[language] -standard = "c++26" -``` - -New projects should use `[package].standard`. If both locations are present, `[package].standard` is authoritative. diff --git a/docs/06-features-and-capabilities.md b/docs/06-features-and-capabilities.md new file mode 100644 index 00000000..66485e46 --- /dev/null +++ b/docs/06-features-and-capabilities.md @@ -0,0 +1,430 @@ +# 06 — Features and Capabilities + +**Reader:** an author with something optional — an extra source, an extra +dependency, or a choice between backends. + +**The question this chapter answers:** how does a package offer something +optional, and how does a consumer ask for it. + +**Not here:** which device backends a build targets, which looks like a feature +and is not — that is [42 — Heterogeneous Builds](42-heterogeneous-builds.md). +Before: [05 — Dependencies and Resolution](05-dependencies.md). After: +[07 — Workspaces](07-workspace.md). + +Features are how a package offers something optional: a compile macro, an extra +source, an extra dependency, or a choice between backends. This chapter is the +reference for declaring them and for consuming them. + +Related documents: [04 — mcpp.toml](04-mcpp-toml.md) is the field reference for +the rest of the manifest; [`examples/11-features`](../examples/11-features/) is +a package that declares all three shapes and a test that uses a +dev-dependency; [42 — Heterogeneous Builds](42-heterogeneous-builds.md) is the +largest consumer of the mechanism, because every accelerator lane is a feature. + +## `[features]` — Features (Cargo-style, additive) + +```toml +[features] +default = ["base"] # Default activation set +base = [] +docking = ["extra"] # Activating docking implies activating extra (transitive closure) +extra = [] +``` + +- Activation sources: the package's own `default` set ∪ explicit requests (the root + package via `mcpp build --features a,b`; dependencies via the long-form dep spec's + `features = [...]` / `backend = "..."` sugar). +- Each activated feature gets the macro `-DMCPP_FEATURE_` during that package's + compilation (the name is uppercased and non-alphanumerics become `_`, e.g. + `backend-a` → `MCPP_FEATURE_BACKEND_A`). +- **strict validation**: when the target package declares a `[features]` table, + requesting an undeclared feature produces a warning; an error under `--strict`. A + package that does not declare `[features]` accepts any request (pure macro usage). + +### Table form — a feature that contributes more than implied features + +A `[features]` entry may be written as a **table** instead of an array, letting the +feature carry package-owned preprocessor `defines`, feature-gated source globs +(`sources`, mcpp 0.0.95+ — the globs leave the default build and compile only when +the feature is active, exactly like an index descriptor's `features..sources`; +the highest-frequency shape for vendored libraries: *feature = a source set + a +define*), feature-gated per-glob compile flags (`flags`, mcpp 0.0.101+), and/or +capability `requires` / `provides` (see *`provides` / `requires`* below) alongside its implied features: + +```toml +[features] +default = [] +# Array shorthand: just implied features. +docking = ["extra"] +extra = [] +# Table form: contribute a package-owned define when active. +mpl2only = { defines = ["EIGEN_MPL2_ONLY"] } +# Table form: a define + an implied feature. +fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] } +# Table form: feature-gated sources + per-glob flags that co-locate with them. +simd = { sources = ["src/simd/**"], flags = [ + { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] } +``` + +- **The table form accepts exactly** `implies`, `forward`, `defines`, `sources`, + `flags`, `requires`, `provides`. Anything else is reported as a schema warning + and ignored (mcpp 2026.9.1.1+); `deps` is reported separately as reserved and + points at `[feature-deps.]`. Before that release `[features]` was + the one structured section with no schema check at all, so a misplaced + `include_dirs` inside a feature built successfully with no diagnostic while + the identical mistake in `[build]` was reported. +- `defines` are **bare** macro names (no `-D`); each desugars to `-D` on the + package's own compile when the feature is active — exactly like `[targets.*] + defines`. They are restricted by convention to the package's **own** namespaced + macros: a feature does **not** inject free-form package-wide `cflags`/`ldflags`, + which would break the additive feature-union model. Link flags come from a + provider dependency (see *`provides` / `requires`* below), not from a feature. +- The automatic `-DMCPP_FEATURE_` is still defined for every active feature, + so `defines` are additive to it. +- `flags` (mcpp 0.0.101+) is the same ordered array-of-inline-tables grammar as + `[build].flags` ([04 §2.3](04-mcpp-toml.md): `glob` required, plus `cflags`/`cxxflags`/`asmflags`/ + `defines`; the `[[features..flags]]` array-of-tables spelling is accepted + too, like `[[build.flags]]`). When the feature is active the entries + are appended **after** the base `[build].flags`, features in name order, so a + feature rule wins over a broader base rule via "last flag wins"; when it is + inactive the entries do not exist (no dead-glob warning). This is how a + feature's group-specific flags co-locate with its `sources` instead of living + as base rules whose globs go dead on feature-off builds. Unlike `defines`, + feature `flags` are **private per-TU build flags** — they never propagate to + consumers (same contract as `[build].flags`), so they stay inside the additive + model: scoped by glob, deterministic order, no cross-package effect. + + +### A feature that is a build rule (mcpp 2026.9.7.1+) + +Two keys turn a feature into a build rule other packages can use. They are what +lets a consumer write one dependency edge and no build program. + +```toml +[features.rules-spirv] +sources = ["rules/spirv.cppm"] +rule_module = "mcpp.rules.spirv" +device_extensions = [".comp", ".vert", ".frag", ".glsl"] +``` + +`device_extensions` states which **device source** extensions this rule +compiles. A consumer that activates the feature gets them classified as device +sources -- never scanned for imports, never a BMI, compiled by something mcpp +does not drive. This is the same shape as `[build] module_extensions`: mcpp +knows what a device source *is* and does not know that `.cu` is CUDA, so a NEW +device language costs no engine change. It is what makes +[42 — Heterogeneous Builds](42-heterogeneous-builds.md)' claim that "a sixth +backend is a package rather than an engine change" true rather than +aspirational; `.slang` was removed from mcpp's built-in table and now arrives +this way. + +`rule_module` names the module a consumer's build program imports to reach the +rule, and whose `compile()` it calls. Declared rather than scanned out of the +source, because the program has to be **written** before anything is compiled +and a build that scanned a dependency to decide what to write would order the +two the wrong way round. + +Two things follow, and neither puts a package name inside mcpp: + +- **`host-module = true` is implied.** A feature naming a rule module has + already said that is the only way to use it, so the edge does not repeat it. +- **A package with no `build.mcpp` gets one.** mcpp writes the program those + rules describe into the build directory and compiles that. A package with its + own program keeps it: the synthesis fills an absence and never overrides, and + the generated file is the program a project would have written, so taking it + over is a copy and an edit. + +The feature is still requested **by name**: + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.3.0", features = ["rules-spirv"] } +``` + +An earlier design derived the set from the extensions a project's sources +carried. It was withdrawn because two packages may claim one extension -- a +third-party CUDA rule is a thing someone will write -- and because a manifest's +job is to describe the build, which a derived feature set no longer does. + +Both keys must appear together. One without the other is a declaration nothing +can act on, and it is refused at parse time rather than in a consumer's build. + + +## `provides` / `requires` — Capabilities (backend selection) + +A **capability** is a shared abstract name (e.g. `blas`). A package can *provide* +one; a feature can *require* one instead of naming a concrete package, and the +resolver binds exactly one provider from the dependency graph. This is how a build selects +one of several interchangeable backends (OpenBLAS / MKL / …) without baking a choice +into the library. + +```toml +# A provider package satisfies a capability for any dependent that requires it. +[package] +name = "compat.openblas" +version = "0.3.0" +provides = ["blas", "lapack"] +``` + +```toml +# A consumer requires the abstract capability via one of its features. +[features] +use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } + +# When >1 provider is in the graph, pick one (else the build errors and lists them). +[capabilities] +blas = "compat.openblas" # equivalently: mcpp build --cap blas=compat.openblas + +[dependencies] +compat.openblas = "0.3.0" # the provider must be a real dependency in the graph +``` + +The reserved prefix `mcpp:` names the target-side layers this engine resolves, +and those names are validated against a closed set. A package-level `requires` +array carries the symmetric statement — what a target-side layer must resolve to +for this package to be usable. + +```toml +[package] +name = "acme.llvm-runtime" +version = "0.1.0" +provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"] +requires = ["mcpp:compiler=llvm"] +``` + +A package that is a standard library states its `std` module source under +`[build]`, where the flags it needs become conditional like any other build +input. + +```toml +[build] +std-module = "llvm-generated/std.cppm" +std-compat-module = "llvm-generated/std.compat.cppm" +std-module-flags = ["--no-default-config", "-nostdinc++"] + +[target.'cfg(c-abi = "musl")'.build] +std-module-flags = ["-D_GNU_SOURCE"] +``` + +See [22 - The Target Side](22-target-side.md) for the five layers, the rules +that govern them, and the diagnostics. + +Binding is **deterministic**: + +| Providers of a required capability in the graph | Result | +|---|---| +| exactly one | bound automatically (no config needed) | +| a `[capabilities]` pin / `--cap` names one | the pin wins | +| zero | **error**: no package provides `` | +| two or more, unpinned | **error**, listing the candidates — never a silent guess | + +The bound provider's link/include flags reach the consumer through normal +dependency mechanics; the capability layer is the *selection-and-validation* step +that turns a silently-wrong or missing backend into a loud configure-time error. + +**Binding selects a provider; it does not prune the link line.** A dependency +package contributes its object files to the consumer's link regardless of whether +its capability was the one bound. Measured with two packages that both provide +one capability and both define `cap_probe`: unpinned, resolution fails as the +table says; after pinning one with `[capabilities]`, the build reaches the linker +and fails there instead — + +``` +ld: obj/mcpplibs_pa/src/impl.o: in function `cap_probe': + multiple definition of `cap_probe'; obj/mcpplibs_pb/src/impl.o: first defined here +``` + +This matters for a capability whose providers define the **same symbols** — a +whole-program singleton such as `operator new`, or a C API with one fixed name +set. For those, two providers in the graph is a defect to fix rather than an +ambiguity to pin: pinning replaces an error that names both candidates with one +that names a mangled symbol. Interchangeable *libraries* (BLAS implementations, +which export distinct symbol sets and are selected per link) are unaffected. + +### `exclusive` — a package declaring it is the only provider + +The paragraph above describes a defect the engine cannot detect. Seeing that two +providers define the same symbols requires their object files, which do not +exist when capabilities are bound; and refusing every duplicate provider as a +rule would break the BLAS case in the same paragraph, which is legitimate. + +So the package says it: + +```toml +[package] +name = "compat.cublas" +provides = ["gpu-blas"] +exclusive = ["gpu-blas"] +``` + +Two packages that both provide `gpu-blas`, where at least one declares it +exclusive, are refused when capabilities are bound — before anything is +compiled, naming the capability and both providers: + +``` +error: capability 'gpu-blas' is provided by more than one package, and they + declare it EXCLUSIVE. + providers: [compat.cublas, compat.rocblas] + exclusive: [compat.cublas, compat.rocblas] + Two implementations of one interface define the same symbols, so the + link would resolve every call to whichever archive it reached first. + Keep one of them — a `[capabilities]` pin selects a provider for a + REQUIREMENT and cannot make two definitions of one symbol safe. +``` + +The refusal reports `exclusive-capability` in `--format json` (chapter 11). + +### `version-floor` — needing more of the machine than it has + +Some facts about a machine bound what may be built for it, and the failure when +they are ignored arrives late: a program built against a runtime newer than the +driver it will meet links cleanly and fails at first use, naming neither side. + +A package states what it needs: + +```toml +[[runtime.requirements]] +kind = "version-floor" +value = "cuda.driver >= 12.0" +``` + +and a package that established a fact about this machine — at install time, +which is where probing belongs — states it: + +```toml +[runtime] +provides = ["cuda.driver=12.4"] +``` + +mcpp compares them when capabilities are bound and refuses before anything is +compiled, reporting `version-floor-unmet`: + +``` +error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4. + stated by: driverfact +``` + +**No vendor vocabulary reaches the engine.** It reads a name, a relation and a +version; `cuda.driver` is data passing through, and a backend mcpp has never +heard of compares the same way. + +**A floor nobody answered is silent.** A machine that never declared what it +has is not a machine that fails the floor — it is one nobody asked. Turning +"we do not know" into "no" is the failure mode this exists to avoid, and it is +asserted directly: `tests/e2e/603_version_floor.sh` builds a project whose floor +names something no package provides. + +**It is a claim about this package's own symbols**, so an entry that names a +capability the package does not provide is reported as a schema warning: there +is nothing to be exclusive about. And a capability nobody declares exclusive +behaves exactly as before — two BLAS implementations still coexist, and the +existing "two or more, unpinned" error still applies only when something +*requires* the capability. + +## `[feature-deps.]` — dependencies a feature pulls in + +A dependency declared under `[feature-deps.]` is **optional**: it is +resolved only when that feature is active (root `--features`, or a dependency +spec's `features = [...]`). A dependency in `[dependencies]` is always resolved; +optionality is expressed by *where* the entry is declared, not by a flag. + +```toml +[features] +use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } +backend-openblas = { implies = ["use_blas"] } + +# Pulled ONLY when `backend-openblas` is active. Each entry is a full dependency +# spec (version/path/git + its own features). +[feature-deps.backend-openblas] +compat.openblas = "0.3" +``` + +**`"^0.3.0"` and not `"0.3.x"` or `"0.3"`.** Measured against a package the +index certainly carries, with a **build** as the criterion: + +| Written | Result | +|---|---| +| `cmdline = "0.0.1"` | builds | +| `cmdline = "^0.0.1"` | builds | +| `cmdline = "0.0"` | resolves, then `install path missing after fetch` | +| `cmdline = "0.0.x"` | `E_NOT_FOUND`, naming the package — which exists | + +The three outcomes are worth distinguishing, because two weaker criteria each +admit a form that does not work: "no `E_NOT_FOUND`" admits the two-segment +prefix, and "resolves" admits it as well. Only building against the real index +settles it. + +This matters more here than in `[dependencies]`. A feature whose +implementation cannot be fetched is a feature that does not exist, and a project +using a **path** dependency during development never consults the index — so the +failure appears only after publication, to somebody else. + +This composes with capabilities (*`provides` / `requires`* above): a single `backend-openblas` feature +both **pulls** the provider (`compat.openblas`, which `provides = ["blas"]`) and +**turns on** the consumer switch (`implies = ["use_blas"]`, which +`requires = ["blas"]`). With one provider in the graph the capability binds +automatically — `features = ["backend-openblas"]` is all the consumer writes. + +In an index package's Lua descriptor the same is written inline: + +```lua +features = { + use_blas = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } }, + ["backend-openblas"] = { + implies = { "use_blas" }, + deps = { ["compat.openblas"] = "0.3.x" }, + }, +} +``` + +### A default implementation that stays replaceable + +The same three pieces cover the case where a library wants to *offer* an +implementation without *imposing* one — a whole-program singleton such as +`operator new`, a logging sink, or a panic handler: + +```toml +[features] +default = [] +# The consumer-side switch: "I use the part of this library that needs an allocator". +alloc = { requires = ["freestanding-allocator"] } +# The built-in default: activating this one is enough. +alloc-kal = { implies = ["alloc"] } + +# Resolved only when `alloc-kal` is active, so the library itself carries no +# dependency on the implementation. +[feature-deps.alloc-kal] +std-freestanding-alloc-kal = "0.1.x" +``` + +Three usages, one line each: + +| Consumer needs | The manifest form | +|---|---| +| none of the allocating parts | `std-freestanding = "0.2.0"` — no allocator enters the graph | +| the default | `features = ["alloc-kal"]` — the implementation arrives with it, and its package name never has to be known | +| its own or a third party's | `features = ["alloc"]` plus a package that `provides = ["freestanding-allocator"]` | + +Two properties make this preferable to shipping the implementation +unconditionally. A library that ships one takes a decision belonging to the +program, and it cannot be undone: features are **additive**, so there is no way +for a consumer to switch a default *off*. And because a dependency package's +objects link unconditionally (*`provides` / `requires`* above), a shipped default plus a program-supplied +one is a duplicate definition rather than a replacement — the archive semantics +that let a C++ standard library offer a replaceable `operator new` do not apply +to a package dependency. Keeping the implementation behind a switch means the +two never coexist. + +## Current limitations + +**A default feature is turned off in the manifest, not on the command line.** +There is no `--no-default-features`. `mcpp build --features metrics` activates +`default ∪ {metrics}`; building without a member of `default` means editing +`[features] default`. Measured on 2026.9.8.1. + +**A dependency cannot be conditioned on the accelerator.** `accelerator` is +resolved from the dependency graph, so a dependency chosen by it would decide +the answer it is asking for. mcpp reports the predicate and ignores it. Packages +are unconditional or conditioned on the platform; `[build] sources` is what the +accelerator selects. + diff --git a/docs/06-workspace.md b/docs/07-workspace.md similarity index 96% rename from docs/06-workspace.md rename to docs/07-workspace.md index fd083625..6f5df7c9 100644 --- a/docs/06-workspace.md +++ b/docs/07-workspace.md @@ -1,4 +1,14 @@ -# Workspace +# 07 — Workspaces + +**Reader:** an author whose repository holds more than one package. + +**The question this chapter answers:** how do several packages become one build, +and what does a member share with the others. + +**Not here:** publishing those packages, which is +[11 — Publishing a Library](11-publishing-a-library.md). Before: +[06 — Features and Capabilities](06-features-and-capabilities.md). After: +[08 — Testing](08-testing.md). A workspace organizes multiple related mcpp packages (libraries or applications) within a single repository. Member packages share a unified set of dependency versions and toolchain settings while each keeping its own `mcpp.toml` project file. @@ -291,7 +301,7 @@ member. `mcpp test --workspace` reports each member separately and continues pas failing member, exiting non-zero if any member failed — ideal as a single, shell-free CI step for a workspace that tests many libraries. -#### What the fan-out reports +#### The fan-out report ``` Workspace testing member 'libs/core' (3/97) @@ -363,3 +373,4 @@ Workspaces work in concert with the C++23 module mechanism: ## 8. Complete Example See [`examples/04-workspace/`](../examples/04-workspace/) for a complete, runnable example of a three-member workspace. + diff --git a/docs/08-testing.md b/docs/08-testing.md new file mode 100644 index 00000000..95415712 --- /dev/null +++ b/docs/08-testing.md @@ -0,0 +1,124 @@ +# 08 — Testing + +**Reader:** anyone with code that has to keep working. + +**The question this chapter answers:** how tests are written and run, what mcpp +considers a test, and how to test something that does not run on this machine. + +**Not here:** how a runner reaches a device — that is +[41 — Reaching a Device](41-devices.md) — and the schema of the machine-readable +stream, which is [50 — Machine-Readable Output](50-machine-output.md). This +chapter states which flag produces it and stops there. + +Before: [05 — Dependencies and Resolution](05-dependencies.md) covers +`[dev-dependencies]`, which is how a test reaches a package the artifact does +not. After: [09 — Commands by Scenario](09-commands-by-scenario.md) is the +lookup for everything else. + +## The definition of a test + +Every `tests/**/*.cpp` is a test: mcpp compiles each one into its own program +and runs it. A test passes when its program exits zero. + +``` +myproject/ + mcpp.toml + src/… + tests/ + test_parse.cpp one program + unit/test_span.cpp another +``` + +There is no framework and no registration. A test may use one — `[dev-dependencies]` +is how it reaches it — but the contract mcpp holds is the exit code, which is +also why a test written for another framework needs no adapter. + +`mcpp new` scaffolds `tests/test_smoke.cpp` so a project starts with the +directory in place. + +## Running them + +```bash +mcpp test # build and run every test +mcpp test parse # only those whose name matches +mcpp test --list # what would run, without building or running it +mcpp test -- --verbose # everything after `--` goes to each test binary +``` + +Tests build with the same axes as `mcpp build`, so a test runs against the +configuration it is meant to check rather than against the default one: + +| flag | the set it selects | +|---|---| +| `--profile ` | `dev` (default), `release`, `dist`, or a `[profile.*]` the manifest declares | +| `--features ` | the feature set for the test build | +| `--target ` | a target other than the host | +| `--accel ` / `--no-accel` | the device backends the build targets | +| `--cap ` | pin a capability provider | + +`--timeout ` kills a test still running (default 300; `0` disables it) and +`--build-timeout ` bounds the compile. A test that hangs is reported as a +failure with its own name, not as a job that stopped. + +## Tests that reach packages the artifact does not + +```toml +[dev-dependencies] +counters = { path = "../counters" } +``` + +A `[dev-dependencies]` entry is resolved for the test build and for nothing +else: it is not in the artifact, and a consumer of the package never sees it. +That is the difference between a test's dependency and the package's own, and it +is why a test framework does not become part of what a library ships. + +[`examples/11-features`](../examples/11-features/) declares one and uses it. + +## Testing on a target this machine cannot run + +A test for a cross target or a bare-metal board is compiled for that target and +executed through a **runner** — an argv a board-support package supplies, which +mcpp performs with the test binary appended. + +```bash +mcpp test --target thumbv7em-none-eabihf # built for the board, run through its runner +mcpp test --no-runner # ignore the runner and execute directly +``` + +Nothing about the test changes. The same `tests/**/*.cpp` compiles for the +device, and the verdict is still the exit code — which is why a semihosting exit +or a QEMU exit code is what a bare-metal runner is chosen to produce. + +`--no-runner` exists for a host that can execute the binaries natively and +should not pay for an emulator. + +The runner itself, named runners, and what a board declares are +[41 — Reaching a Device](41-devices.md). + +## Tests that cannot run beside each other + +`mcpp test` runs test programs on a worker pool. One board on one probe, one +GPU, one serial port or a single-seat licence admits one user at a time, and two +workers reaching for it interleave rather than fail. + +The package that owns the resource states this about itself, and `mcpp test` +then serialises those tests. A project never has to remember `-j1`. + +## Reporting to a program + +```bash +mcpp test --message-format json +``` + +One NDJSON record per test, for a CI job or an editor. The schema and its +version are [50 — Machine-Readable Output](50-machine-output.md); what belongs +here is only that the flag exists and that the human format is the default. + +## Current limitations + +- A test is one `.cpp` producing one program. mcpp does not discover cases + inside a file, so a framework's per-case selection happens inside the program, + through arguments after `--`. +- `--build-timeout` is POSIX-only. +- `--workspace-timeout` bounds a `--workspace` fan-out and reports what did run; + it does not attribute the timeout to a member. diff --git a/docs/09-commands-by-scenario.md b/docs/09-commands-by-scenario.md new file mode 100644 index 00000000..4d9f3f55 --- /dev/null +++ b/docs/09-commands-by-scenario.md @@ -0,0 +1,417 @@ +# 09 — Commands by Scenario + +**Reader:** someone who knows the nouns and wants the verb. + +**The question this chapter answers:** which command does the thing at hand — +reclaiming disk, explaining a resolution, validating a descriptor, diagnosing an +environment. + +**Not here:** what each command means in depth. A scenario names the command and +links to the chapter that owns it. Before: [08 — Testing](08-testing.md). + +The command reference is `mcpp --help`, and each subcommand carries its own +`--help`. This chapter answers a different question: which command applies to a +situation that has already arisen — a build directory that keeps growing, a +resolution nobody expected, a descriptor about to be published, an index that +may be stale. The commands collected here are the ones whose name does not +announce the situation they belong to. + +Related documents: [01 — Getting Started](01-getting-started.md) for the +everyday build/test loop, [20 — Toolchain Management](20-toolchains.md), +[11 — Publishing a Library](11-publishing-a-library.md), +[50 — Machine-Readable Output](50-machine-output.md). + +Every output below was produced by the version of mcpp this chapter ships with. + +## Reclaiming disk without forcing a rebuild + +Two stores grow, they grow for different reasons, and one command each empties +them. Confusing them costs a full rebuild. + +| Store | Scope | Growth trigger | Emptied by | +|---|---|---|---| +| `target///` | one project | a configuration fingerprint changes and opens a new directory | `mcpp clean`, `mcpp clean --stale` | +| the build cache (`mcpp cache dir`) | the whole machine | any project compiles a dependency or a `std` module, or builds a host tool | `mcpp cache gc`, `mcpp cache prune`, `mcpp cache clean` | + +`mcpp clean` removes `target/` entirely, and the next build recompiles +everything. `mcpp clean --stale` removes only the fingerprint directories that +no recorded build still uses, so the configurations in use survive: + +``` +$ mcpp clean --stale --dry-run +would remove target/x86_64-linux-gnu/0123456789abcdef (0.0 B) +Would remove 1 directory (0.0 B) +``` + +"In use" means recorded in `target/.build_cache`, which `mcpp build` writes and +the fast paths read. Three consequences follow from that definition: + +- A directory no record names is not deleted merely for that. `mcpp test` + builds through a path that writes no record, and so does a `--no-cache` + build. An unrecorded directory written within `--older-than` (one day by + default) is kept; older than that it goes, and the cost of being wrong is one + rebuild of a configuration nothing has touched since. +- With no record at all the command refuses rather than guess. One + `mcpp build` establishes what is current. +- Directories under `target/` that are not fingerprint directories — `dist/` + from `mcpp pack`, among others — are never visited. + +`--dry-run` lists and deletes nothing. `--stale`, `--dry-run` and +`--older-than` each select this mode: `mcpp clean --older-than 3d` is a scoped +request and is not read as a full wipe. `--older-than 0` keeps no unrecorded +directory; a negative duration is refused. + +The build cache is machine-wide, so a project-local command must not empty it — +`--stale` and `--bmi-cache` are refused together. `mcpp cache list` shows what +occupies it. The rows carry no order, and a `0.0 B (incomplete)` row is an +entry an interrupted build left behind: + +``` +$ mcpp cache list +key kind size last used package +8a150ad49d666f94 std 29.6 MiB 6d ago std gcc@16.1.0 c++23 libstdc++ +9234eed9ef786c13 std 0.0 B 2d ago std (incomplete) +``` + +`mcpp cache gc` requires `--max-size`, `--older-than`, or both, and evicts +package entries only. A `std` BMI is shared by every project on the machine, and +the implementation excludes it from size-driven eviction on the grounds that +rebuilding one trades a lot of time for a little disk. `mcpp cache clean --std` +remains the explicit way to remove it. + +## The versions a package publishes + +`mcpp search` matches a substring and appends what each hit publishes, merged +across the descriptor's per-OS tables and sorted semver-descending: + +``` +$ mcpp search imgui + compat:imgui Dear ImGui immediate-mode GUI library core sources (1.92.8, 1.92.8-docking) + mcpplibs:imgui C++23 module package for Dear ImGui core and GLFW/OpenGL3 backends (0.0.6, 0.0.5, 0.0.4, ...) +``` + +The trailing `, ...` marks truncation: three versions are shown by default, and +its absence means the list is complete. `--all-versions` prints the whole list. +A package whose descriptor cannot be read prints as two columns — the version +list is best-effort display and never fails the search. + +`mcpp add` carries the same information when a name does not resolve. The +suggestion names the namespace to write and the versions behind it: + +``` + a package with this name exists under another namespace: + compat.eui-neo (0.5.6, 0.5.5, 0.5.3) +``` + +This scan runs only after a lookup has already failed, and its result reaches +error text and search output only. A bare name never resolves across namespaces +on the strength of it. + +## Explaining a resolution + +`mcpp why` reports what a build would resolve, and builds nothing: + +``` +$ mcpp why toolchain +toolchain: gcc 16.1.0 (x86_64-linux-gnu) + abi(libc)=glibc cxxstdlib=libstdc++ arch=x86_64 os=linux triple=x86_64-linux-gnu + reason: [toolchain] in mcpp.toml if set, else platform-native default +``` + +The topic is `toolchain`, `runtime`, `deps` or `runners`, and all four report +when none is named. `--target` and `--toolchain` turn the report into a query +about a pair the current directory does not use, which is how a target matrix +asks one cell at a time. + +An error code in a diagnostic expands through `mcpp self explain`: + +``` +$ mcpp self explain E0006 +E0006: index requires a newer mcpp + +The package index declares (index.toml [index].min_mcpp) that its +descriptors need a newer mcpp than this binary — parsing them would +silently misbehave, so resolution stops instead. Upgrade mcpp: +``` + +## Index freshness and offline builds + +`mcpp index status` answers whether the local index copies are current without +touching the network: + +``` +$ mcpp index status + index state refreshed revision path + xim fresh 28s ago 1f4b39d /home/speak/.mcpp/registry/data/xim-pkgindex + mcpplibs fresh 28s ago d4b36d7 /home/speak/.mcpp/registry/data/mcpplibs +``` + +`mcpp index update` refreshes them. A package published minutes ago and still +absent after a refresh is a propagation question, not a naming one — indices +reach clients as artifacts rather than git clones. + +`--offline` (or `MCPP_OFFLINE=1`) forbids the network for one invocation and +fails rather than fetch. `--locked` fails when resolution differs from +`mcpp.lock` instead of rewriting it, which is the shape a CI job wants. +`mcpp index pin ` records a commit for a custom index in +`mcpp.toml`; `mcpp index unpin` removes it. + +## Validating a descriptor before publishing + +`mcpp xpkg parse` reads a descriptor with the resolver's own grammar, so what +it reports is what resolution will see: + +``` +$ mcpp xpkg parse mcpp.plugins.lua +package mcpp.plugins (namespace 'mcpp') +versions linux 0.1.1, 0.1.0, latest +versions macosx 0.1.1, 0.1.0, latest +versions windows 0.1.1, 0.1.0, latest +form A — no mcpp segment (build info from the source's mcpp.toml) +parse OK +``` + +The per-OS lists are printed separately on purpose: a version added to one +platform table and forgotten in the others reads as "not found" on the +platforms that lack it, against a file that contains the version string. +`--json` emits the same facts for a script: + +``` +$ mcpp xpkg parse mcpp.plugins.lua --json +{"namespace":"mcpp","name":"plugins","versions":{"linux":["0.1.1","0.1.0","latest"],"macosx":["0.1.1","0.1.0","latest"],"windows":["0.1.1","0.1.0","latest"]},"form":"A"} +``` + +`mcpp emit xpkg` generates the entry to submit. See +[11 — Publishing a Library](11-publishing-a-library.md) for the full path. + +## Environment diagnosis + +`mcpp self doctor` checks the toolchain, the `std` module, the registry, cache +health and the last runtime-closure verdict, and reports what it found rather +than only what failed: + +``` +$ mcpp self doctor + Checking toolchain + ok gcc 13.3.0 (x86_64-linux-gnu) at /usr/bin/g++ + Checking cache health + ok build cache size = 2.5 GiB +warning: pre-v1 cache at '/home/speak/.mcpp/bmi' occupies 167.5 MiB and is no longer used — `mcpp cache clean --legacy` reclaims it +``` + +`mcpp self env` prints the paths and the resolved toolchain, `--format json` +included. `mcpp self config --mirror CN|GLOBAL` selects the download mirror; +mcpp and xlings hold this setting separately, so selecting it for one does not +select it for the other. + +## `[hooks]` — Project Build Lifecycle Commands (experimental) + +> **Experimental.** A hook cannot currently decide whether a build succeeded. +> Every hook failure is reported as a **warning** and `mcpp build` keeps the +> result it earned on its own; `side_effect = true` is refused with an error +> rather than honoured. The key stays in the schema so that manifests written +> today do not have to change when the feature is promoted. Two further limits +> are permanent rather than provisional: only the root project's hooks run, and +> only `mcpp build` runs them. + +A hook is a command `mcpp build` **owns for an interval**, and the event names +the interval: + +```toml +[hooks] +build_start = "echo build started" +build_failed = "notify-send 'build failed'" +build_finished = "notify-send 'build finished'" + +# Optional; these are the defaults. +timeout_seconds = 10 +enabled = true +side_effect = false # `true` is refused while this is experimental +``` + +| Key | Type | Default | The interval it names | +|---|---|---:|---| +| `build_start` | command | — | Opens after project preparation, closes when the command exits | +| `build_finished` | command | — | Opens after a build that succeeded, closes when the command exits | +| `build_failed` | command | — | Opens after a build that failed, closes when the command exits | +| `during_build` | command | — | Opens before the build, closes after it | +| `timeout_seconds` | integer, 1–86400 | `10` | Bounds one run of a command | +| `enabled` | bool | `true` | Enables all commands in this table | +| `side_effect` | bool | `false` | Whether a hook failure makes the build fail. **Reserved** — only `false` is accepted while this is experimental | + +The first three intervals are **self-closing** — they end when the command +does. "Synchronous" is not a separate mode here; it is what a self-closing +interval looks like. `during_build` is the one interval closed by something +else, and the two keys that only make sense for one shape follow from that +rather than being exceptions. + +A command is a string, or a table when it needs options: + +| Table key | Applies to | Meaning | +|---|---|---| +| `cmd` | every event | The command. Required. | +| `timeout_seconds` | self-closing events | Overrides the table default for this event | +| `loop` | `during_build` | Restart the command if it exits before the build ends | + +`loop` on a self-closing event and `timeout_seconds` on `during_build` are both +**errors**, not ignored keys: a self-closing interval ends when its command +exits, so there is nothing to restart, and `during_build` is already bounded by +the build. A key that is accepted and does nothing reads as a broken feature. + +Commands run through the host shell (`/bin/sh` or `cmd.exe`), with the +**project root** as their working directory — not the directory `mcpp build` +was typed in, so a relative path in a hook means the same thing wherever the +build was started. A self-closing command keeps ordinary terminal +input/output. Missing event commands are skipped. + +The lifecycle is: + +```text +during_build opens +build_start + ├─ build succeeds → during_build closes → build_finished + └─ build fails → during_build closes → build_failed +``` + +`during_build` closes **before** the terminal hook, so the two commands never +overlap. + +`build_failed` and `build_finished` are mutually exclusive, and both are +reachable only after `build_start` has run. A project that cannot be *prepared* +— an invalid manifest, an unresolvable dependency, no usable toolchain — fires +nothing: it has not started building, and its hook program may be exactly what +preparation would have installed. + +A hook command that cannot start, returns non-zero, or exceeds its timeout is a +hook failure. For `during_build` there is one more: a looped command that +**fails to stay up** — five consecutive runs ending unsuccessfully within a +second — stops being restarted and is reported. (A command that finishes +quickly and *successfully* is doing exactly what `loop` was asked to repeat, +and is not a failure.) Every one of those is reported as a **warning**, and the +build keeps the result it earned on its own — while `[hooks]` is experimental +it does not get a vote. A hook's own failure does not trigger another hook. + +`side_effect = true` is what will change that, and asking for it today is an +error: + +```text +error: mcpp.toml: error: [hooks].side_effect = true is not available yet: +[hooks] is experimental and cannot decide whether a build succeeded. … +``` + +Refused rather than quietly downgraded, because both silent options are worse: +honouring it would give an experimental feature a veto over every build, and +ignoring it would leave a project believing its build is gated on a notifier +when nothing is. When the feature is promoted, `true` will mean "a hook failure +fails the build" — and a build that failed on its own will still keep its own +exit code, so `mcpp build` never reports a compile error as a notifier problem. + +Two things are worth knowing about a `during_build` command specifically: + +- **Its output is discarded**, because it writes concurrently with the build + and would otherwise land in the middle of a compiler diagnostic. Run + `mcpp build --verbose` to see it. +- **It is stopped as a process tree**, not as a process. `player & wait` makes + the player a grandchild of the command mcpp started, and stopping only the + latter would leave the audio device held after the build. mcpp puts the + command in its own process group (a job object on Windows) and stops that, + including when the build is interrupted with Ctrl-C. + +Scope, precisely: + +- Only `mcpp build` runs hooks. `mcpp run`, `mcpp test` and + `mcpp build --configure-only` build too, and deliberately do not. +- Hooks belong to the **package being built**. In a workspace fan-out that is + each member in turn — its own `[hooks]`, around its own build, in its own + root. A *virtual* workspace root (`[workspace]` with no `[package]`) builds + nothing, so a `[hooks]` table there never fires. +- A dependency's `[hooks]` is **skipped**, always. Only the root project's run. + Every manifest mcpp parses carries the section, a dependency's included, and + nothing reads it — which is what keeps `mcpp add` from meaning "run this + author's shell command on my next build". This is a property of the design, + not a default awaiting a switch. +- Declaring an active hook opts the project out of the no-op fast path, because + `build_start` is specified to run after preparation. Expect `mcpp build` on an + already-current hooked project to cost a preparation pass rather than + milliseconds. + +An unrecognised key in `[hooks]`, or inside one event's table, is a warning (an +error under `--strict`), so a manifest written for a newer mcpp still loads. An +unrecognised *value* — a missing or non-string `cmd`, a `timeout_seconds` +outside 1–86400, a key offered to the wrong interval — is a manifest error. + +> **A hook is code, and `mcpp.toml` is part of the repository.** Building a +> freshly cloned project runs whatever its `[hooks]` say, with the privileges +> of whoever invoked `mcpp build`. This is the same trust `build.mcpp` already +> asks for ([30 — build.mcpp](30-build-mcpp.md)); `[hooks]` widens its reach +> rather than introducing it. + +Hook programs can be installed as ordinary xlings dependencies. For example, +an audio notifier can keep its sound files inside its own executable rather +than adding media handling to mcpp: + +```toml +[hooks] +build_finished = "mcpp-hooks-audioplayer niulai-mm" +build_failed = "mcpp-hooks-audioplayer niulai-niulai" +side_effect = false + +[xlings.workspace] +"xim:mcpp-hooks-audioplayer" = "0.0.1" +``` + +A different sound for a successful or failed build. `side_effect = false` is +written out rather than left to the default: it is the value this manifest +wants on its own terms — a missing audio device should never fail a build — so +it will still say so once the key has more than one accepted value. + +## The stage a failure belongs to + +A build crosses several stages, and a message names the one that failed. Reading +that first saves opening the wrong chapter. + +| a message about | the stage | the chapter to read | +|---|---|---| +| a package name, a version, or "no candidate" | resolution | [05](05-dependencies.md), [11](11-publishing-a-library.md) | +| a download, a payload, or a version floor | provisioning | [20](20-toolchains.md), [23](23-the-project-environment.md) | +| a triple, or "unsupported target" | the target | [21](21-the-target-triple.md) | +| a module that cannot be read or is not provided | the module graph | [30](30-build-mcpp.md) | +| a compile or link error inside a file of the project | none of them | the compiler's own message | + +The last row is the useful one: when the error is about the code, no part of +mcpp is involved and its documentation will not help. + +## Current limitations + +- `mcpp why --format json` is defined for the `toolchain` topic only. The other + topics report `'' has no machine-readable shape yet` and exit non-zero. +- `mcpp search` matches a substring; there is no field selector, and no way to + restrict a search to one namespace. +- `mcpp clean --stale` reads `target/.build_cache`, which holds a bounded number + of recent entries. A project built across more (target, profile) pairs than it + holds loses its oldest entries, and a directory whose entry has been evicted + is then treated as unrecorded — kept while it is newer than `--older-than`, + removed after that. +- `mcpp cache gc --older-than 0` is rejected with `bad --older-than value '0' + (expected {s,m,h,d})`, while `mcpp clean --stale --older-than 0` accepts it. + The two options share a parser but not this case. + +`mcpp emit xpkg` writes a key `mcpp xpkg parse` does not know. For a package +that keeps its own `mcpp.toml`, the emitted `mcpp` segment ends with +`manifest = "mcpp.toml"`, and the descriptor parser reports it as unknown: + +``` +error: unknown mcpp-segment key 'manifest' — silently ignored at build time + by this mcpp version +error: synthesised manifest missing sources (mcpp segment must declare + `sources = { ... }`) +``` + +The second error follows from the first: the ignored key means no sources are +derived from the manifest it names. Adding `sources = { … }` by hand removes the +second and not the first, and `mcpp xpkg parse` still exits 1. + +No descriptor in `mcpp-index` uses that key — 0 of 218. A package that keeps its +own `mcpp.toml` omits the `mcpp` field entirely, and mcpp looks the manifest up +under the version directory. Measured on 2026.9.8.1. + diff --git a/docs/02-pack-and-release.md b/docs/10-pack-and-release.md similarity index 96% rename from docs/02-pack-and-release.md rename to docs/10-pack-and-release.md index 2eed3c6a..d99bb095 100644 --- a/docs/02-pack-and-release.md +++ b/docs/10-pack-and-release.md @@ -1,4 +1,13 @@ -# 02 — Packaging an Application for Release +# 10 — Packaging an Application for Release + +**Reader:** someone shipping a program to a machine that does not have mcpp. + +**The question this chapter answers:** how do I turn a build into something +another machine can run, and what does each packaging mode carry. + +**Not here:** shipping a **library** for other packages to build against, which +is [12 — Distributing a Prebuilt Library](12-binary-distribution.md). After: +[11 — Publishing a Library to mcpp-index](11-publishing-a-library.md). > This page is about bundling a **program**. To ship a *library* as interface + > prebuilt binaries, see [12 - Distributing a Prebuilt Library](12-binary-distribution.md). @@ -21,7 +30,7 @@ private glibc precisely so a binary's behaviour does not depend on which distribution happens to be underneath it, and reaching back out to the host's libc to distribute would give that away at the last step. -| | Route | Command | Where its C runtime comes from | Choose it when | +| | Route | Command | Source of its C runtime | Applicability | |---|---|---|---|---| | **A** | Through the ecosystem | `mcpp emit xpkg` → `xlings install ` | the target machine's own xlings payloads | the target has xlings | | **B** | One static file | `mcpp build --target x86_64-linux-musl` | nowhere — it is linked in | a single file with no runtime dependency | @@ -85,7 +94,7 @@ ELF in the bundle for anything under `$MCPP_HOME` and fails on a hit. machine: a graphics driver's user-space half is version-locked to the running kernel module, and for the proprietary stacks redistribution is not permitted. Declare those as run-phase capability requirements (§2.11 of -`docs/05-mcpp-toml.md`), and the mode table gains a column: +`docs/04-mcpp-toml.md`), and the mode table gains a column: | Mode | Program needing a host-provided capability | |---|---| @@ -143,7 +152,7 @@ when it includes a directory (relative or absolute), the literal path is used. For the full set of options, see `mcpp pack --help`. -### What a packed artifact is built with, and what travels inside it +### The build inputs of a packed artifact, and its contents Two things differ from `mcpp build`, and both exist because a package leaves this machine: @@ -168,7 +177,7 @@ producer's source tree and build directory. What is removed depends on what the artifact *is* — this is dh_strip's division, and the archive row is the one that matters: -| artifact | strip flags | why not more | +| artifact | strip flags | the reason for no more | |---|---|---| | executable | `--strip-all` | nothing links against it | | shared library | `--strip-unneeded` | keeps `.dynsym` — that IS the export list | @@ -325,7 +334,7 @@ force_bundle` still overrides this, as it does the ELF skip list. `vcruntime140.dll` and `msvcp140.dll` are **not** Windows' own: they belong to the MSVC toolset, exactly as `libstdc++.so` belongs to gcc. Whether they -travel is decided by `cxx_runtime` (see `docs/05-mcpp-toml.md`), not by this +travel is decided by `cxx_runtime` (see `docs/04-mcpp-toml.md`), not by this list — and `mcpp pack` refuses a combination that cannot deliver what the contract promised: @@ -416,3 +425,4 @@ the current `.zip`, and distribution formats such as `.deb` / `.rpm` / AppImage, are also on the roadmap. This document evolves alongside the `mcpp pack` implementation; for the latest options, refer to `mcpp pack --help`. + diff --git a/docs/10-publishing-a-library.md b/docs/11-publishing-a-library.md similarity index 89% rename from docs/10-publishing-a-library.md rename to docs/11-publishing-a-library.md index 25b65205..1205ac39 100644 --- a/docs/10-publishing-a-library.md +++ b/docs/11-publishing-a-library.md @@ -1,10 +1,21 @@ -# 10 - Publishing a Library to mcpp-index +# 11 — Publishing a Library to mcpp-index -**English** | [简体中文](zh/10-publishing-a-library.md) +**English** | [简体中文](zh/11-publishing-a-library.md) + +**Reader:** a library author whose package should become one others can name in +their `[dependencies]`. + +**The question this chapter answers:** what steps take a tagged release to a +resolvable package, and in which order they must happen. + +**Not here:** what makes two packages the same package — +[SPEC-001](specs/package-identity.md) — and shipping compiled artifacts, which +is [12](12-binary-distribution.md). Before: +[10 — Packaging an Application](10-pack-and-release.md). How a library becomes something `[dependencies]` can name. This is the -*library author's* chain; [09 - Releasing mcpp](09-release.md) is about -releasing mcpp itself, and [02 - Packaging & Release](02-pack-and-release.md) +*library author's* chain; [92 - Releasing mcpp](92-release.md) is about +releasing mcpp itself, and [10 - Packaging & Release](10-pack-and-release.md) is about `mcpp pack` bundling an application. ## Release sequence @@ -158,3 +169,4 @@ re-refreshing the index looking for it. - [ ] `publish-artifact.yml` succeeded - [ ] cold resolve (seeded copy deleted) downloads and compiles it - [ ] consumers bumped + diff --git a/docs/12-binary-distribution.md b/docs/12-binary-distribution.md index 1feaa7fb..6b44891f 100644 --- a/docs/12-binary-distribution.md +++ b/docs/12-binary-distribution.md @@ -1,13 +1,23 @@ -# 12 - Distributing a Prebuilt Library +# 12 — Distributing a Prebuilt Library **English** | [简体中文](zh/12-binary-distribution.md) +**Reader:** a publisher who ships compiled artifacts rather than source. + +**The question this chapter answers:** how do I ship binaries, and how does a +consumer's build decide whether one of them fits. + +**Not here:** publishing source, which is +[11 — Publishing a Library](11-publishing-a-library.md), and the accelerator +field of a compatibility tag, which is +[42 — Heterogeneous Builds](42-heterogeneous-builds.md). + > Ship a library as **interface + prebuilt binaries** instead of as source. > It applies to closed-source distribution, offline environments, and builds whose > artifacts a build farm has already produced. > -> Related: [02 - Packaging & Release](02-pack-and-release.md) covers bundling an -> *application*. [10 - Publishing a Library](10-publishing-a-library.md) is the +> Related: [10 - Packaging & Release](10-pack-and-release.md) covers bundling an +> *application*. [11 - Publishing a Library](11-publishing-a-library.md) is the > source route. ## Overview @@ -31,7 +41,7 @@ mcpp pack mathkit --target x86_64-linux-gnu \ | `kind` | `mcpp pack ` produces | `--mode` | |---|---|---| -| `bin` | an application bundle (see [02](02-pack-and-release.md)) | the four depths | +| `bin` | an application bundle (see [10](10-pack-and-release.md)) | the four depths | | `lib` | a **static library package** | — | | `shared` | a **dynamic library package** | — | @@ -79,7 +89,7 @@ links `lib.so` and the loader then asks for the `SONAME`, and those are different filenames. Shipping only the built file links cleanly and then fails to start. -### Why neither set may be trimmed +### The reason neither set may be trimmed A **source** distribution of the same package puts every one of its `include_dirs` on its consumers' include path. If a binary package shipped a @@ -256,7 +266,7 @@ That is a degradation, not a break, and it is the right direction. But it means **the gate protects new clients only**, which belongs in the release notes of any package published to a mixed-version audience. -## What travels inside a package, and what deliberately does not +## The package contents, and the deliberate exclusions A published package must work on a machine that is not the publisher's. Two steps enforce that, and both run on every artifact the packer stages. @@ -285,7 +295,7 @@ error while loading shared libraries: libstdc++.so.6: cannot open shared object **`$ORIGIN` is not the fix.** Measured on a real package with the build machine's store made unreachable: -| state on the shipped `.so` | consumer's `DT_RPATH` inherited? | result | +| state on the shipped `.so` | inheritance of the consumer's `DT_RPATH` | result | |---|---|---| | stale absolute `DT_RUNPATH` | no | fails | | **no tag at all** | **yes** | **runs** | @@ -310,7 +320,7 @@ no test in this suite produces a `.dylib` to measure the edit on. ### Debug information is removed -See [docs/02](02-pack-and-release.md) for the flags, the per-shape table, and +See [docs/02](10-pack-and-release.md) for the flags, the per-shape table, and `--debug-symbols`. The rule that matters for a *library* package: a static archive is only ever `--strip-debug`ed, because `--strip-all` removes the archive symbol index and the consumer's link then fails with `archive has no @@ -490,3 +500,4 @@ answers "not installed". So the static half of the old-client check (the generat manifest uses no section a previous mcpp cannot read) runs everywhere, and the real half — build against the package with the previous release — has been run by hand, not by CI. + diff --git a/docs/17-the-project-environment.md b/docs/17-the-project-environment.md deleted file mode 100644 index 414d8eda..00000000 --- a/docs/17-the-project-environment.md +++ /dev/null @@ -1,199 +0,0 @@ -# 17 - The Project Environment - -A project can declare the environment it builds in. That one declaration -decides which C library the project links against and which tools its build -programs find — so a `mcpp.toml` means the same build on a developer's laptop -and in CI, whatever else those two machines happen to have installed. - -```toml -[xlings] -subos = "tools" - -[xlings.workspace] -"xim:qemu-riscv" = "9.2.4-1" -``` - -Working project: `examples/07-project-subos/`. - -## 1. What a SubOS is - -A SubOS is a directory that holds a userspace: its own `bin`, its own library -view, its own installed package versions, and a `subos_info` block describing -itself. mcpp treats it as the answer to "what does this project build -against", and it is the only mechanism that answers that question — not the -compiler's path, not `XLINGS_ACTIVE_SUBOS`, not the shell. - -Two kinds exist, and the difference is where the directory lives: - -| Declaration | Directory | Shared with | -|---|---|---| -| none | mcpp's initialized `subos/default` | every project on the machine | -| `subos = "default"` | the same directory, named explicitly | every project on the machine | -| `subos = ""` | `/.mcpp/.xlings/subos//` | nothing | - -The third row is the isolated one. It belongs to the project, it sits beside -the manifest, and removing the project removes it. - -## 2. What the declaration decides - -**The C library.** A payload-first build links against one specific glibc, and -which one is a fact about the project rather than about the machine. Chapter 8 -covers the binding, the degradation rules, and what a SubOS that does not -describe itself does to them. - -**The tools a build program sees** (mcpp 2026.8.25.1+). The declared -environment's `bin` goes to the front of the `PATH` that `build.mcpp` runs -with: - -``` -PATH=: -``` - -A build program that spells `qemu-system-riscv64` as a bare name therefore gets -the copy inside the declared environment. Chapter 7 covers the contract this -rides on. - -**Only for projects that declare one.** A project with no `[xlings].subos` -gets the `PATH` mcpp was started with, byte for byte. Putting a shared -directory in front of every project would make what a build sees depend on what -else had been installed on that machine — two projects on one machine would -agree with each other, and the same project on two machines would not. -Declaring it is what puts it there. - -**Prefixed, not replaced.** A build program legitimately calls `git`, -`python3` or a shell, and none of those live in a SubOS. Front position makes -the declared environment the default answer; everything else stays reachable -behind it. - -### 2.1 Which version pins apply (2026.9.3+) - -Naming an environment also changes where a tool's version comes from. A -project's own `[xlings.workspace]` entries always win — over the environment -here, and over a dependency's declaration by the rule in section 3 — and what -differs is what they are laid over: - -| The project declares | The version of a tool it did not name comes from | -|---|---| -| `[xlings.workspace]`, no `subos` | the machine's environment | -| `[xlings.workspace]` and `subos = ""` | that environment's own workspace; the machine's does not apply | - -The second row is what isolation means. A named environment has its own -installed set, and carrying the machine's versions into it would name versions -that are not there — so a project that relied on the machine's tools has to -declare them once it names an environment. - -An `xlings use` performed inside the project outranks both, until mcpp rewrites -the environment: it is the layer merged last, and an action a person took -should beat a file. - -## 3. What the declaration does not decide - -`[xlings.workspace]` names packages to be present in the environment, and each one's -payload directory is delivered separately as `MCPP_XPKG__DIR`. That is a -different question from `PATH` and stays a different answer: a build program -that needs a package's data files (protoc's well-known `.proto` files, say) -asks for the directory, and one that needs to *run* a program asks `PATH`. - -**A workspace member's declaration is not the workspace's.** In a workspace -build the workspace root owns the selection; a member's `[xlings]` applies only -when that member is built as an independent root. - -**A dependency's declaration is a different matter, and it is honoured** -(2026.9.5.4+ for `[xlings] deps`, 2026.9.6.6 for the version rule below). A -board-support package knows which emulator reaches its machine, and a rule -package knows which toolkit its rule drives; a consumer that had to repeat -either is the duplication such packages exist to remove. What the dependency -declares is installed, and `MCPP_XPKG__DIR` answers for it in that -dependency's own build program. - -Where a project and a dependency name **one package**, one version of it is -installed: identity is `(namespace, name)` and the version is a constraint on -it. The declaration nearer the artifact wins and the override is reported; -a pin that fails a requirement the other side stated is refused naming both. -See *One package, one version* in [05 — mcpp.toml](05-mcpp-toml.md). - -## 4. Reading an environment, never creating one - -mcpp resolves a declared name and reads what it finds. A name that does not -resolve is a hard error: - -``` -error: selected SubOS 'tools' does not exist at …/.mcpp/.xlings/subos/tools; -create/bootstrap that environment instead of falling back to active/default -``` - -Falling back to the default or to whatever is active would substitute a -different environment for the one the manifest named, which is precisely what -would make one `mcpp.toml` mean two different builds. Creating and populating -a SubOS is xlings' layer — `xlings subos new` — and mcpp managing SubOS state -would invert that layering. - -An environment that exists but carries no `subos_info` block **degrades rather -than fails**: the runtime binding reports `inconclusive`, no payload-first -binding is available, a note is printed, and the build continues. Chapter 8 -gives the full rule. - -## 5. When a private environment is worth it - -- **A generator whose version changes what it emits.** `protoc`, `flatc`, a - shader compiler: the output is an input to everything downstream, so the - project pins the producer instead of hoping the machine has a compatible one. -- **An emulator a build program runs.** Several bare-metal packages boot an - artefact under QEMU as part of proving it works; which QEMU is part of what - was proven. -- **A project whose CI and developer machines differ**, where neither is wrong - and the build must not notice. -- **Two projects on one machine that need different versions of one tool.** - Sharing a directory means one of them loses; a private environment means the - question does not arise. - -Against that: an isolated environment is a directory that has to be created and -populated, and the first build pays for it. Since 2026.8.29 mcpp does that -work — a declared `[xlings.workspace]` entry is provisioned on first use, and a named -`[xlings] subos` that does not exist yet is created rather than refused — but -the cost is real: the first build on a clean machine downloads and installs -before it compiles anything. A project whose tools are ordinary and whose -versions do not matter is better off declaring nothing and inheriting the -machine's. - -Under `--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL`, mcpp refuses -instead of installing, and names the packages so they can be provisioned -out of band — the same two knobs `[toolchain]` honours, for the same reason: an -unasked-for download is not something a build decides on a project's behalf. - -The declaration is provisioned on every host that builds the project, and a -package the host cannot install is an error, not a skipped entry. A tool that -exists for one host platform only is therefore declared for that platform -(2026.9.2.1): `deps = [{ linux = "qemu-user-aarch64" }]` declares the emulator -on Linux and nothing elsewhere. The keys and the resolution rule are in -chapter 5, §2.13. - -**Which verbs install it.** An entry may name a tier — -`{ version = "0.24.0", when = "run" }` — and a `[feature-xlings.]` -table gates one on a feature. A tool the project will not use is then not -downloaded: chapter 5, §2.13. Omitting the tier is the historical behaviour. - -**The runner.** A program under `[xlings.workspace]` is also where -`[target.].runner` looks first for its first element, before `PATH` -(chapter 5, §2.7.3). The two keys together provision a user-mode emulator on a -CI host and execute a cross-built artifact through it, without the manifest -naming the payload's path. - -## 6. What belongs somewhere else - -| Need | Where it goes | -|---|---| -| a library the program links | `[dependencies]` | -| the compiler | `[toolchain]`, chapter 3 | -| a host tool a dependency produces | `tools = [...]`, chapter 7 | -| a tool present in the environment | `[xlings.workspace]` | -| a tool only one verb or one feature needs | `when = "run"`, `[feature-xlings.]` | -| which environment | `[xlings] subos` | - -## 7. Related chapters - -- [7 - build.mcpp](07-build-mcpp.md) — the contract a build program receives, - including the `PATH` it runs with. -- [8 - Toolchain Internals](08-toolchain-internals.md) — runtime selection, - the `RuntimeBinding` snapshot, and the degradation rules. -- [5 - mcpp.toml](05-mcpp-toml.md) — every manifest key, including `[xlings]`. diff --git a/docs/03-toolchains.md b/docs/20-toolchains.md similarity index 68% rename from docs/03-toolchains.md rename to docs/20-toolchains.md index 43affda1..0756c215 100644 --- a/docs/03-toolchains.md +++ b/docs/20-toolchains.md @@ -1,4 +1,14 @@ -# 03 — Toolchain Management +# 20 — Toolchain Management + +**Reader:** anyone for whom which compiler runs matters — a version floor, a +second toolchain, a machine that must not use its own. + +**The question this chapter answers:** which compiler will build this project, +how is it installed, and how do I choose another. + +**Not here:** how the resolution is implemented, which is +[91 — Toolchain Internals](91-toolchain-internals.md), and how a target is +named, which is [21 — The Target Triple](21-the-target-triple.md). > mcpp maintains an independent toolchain sandbox, fully isolated from the system PATH. @@ -81,7 +91,7 @@ The pair persists as `[toolchain] default = "gcc@16.1.0"` + configs with combined spellings like `default = "gcc@15.1.0-musl"` keep working unchanged.) -### What decides a build's compiler +### Compiler selection for a build Five things can name it. They are ranked, and the rank is what makes the two statements a project can write outrank everything mcpp keeps on its own: @@ -163,7 +173,7 @@ Available toolchains (run `mcpp toolchain install `): `*` marks the default pair. The Targets block is the live view of the target vocabulary, in four statuses: -| Status | Meaning | What to do next | +| Status | Meaning | Next step | |---|---|---| | `installed` | a payload here already produces it | nothing | | `available` | a payload exists for this host | `mcpp toolchain install` | @@ -212,7 +222,7 @@ It uses the regular GCC module pipeline (`gcm.cache`, `import std` via libstdc++'s `bits/std.cc`). The target's default linkage is **static** — the produced `.exe` is fully self-contained (no `libstdc++-6.dll` to ship, runs directly under wine). To opt out, set it on the target section — -`linkage` is exact-triple only (§2.7 of [mcpp.toml](05-mcpp-toml.md)), and a +`linkage` is exact-triple only (§2.7 of [mcpp.toml](04-mcpp-toml.md)), and a `[build] linkage` key does not exist and is silently ignored: ```toml @@ -329,7 +339,7 @@ error: [toolchain] linux = "system" is not supported: mcpp builds only with *family* whose installation mcpp locates and identifies, on the one platform where the compiler cannot be redistributed. See the section above. -#### Why the toolchain and the libraries get different answers +#### The reason the toolchain and the libraries differ mcpp's rule about host dependence is not uniform across axes, and the split is deliberate: @@ -433,7 +443,7 @@ environment from the VC tools + Windows SDK (no `vcvarsall` involved), stages **The Windows SDK follows the origin**, because the two origins answer different questions and so must the SDK: -| origin | how the SDK is chosen | +| origin | SDK selection | |---|---| | `msvc@` | the `xim:windows-sdk` payload installed **with that toolset**, in mcpp's own store. `WindowsSdkDir` / `WindowsSdkVersion` in the environment are **ignored**, and mcpp prints a `note:` saying so. | | `msvc@system` | **`WindowsSdkDir`** (+ `WindowsSdkVersion`) if declared, then `C:\Program Files (x86)\Windows Kits\10`. | @@ -533,7 +543,7 @@ error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' Two rows answer a different question, and their pin cannot be overridden at all: -| row | why | +| row | reason | |---|---| | every `*-none-elf` | no per-host cross payload exists; clang and lld are cross-compilers by construction and gcc is not | | `x86_64-windows-musl` | no gcc payload emits a PE with a musl C library — the mingw payload emits PE with the MinGW CRT, which is the separate `-gnu` row | @@ -551,7 +561,7 @@ option, for a decision made a hundred lines earlier. A program classifying these reads `data.reason` from `mcpp why toolchain --format json` (`convention-unreplaced` / `capability-pin`) -rather than the sentence — see [chapter 11](11-machine-output.md). +rather than the sentence — see [chapter 11](50-machine-output.md). A project can set its *default* build target — this is where "this project ships fully-static" belongs (static output is a product property, not a @@ -650,3 +660,253 @@ Tracked as [mcpp#256](https://github.com/mcpp-community/mcpp/issues/256). `tests/e2e/150_clang_module_operator_template.sh` is a canary over the bundled LLVM toolchains, so a future Clang bump that fixes — or re-breaks — this becomes visible instead of silently changing what packages can express. + +## The C++ runtime contract (`cxx_runtime`) + +`cxx_runtime` states what the produced artifact promises about the machine that +runs it. It is a **distribution** property, not a build one — it describes the +runtime dependency set, and the flags that deliver it differ per platform. + +> **A target with no C++ in it has no C++ runtime contract to honour.** mcpp +> links such a target with the C driver and leaves the C++ runtime flags off the +> line entirely, so a pure-C shared library does not acquire a dependency on +> `libstdc++`/`libc++` it has no use for. One C++ translation unit anywhere in +> the target puts the whole target back on the C++ driver. This is derived from +> the sources — there is no key for it. + +```toml +[build] +cxx_runtime = "self-contained" # applies to every target (the default) + +# or, per role: +[build.cxx_runtime] +default = "self-contained" # executables +tests = "host-coupled" # test binaries never leave this machine +shared = "self-contained" # shared libraries (see below — the + # default differs by target format) + +# or, per target triple — beside `linkage`, which is the same axis: +[target.x86_64-linux-gnu] +cxx_runtime = "host-coupled" # e.g. this build is for a distro package +``` + +| value | the artifact needs, at run time | typical use | +|---|---|---| +| `self-contained` (default) | no C++ runtime outside itself | shipping a binary | +| `toolchain-coupled` | the C++ runtime of the toolchain mcpp installed | local iteration | +| `host-coupled` | whatever the driver resolves by default (the system runtime) | distro packaging, `dlopen` plugins that must share a runtime with their host | + +**Self-contained by default (portable by default)**: on macOS this statically +links LLVM's bundled libc++/libc++abi — the system libc++ would otherwise pin the +runnable version to the build machine's OS (older systems lack newer symbols, e.g. +the support symbols behind `std::print`), and only static linking can truly deliver +the `macos_deployment_target` floor. On Linux/MinGW it is `-static-libstdc++` (GCC) +or the whole-link `-static` (MinGW); on a Linux clang/libc++ toolchain it links +libc++.a/libc++abi.a/libunwind.a explicitly. A lower macOS floor (11–13) requires a +self-built libc++ archive (already verified to work, a data-level switch, available +on request). + +**Shared libraries are the one role whose default depends on the target format**, +because the hazard does. A `.so`/`.dylib`/`.dll` is not a small executable — it is +loaded *into* a process that already has a C++ runtime. + +| target | default for `kind = "shared"` | reason | +|---|---|---| +| ELF (Linux, …) | `toolchain-coupled` | ELF has one global symbol namespace and the first definition loaded wins. A `.so` that statically embedded libstdc++ **exports** it, and the executable linking that library binds *its* `std::` references there — its own `self-contained` contract silently becomes a no-op, and its C++ runtime is whichever build of that library happens to load. | +| Mach-O | `self-contained` | the mechanism there is already `-load_hidden`, i.e. hidden visibility, so dyld never unifies those symbols; and toolchain-coupled is not available on macOS at all (see the note below). | +| PE (Windows) | `self-contained` | PE has no global symbol namespace — imports resolve per-DLL by name, so a DLL's private runtime cannot be picked up by anything else. | + +Setting `shared = "self-contained"` on ELF is supported and does exactly what it +says: the library embeds the runtime. mcpp additionally passes +`-Wl,--exclude-libs` for the standard-library archives, so the embedded copy stays +out of the library's dynamic symbol table and cannot be picked up by anything that +links it. Template instantiations emitted by the consuming code (weak/COMDAT `std::string` +symbols and the like) are still exported — that is the intended C++ ABI behaviour +and is not the leak this guards against. + +A project-wide `cxx_runtime = "…"` (or `static_stdlib = false`) applies to shared +libraries too: a human said what the whole project promises. The format-specific +default applies only when nobody said anything. + +`static_stdlib` is the older spelling and still works: `true` means +`self-contained`, `false` means `host-coupled`. An explicit `cxx_runtime` wins. + +**A contract that cannot be honored is reported, never silently downgraded.** If a +toolchain ships no `libc++.a`, or a contract has no mechanism on that platform, +the build prints what it fell back to instead of quietly producing a different +artifact than the manifest asked for. + +### On the MSVC runtime + +The CRT model is the mechanism here, and it is a **whole-project** switch: cl +bakes `_MSVC_MT`/`_MSVC_MD` into the one `std` module a project builds, so a +per-role contract that disagrees with the project's cannot be honoured and is +reported rather than ignored. + +| value | meaning on MSVC | +|---|---| +| `self-contained` | `/MT` — the static CRT. `linkage = "static"` selects the same thing from the libc axis. | +| `host-coupled` (default under `/MD`) | the target provides `vcruntime140.dll` / `msvcp140.dll` — i.e. Visual Studio or the redistributable is installed there. | +| `toolchain-coupled` | the toolset's **own** copy of those DLLs travels with the artifact. | + +`toolchain-coupled` is worth spelling out, because the obvious reading is +wrong. `ucrtbase.dll` *is* a Windows component (since Windows 10) and mcpp +never ships it. `vcruntime140.dll` and `msvcp140.dll` are **not**: every MSVC +toolset carries them under `VC\Redist\MSVC\\\`, exactly the +way a gcc payload carries `libstdc++.so`. Under this contract mcpp stages them +beside the artifact — which is what makes a default `/MD` build runnable on a +machine that has only the pinned toolset and no Visual Studio at all. + +The debug CRT (`vcruntime140d.dll` and friends, under `debug_nonredist\`) is +never staged: it may not be redistributed. + +> **Upgrading from 2026.8.15 or earlier?** This key used to be **inert** on the +> MSVC ABI — it reported `not implemented for the MSVC runtime yet` and every +> value fell back to `/MD`. Since 2026.8.16 it is honoured, so a manifest that +> carries `cxx_runtime = "self-contained"` from that era **changes CRT model on +> upgrade**, from `/MD` to `/MT`. It is not a stricter version of the same +> model, and the switch is silent because the value was always valid. A project +> that set it while the key did nothing should re-confirm the intended value. + +Combining it with `/MT` is a contradiction rather than a missing feature — a +static CRT leaves no DLL to couple to — so it is reported and resolved to +`self-contained`. `mcpp pack` enforces the other half: a mode that bundles +nothing (`--mode system`, `--mode static`) cannot deliver `toolchain-coupled` +and refuses. + +**Scope.** The contract governs the C++ runtime only. Static **libc** is a separate +axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment +floor is a third (`macos_deployment_target`). Also, `host-coupled` means mcpp adds +nothing to embed a C++ runtime; it does not strip the toolchain rpath the link +carries for other reasons, so on ELF such an artifact may still find the +toolchain's libraries first. + +> **macOS + `self-contained` and static initialization order.** Mach-O has no +> priority-ordered initializer section and libc++'s `` carries no +> `ios_base::Init` guard of its own (unlike libstdc++ and the MSVC STL), so a +> stream initializer pulled out of `libc++.a` would otherwise run *after* the +> program's own global constructors — a global whose constructor touches +> `std::cout` would read an unconstructed stream and crash at process start. mcpp +> links a tiny generated object first to force the streams up; nothing is required +> of the calling code. See mcpp-community/mcpp#336. + +`defines` takes **bare** macro names (no `-D`) and desugars each entry to `-D` on +both the C and C++ compile channels. It reaches every TU in the package — module +interface units included — so it also reaches the compiler's own P1689 module scan. + +> **It does not make a macro-guarded `import` acceptable.** mcpp runs its own +> lexical pre-scan before the compiler ever sees the file, and that scanner +> rejects an `import` inside **any** `#if` / `#ifdef` block without evaluating the +> condition: +> +> ``` +> error: import statement inside conditional preprocessor block (forbidden in M1) +> ``` +> +> So a `#ifdef FOO` / `import bar;` pair fails even when `FOO` is in `defines`. +> Put the conditional around an `#include` in the global module fragment instead. +> Tracked as mcpp-community/mcpp#421. Assembly units pick it up too. It is a +build input like any other, so `[target.'cfg(...)'.build]` can carry it: + +```toml +[build] +defines = ["APP_NAME=\"demo\""] + +[target.'cfg(windows)'.build] +defines = ["USE_WIN32", "WINVER=0x0A00"] +``` + +Picking the right axis: + +| You want the macro on… | Use | +|---|---| +| every TU of this package | `[build].defines` (here) | +| one binary's own entry source only | `[targets.].defines` | +| a specific set of files | `[build].flags` with a `glob` + `defines` | +| every TU **and** every consumer's TUs | `[features.].defines` (an interface contribution) | + +`[build].defines` is private to the package: it does not propagate to consumers. + +Unsupported keys under `[build]` are reported as a warning (an error under +`--strict`) rather than silently ignored. + +Do not configure the C++ standard via `build.cxxflags = ["-std=..."]`. Instead use: + +```toml +[package] +standard = "c++26" +``` + +mcpp applies the same standard to ordinary C++ compilation, module scanning, +`compile_commands.json`, and the standard library BMI build for `import std`. + +**glob exclusion** (`!` prefix, mcpp 0.0.4+): + +```toml +[build] +sources = [ + "src/**/*.cpp", + "!src/**/*_test.cpp", # Exclude test files + "!src/**/*_fuzzer.cpp", # Exclude fuzzers +] +``` + +**Per-glob flags** (mcpp 0.0.95+): `[build] flags` is an ordered array of +inline tables attaching extra compile flags to exactly the sources a glob +matches — the tool for SIMD dispatch TUs and vendored-code warning isolation: + +```toml +[build] +flags = [ + { glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] }, + { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] }, + { glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] }, +] +``` + +Keys per entry: `glob` (package-root-relative, required) plus `cflags` / +`cxxflags` / `asmflags` / `defines` (no `ldflags` — linking has no per-TU +scope). Declaration order is application order: a later entry's flags land +later on the command line, so with GNU "last flag wins" a narrower glob +placed after a broader one overrides it. All matching entries apply. These +are private build flags — they never propagate to consumers. A glob that +matches no source file prints a warning (a typo'd glob must not silently do +nothing). + +**Generated files** (mcpp 0.0.95+): `[generated_files]` maps a relative path +to file contents (TOML multiline strings supported). Entries are written +into the project tree before source globs expand — the same mechanism index +descriptors use to synthesize module wrappers — and the content enters the +fingerprint, so editing it rebuilds: + +```toml +[generated_files] +"src/gen/wrap.cppm" = """ +module; +#include +export module wrap; +""" +``` + +Paths must stay inside the project root (`..` / absolute paths are parse +errors). + +**Assembly sources** (mcpp 0.0.95+): `.S`/`.s` (GAS — preprocessed by the C +driver, covers ARM and AT&T-syntax x86) and `.asm` (NASM — Intel-syntax x86) +are first-class sources: default-globbed, fingerprinted, built incrementally +in parallel, and linked like any other object. The NASM output format is +derived from the target triple (`elf64`/`win64`/`macho64`/... — cross builds +just work), and `nasm` itself is resolved lazily only when `.asm` units exist: +`PATH` first, then the mcpp sandbox, then `xlings install nasm`; if none +yields nasm ≥ 2.16 the build **fails hard** (assembly is never silently +skipped). Limits: `.asm` targets x86 only (hard error elsewhere — gate the +files off other targets), `.S` is unavailable on the MSVC toolchain, and +`.asm` means NASM syntax (MASM sources should be `!`-excluded). + +## Current limitations + +- **A macOS host has no Linux-targeting payload at all**, so no Linux target is + reachable from there. This is a publishing boundary rather than an engine one. +- An explicit `[toolchain]` in the manifest, or a `[target.X].toolchain`, is + **never overruled**. A project that pins one and then meets a target the pin + cannot serve is refused rather than silently given another compiler. diff --git a/docs/21-commands-by-scenario.md b/docs/21-commands-by-scenario.md deleted file mode 100644 index 2f385eb4..00000000 --- a/docs/21-commands-by-scenario.md +++ /dev/null @@ -1,211 +0,0 @@ -# 21 — Commands by Scenario - -The command reference is `mcpp --help`, and each subcommand carries its own -`--help`. This chapter answers a different question: which command applies to a -situation that has already arisen — a build directory that keeps growing, a -resolution nobody expected, a descriptor about to be published, an index that -may be stale. The commands collected here are the ones whose name does not -announce the situation they belong to. - -Related documents: [00 — Getting Started](00-getting-started.md) for the -everyday build/test loop, [03 — Toolchain Management](03-toolchains.md), -[10 — Publishing a Library](10-publishing-a-library.md), -[11 — Machine-Readable Output](11-machine-output.md). - -Every output below was produced by the version of mcpp this chapter ships with. - -## Reclaiming disk without forcing a rebuild - -Two stores grow, they grow for different reasons, and one command each empties -them. Confusing them costs a full rebuild. - -| Store | Scope | Grows when | Emptied by | -|---|---|---|---| -| `target///` | one project | a configuration fingerprint changes and opens a new directory | `mcpp clean`, `mcpp clean --stale` | -| the build cache (`mcpp cache dir`) | the whole machine | any project compiles a dependency or a `std` module | `mcpp cache gc`, `mcpp cache prune`, `mcpp cache clean` | - -`mcpp clean` removes `target/` entirely, and the next build recompiles -everything. `mcpp clean --stale` removes only the fingerprint directories that -no recorded build still uses, so the configurations in use survive: - -``` -$ mcpp clean --stale --dry-run -would remove target/x86_64-linux-gnu/0123456789abcdef (0.0 B) -Would remove 1 directory (0.0 B) -``` - -"In use" means recorded in `target/.build_cache`, which `mcpp build` writes and -the fast paths read. Three consequences follow from that definition: - -- A directory no record names is not deleted merely for that. `mcpp test` - builds through a path that writes no record, and so does a `--no-cache` - build. An unrecorded directory written within `--older-than` (one day by - default) is kept; older than that it goes, and the cost of being wrong is one - rebuild of a configuration nothing has touched since. -- With no record at all the command refuses rather than guess. One - `mcpp build` establishes what is current. -- Directories under `target/` that are not fingerprint directories — `dist/` - from `mcpp pack`, among others — are never visited. - -`--dry-run` lists and deletes nothing. `--stale`, `--dry-run` and -`--older-than` each select this mode: `mcpp clean --older-than 3d` is a scoped -request and is not read as a full wipe. `--older-than 0` keeps no unrecorded -directory; a negative duration is refused. - -The build cache is machine-wide, so a project-local command must not empty it — -`--stale` and `--bmi-cache` are refused together. `mcpp cache list` shows what -occupies it. The rows carry no order, and a `0.0 B (incomplete)` row is an -entry an interrupted build left behind: - -``` -$ mcpp cache list -key kind size last used package -8a150ad49d666f94 std 29.6 MiB 6d ago std gcc@16.1.0 c++23 libstdc++ -9234eed9ef786c13 std 0.0 B 2d ago std (incomplete) -``` - -`mcpp cache gc` requires `--max-size`, `--older-than`, or both, and evicts -package entries only. A `std` BMI is shared by every project on the machine, and -the implementation excludes it from size-driven eviction on the grounds that -rebuilding one trades a lot of time for a little disk. `mcpp cache clean --std` -remains the explicit way to remove it. - -## The versions a package publishes - -`mcpp search` matches a substring and appends what each hit publishes, merged -across the descriptor's per-OS tables and sorted semver-descending: - -``` -$ mcpp search imgui - compat:imgui Dear ImGui immediate-mode GUI library core sources (1.92.8, 1.92.8-docking) - mcpplibs:imgui C++23 module package for Dear ImGui core and GLFW/OpenGL3 backends (0.0.6, 0.0.5, 0.0.4, ...) -``` - -The trailing `, ...` marks truncation: three versions are shown by default, and -its absence means the list is complete. `--all-versions` prints the whole list. -A package whose descriptor cannot be read prints as two columns — the version -list is best-effort display and never fails the search. - -`mcpp add` carries the same information when a name does not resolve. The -suggestion names the namespace to write and the versions behind it: - -``` - a package with this name exists under another namespace: - compat.eui-neo (0.5.6, 0.5.5, 0.5.3) -``` - -This scan runs only after a lookup has already failed, and its result reaches -error text and search output only. A bare name never resolves across namespaces -on the strength of it. - -## Explaining a resolution - -`mcpp why` reports what a build would resolve, and builds nothing: - -``` -$ mcpp why toolchain -toolchain: gcc 16.1.0 (x86_64-linux-gnu) - abi(libc)=glibc cxxstdlib=libstdc++ arch=x86_64 os=linux triple=x86_64-linux-gnu - reason: [toolchain] in mcpp.toml if set, else platform-native default -``` - -The topic is `toolchain`, `runtime`, `deps` or `runners`, and all four report -when none is named. `--target` and `--toolchain` turn the report into a query -about a pair the current directory does not use, which is how a target matrix -asks one cell at a time. - -An error code in a diagnostic expands through `mcpp self explain`: - -``` -$ mcpp self explain E0006 -E0006: index requires a newer mcpp - -The package index declares (index.toml [index].min_mcpp) that its -descriptors need a newer mcpp than this binary — parsing them would -silently misbehave, so resolution stops instead. Upgrade mcpp: -``` - -## Index freshness and offline builds - -`mcpp index status` answers whether the local index copies are current without -touching the network: - -``` -$ mcpp index status - index state refreshed revision path - xim fresh 28s ago 1f4b39d /home/speak/.mcpp/registry/data/xim-pkgindex - mcpplibs fresh 28s ago d4b36d7 /home/speak/.mcpp/registry/data/mcpplibs -``` - -`mcpp index update` refreshes them. A package published minutes ago and still -absent after a refresh is a propagation question, not a naming one — indices -reach clients as artifacts rather than git clones. - -`--offline` (or `MCPP_OFFLINE=1`) forbids the network for one invocation and -fails rather than fetch. `--locked` fails when resolution differs from -`mcpp.lock` instead of rewriting it, which is the shape a CI job wants. -`mcpp index pin ` records a commit for a custom index in -`mcpp.toml`; `mcpp index unpin` removes it. - -## Validating a descriptor before publishing - -`mcpp xpkg parse` reads a descriptor with the resolver's own grammar, so what -it reports is what resolution will see: - -``` -$ mcpp xpkg parse mcpp.plugins.lua -package mcpp.plugins (namespace 'mcpp') -versions linux 0.1.1, 0.1.0, latest -versions macosx 0.1.1, 0.1.0, latest -versions windows 0.1.1, 0.1.0, latest -form A — no mcpp segment (build info from the source's mcpp.toml) -parse OK -``` - -The per-OS lists are printed separately on purpose: a version added to one -platform table and forgotten in the others reads as "not found" on the -platforms that lack it, against a file that contains the version string. -`--json` emits the same facts for a script: - -``` -$ mcpp xpkg parse mcpp.plugins.lua --json -{"namespace":"mcpp","name":"plugins","versions":{"linux":["0.1.1","0.1.0","latest"],"macosx":["0.1.1","0.1.0","latest"],"windows":["0.1.1","0.1.0","latest"]},"form":"A"} -``` - -`mcpp emit xpkg` generates the entry to submit. See -[10 — Publishing a Library](10-publishing-a-library.md) for the full path. - -## Environment diagnosis - -`mcpp self doctor` checks the toolchain, the `std` module, the registry, cache -health and the last runtime-closure verdict, and reports what it found rather -than only what failed: - -``` -$ mcpp self doctor - Checking toolchain - ok gcc 13.3.0 (x86_64-linux-gnu) at /usr/bin/g++ - Checking cache health - ok build cache size = 2.5 GiB -warning: pre-v1 cache at '/home/speak/.mcpp/bmi' occupies 167.5 MiB and is no longer used — `mcpp cache clean --legacy` reclaims it -``` - -`mcpp self env` prints the paths and the resolved toolchain, `--format json` -included. `mcpp self config --mirror CN|GLOBAL` selects the download mirror; -mcpp and xlings hold this setting separately, so selecting it for one does not -select it for the other. - -## Current limitations - -- `mcpp why --format json` is defined for the `toolchain` topic only. The other - topics report `'' has no machine-readable shape yet` and exit non-zero. -- `mcpp search` matches a substring; there is no field selector, and no way to - restrict a search to one namespace. -- `mcpp clean --stale` reads `target/.build_cache`, which holds a bounded number - of recent entries. A project built across more (target, profile) pairs than it - holds loses its oldest entries, and a directory whose entry has been evicted - is then treated as unrecorded — kept while it is newer than `--older-than`, - removed after that. -- `mcpp cache gc --older-than 0` is rejected with `bad --older-than value '0' - (expected {s,m,h,d})`, while `mcpp clean --stale --older-than 0` accepts it. - The two options share a parser but not this case. diff --git a/docs/16-the-target-triple.md b/docs/21-the-target-triple.md similarity index 93% rename from docs/16-the-target-triple.md rename to docs/21-the-target-triple.md index 78f5a30f..b6a74e99 100644 --- a/docs/16-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -1,4 +1,13 @@ -# The Target Triple +# 21 — The Target Triple + +**Reader:** someone building for a machine that is not this one. + +**The question this chapter answers:** how is a target named, which targets are +supported and at what tier, and which build hosts can serve each. + +**Not here:** how a manifest conditions on a target, which is +[22 — The Target Side](22-target-side.md), and targets with no operating +system, which are [40 — Bare-Metal](40-baremetal.md). A target triple is written `-` or `--`. This chapter states what each segment means, when the third may be declined, and why the @@ -19,7 +28,7 @@ the payload is chosen. **The build-time system.** The target side arrives as packages in the dependency graph and is compiled from source by whichever compiler is running. The third segment selects nothing, because the graph has already decided. This -is what [chapter 15](15-openkal-cross.md) describes. +is what [chapter 15](24-openkal-cross.md) describes. The two differ in what the third segment *does*, not in how it is spelled. A project does not declare which system it is in; the dependency graph decides, @@ -120,7 +129,7 @@ from `planned`, the lexical answer wins again with nothing to edit. is the escape hatch for opting into a row early with an explicit `[target.] toolchain`. -### Which Spelling To Use +### The Spelling To Use **Under the build-time system, decline it.** The graph supplies the C library and the runtimes, so the segment states a request that is not consulted. Under @@ -135,7 +144,7 @@ different artefacts. Writing the segment is how that choice is made. default fill, and `msvc` is a different object ABI rather than a different C library, so the segment is meaningful there in both systems. -## What The Build Reports +## The Build Report The report heads with the target as written and resolves it to the compiler's own spelling: @@ -177,7 +186,7 @@ convention the **objects follow**, which several layers must agree on. Reading it as `c++-abi libc++` is a second wrong answer, since libstdc++ sits on the same ABI. -## Three Vocabularies, And Why They Differ +## Three Vocabularies, And The Reason They Differ A triple is written by three parties that do not share a convention, and mcpp translates between them. Knowing which one a string belongs to removes most of @@ -225,7 +234,7 @@ a payload. It is also why the build-time system needs only one compiler. MinGW's own triple is `x86_64-w64-mingw32`: -| Field | Value | Why | +| Field | Value | Reason | |---|---|---| | arch | `x86_64` | | | vendor | `w64` | the project is `mingw-w64`, distinguishing it from the stalled original `mingw32` | @@ -362,7 +371,7 @@ why the refusal names openkal in its own text. **A bare-metal row's toolchain is not a convention, and neither is `x86_64-windows-musl`'s** — no gcc payload emits a PE with a musl C library, so -those rows cannot be overridden at all. See [chapter 03](03-toolchains.md). +those rows cannot be overridden at all. See [chapter 03](20-toolchains.md). ### And a dependency graph replaces that axis entirely @@ -410,7 +419,7 @@ not from `linux-aarch64`, while `aarch64-linux-gnu` is the mirror case and is `planned` on both. Collapsing them to `linux` would let one overwrite the other's rows. -### Which build host serves which target +### Build hosts and the targets they serve | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| @@ -443,7 +452,7 @@ installed by mcpp · `SDK` the platform's own · `—` unreachable from this hos ### The rule behind the columns -| target class | which build hosts serve it | why | +| target class | the build hosts that serve it | reason | |---|---|---| | `*-linux-musl` | Linux (any arch), Windows (same arch only) | the musl payloads are self-contained | | `*-linux-gnu` | Linux, same arch only | additionally needs the host-native `xim:glibc` / `xim:linux-headers` | @@ -500,11 +509,21 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", `sysroot = ""` selects the zero-libc tier: no C library on the compile line and none on the link. An absent `sysroot` key is a different answer — it inherits -the target row's own default. See [chapter 13](13-baremetal.md). +the target row's own default. See [chapter 13](40-baremetal.md). ## Reference -[chapter 14](14-target-side.md) for the five layers and who supplies them. -[chapter 15](15-openkal-cross.md) for the build-time system in full. -[chapter 03](03-toolchains.md) for the toolchain axis, which is separate: a +[chapter 14](22-target-side.md) for the five layers and who supplies them. +[chapter 15](24-openkal-cross.md) for the build-time system in full. +[chapter 03](20-toolchains.md) for the toolchain axis, which is separate: a target does not determine a compiler. + +## Current limitations + +- A row at the `planned` tier is **registered in the vocabulary and nothing is + wired yet**. Naming such a target is accepted by the parser and refused by the + build. +- `host_can_serve` decides whether this machine can produce a target, and it is + a different question from the tier. A target mcpp supports may still be + unreachable from this host; the escape hatch is an explicit + `[target.X] toolchain = "…"`. diff --git a/docs/14-target-side.md b/docs/22-target-side.md similarity index 62% rename from docs/14-target-side.md rename to docs/22-target-side.md index cc7c62a5..a323e0d7 100644 --- a/docs/14-target-side.md +++ b/docs/22-target-side.md @@ -1,4 +1,14 @@ -# The Target Side +# 22 — The Target Side + +**Reader:** an author whose one manifest must serve several targets. + +**The question this chapter answers:** how does a manifest say "only there", and +what may be conditioned that way. + +**Not here:** the vocabulary of target names, which is +[21 — The Target Triple](21-the-target-triple.md), and the accelerator axis, +which resolves after the graph and is +[42 — Heterogeneous Builds](42-heterogeneous-builds.md). A build must answer one question before it can emit a command line: where the target's compiler runtime, platform interface, C library and C++ runtime come @@ -332,3 +342,152 @@ published package. An unknown key elsewhere in a manifest is ignored. That provision governs future engines only. A package declaring a layer name still requires its consumers to run an engine no older than the release that introduced the name. + +## `[target.*]` — Platform-Conditional Dependencies & Flags + +Scope dependencies and build flags to a platform with a `[target.]` table. +The selector `` has three forms: + +| Selector | Meaning | Example | +|---|---|---| +| **bare OS alias** | a single OS / family — the concise, common form | `[target.windows]`, `[target.unix]` | +| **`cfg(...)` predicate** | a compound condition (arch / env / combinators) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | +| **exact triple** | one specific target (also carries `toolchain` / `linkage` / `sysroot` / `runner`; see [04 §2.7.3](04-mcpp-toml.md)) | `[target.x86_64-linux-musl]` | + +A selector may carry platform-conditional **dependencies** and **build flags**: + +```toml +# Concise bare-alias form — pull OpenBLAS and link it only on Windows. +[target.windows.dependencies.compat] +openblas = "0.3.33" +[target.windows.build] +ldflags = ["-Llib", "-llibopenblas"] + +# cfg(...) for compound predicates (grammar: all/any/not over os/arch/family/env, +# plus the bare aliases windows/unix/linux/macos). +[target.'cfg(all(linux, not(arch = "aarch64")))'.build] +cxxflags = ["-march=x86-64-v2"] +``` + +`[target.windows]` is exactly equivalent to `[target.'cfg(windows)']` — the bare +aliases `windows` / `linux` / `macos` / `unix` are never valid target triples, so +there is no ambiguity. Use the bare form for a single OS/family; use `cfg(...)` +for arch/env conditions and combinators. + +- **Keys**: `dependencies` / `dev-dependencies` / `build-dependencies` / + `feature-deps.` (mcpp 2026.8.6.2+ — see [30 — build.mcpp](30-build-mcpp.md); the feature is + registered unconditionally, only its dependency set is scoped), and + `build` with `cflags` / `cxxflags` / `ldflags` / `sources` (mcpp 0.0.95+ — + conditional source globs, e.g. gating `src/x86/**/*.asm` behind + `cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and + `include_dirs` / `include_dirs_after` (mcpp 0.0.102+), plus + `private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+). +- **What `build` accepts is exactly the set of *additive build inputs*** — the + things that combine by appending and are consumed after the predicate is + evaluated, which is the member list of `BuildInputs`. `linkage`, `target`, + and the profile knobs are deliberately not among them: they are *inputs to* + target selection (conditioning `target` on a predicate evaluated against + `target` is circular), or they need override-rather-than-append semantics. + A key outside the set is reported and ignored; the message lists the set it + checked against, so it cannot drift from the check. +- **Evaluated against the resolved target** — the `--target` triple for a cross + build, otherwise the host. So a native Linux build never even *downloads* a + `[target.windows]` dependency. +- **Predicate keys**: `os`, `arch`, `family`, `env` — the triple's coordinates — + and, from mcpp 2026.9.1.1, the five target-side layer names `compiler`, + `compiler-runtime`, `kernel-abi`, `c-abi`, `c++-abi` + ([22 — The Target Side](22-target-side.md)). `accelerator` is a key here too + and is answered from this build's own `accel` — the backend names in + `--accel` or `[build] accel` — so it is a membership test over a set, and + `accelerator = "none"` is how a section says "this build named no backend" + without enumerating the ones it is not. Barewords `linux` / `macos` / + `windows` / `unix` are sugar for the matching `os` / `family` test. A key + outside this set is reported as a schema warning and the section does not + apply — it used to answer false in silence, which is indistinguishable from + a section that correctly did not match. +- **A resolved-layer predicate cannot select dependencies.** A layer is + resolved *from* the dependency graph, so a dependency chosen by one would + decide the answer it is asking for. `[target.'cfg(c-abi = "musl")'.dependencies]` + is reported and ignored; the `build` inputs under the same predicate do apply. + `accelerator` is not one of these (mcpp 2026.9.6.5): it is an input to the + build rather than an answer from the graph, so + `[target.'cfg(accelerator = "cuda")'.dependencies]` applies. +- **Precedence**: an exact-triple table wins over a `cfg`/alias table; multiple + matching predicate tables have their flags concatenated. Conditional entries + are appended **after** the unconditional `[build]` ones, so under GNU + "last flag wins" a conditional rule overrides a broader unconditional one. + That is what makes a per-OS **removal** expressible: + + ```toml + [build] + flags = [{ glob = "third_party/zlib/**", defines = ["HAVE_UNISTD_H=1"] }] + + # clang-MSVC has no : undo the base define, add the windows one. + [target.'cfg(windows)'.build] + flags = [{ glob = "third_party/zlib/**", + defines = ["NO_FSEEKO"], cflags = ["-UHAVE_UNISTD_H"] }] + ``` + +- **A conditional `flags` entry that does not match the current target does not + exist at all**, so it cannot produce a "glob matched no source file" warning. + One manifest can therefore carry all three OSes' flag tables without any of + them generating noise on the other two — the same way an inactive feature's + entries simply are not there. A zero-hit glob in the *unconditional* table + still warns, because there it is a real defect. +- **`toolchain` / `linkage` / `sysroot` are exact-triple only** — they describe + one specific cross target, so put them under `[target.]` (above), not + under a bare alias or `cfg(...)`. + +### `sysroot` — the target's C library + +`sysroot` (mcpp 2026.8.20.2+) overrides the C library the target table binds to +a triple, on the same axis as `toolchain` overriding the compiler pin: one names +the compiler a target resolves, the other names its C library, and both were +engine-only until a project had a reason to disagree. + +```toml +[target.riscv64-none-elf] +sysroot = "xim:newlib-riscv@4.4" # a different C library +``` + +```toml +[target.riscv64-none-elf] +sysroot = "" # no C library at all +``` + +**An absent key and an empty one are different answers.** Absent inherits the +target table's C library. Present-and-empty is the **zero-libc tier**: no C +library is resolved, no include or library path is added, and the link carries +only what the project and its dependencies supply. `#include ` stops +resolving. A kernel or a bootloader wants exactly that, and collapsing the two +cases would silently hand such a project the target's C library back. + +The value is an xpkg reference or the empty string; a bare name is rejected when +the manifest is parsed, because accepting it would install nothing and then fail +much later naming a missing libc. + +A build program can ask which C library **payload** supplies the sysroot: +`mcpp::target_libc()` returns that package's name and +`mcpp::target_libc_profile()` the sub-directory for the target's ISA profile. +Both are empty on the zero-libc tier. See +[40 — Bare-Metal and Freestanding Targets](40-baremetal.md). + +**That is not the same question as "which C library did the target side +resolve to".** `target_libc()` names the payload mcpp installed, and that value +is an *input* to target-side resolution — a package in the dependency graph can +supply the C library instead, in which case the resolved `c-abi` is not what +this returns. To branch on the resolved layer, use a layer predicate: +`[target.'cfg(c-abi = "musl")'.build]` ([22 — The Target +Side](22-target-side.md)). This paragraph said "which C library was resolved" +until 2026.9.1.1, which was the wrong one of the two. + +## Current limitations + +- **A dependency cannot be conditioned on the accelerator.** The accelerator + layer is resolved from the dependency graph, so a dependency chosen by it would + decide the answer it is asking for. mcpp reports the predicate and ignores it; + packages are unconditional or conditioned on the platform, and `[build] + sources` is what the accelerator selects. +- A layer cannot select a dependency for the same reason, in the general case: + any derivation earlier than the graph is an inference about a fact that does + not yet exist. diff --git a/docs/23-the-project-environment.md b/docs/23-the-project-environment.md new file mode 100644 index 00000000..fe70bcd5 --- /dev/null +++ b/docs/23-the-project-environment.md @@ -0,0 +1,535 @@ +# 23 — The Project Environment + +**Reader:** an author whose build needs tools that are not the compiler. + +**The question this chapter answers:** how does a project declare the +environment its build runs in, and what does that declaration decide. + +**Not here:** what a build program does with those tools, which is +[30 — Build Programs](30-build-mcpp.md), and choosing a compiler, which is +[20 — Toolchain Management](20-toolchains.md). + +A project can declare the environment it builds in. That one declaration +decides which C library the project links against and which tools its build +programs find — so a `mcpp.toml` means the same build on a developer's laptop +and in CI, whatever else those two machines happen to have installed. + +```toml +[xlings] +subos = "tools" + +[xlings.workspace] +"xim:qemu-riscv" = "9.2.4-1" +``` + +Working project: `examples/07-project-subos/`. + +## 1. The definition of a SubOS + +A SubOS is a directory that holds a userspace: its own `bin`, its own library +view, its own installed package versions, and a `subos_info` block describing +itself. mcpp treats it as the answer to "what does this project build +against", and it is the only mechanism that answers that question — not the +compiler's path, not `XLINGS_ACTIVE_SUBOS`, not the shell. + +Two kinds exist, and the difference is where the directory lives: + +| Declaration | Directory | Shared with | +|---|---|---| +| none | mcpp's initialized `subos/default` | every project on the machine | +| `subos = "default"` | the same directory, named explicitly | every project on the machine | +| `subos = ""` | `/.mcpp/.xlings/subos//` | nothing | + +The third row is the isolated one. It belongs to the project, it sits beside +the manifest, and removing the project removes it. + +## 2. The scope of the declaration + +**The C library.** A payload-first build links against one specific glibc, and +which one is a fact about the project rather than about the machine. Chapter 8 +covers the binding, the degradation rules, and what a SubOS that does not +describe itself does to them. + +**The tools a build program sees** (mcpp 2026.8.25.1+). The declared +environment's `bin` goes to the front of the `PATH` that `build.mcpp` runs +with: + +``` +PATH=: +``` + +A build program that spells `qemu-system-riscv64` as a bare name therefore gets +the copy inside the declared environment. Chapter 7 covers the contract this +rides on. + +**Only for projects that declare one.** A project with no `[xlings].subos` +gets the `PATH` mcpp was started with, byte for byte. Putting a shared +directory in front of every project would make what a build sees depend on what +else had been installed on that machine — two projects on one machine would +agree with each other, and the same project on two machines would not. +Declaring it is what puts it there. + +**Prefixed, not replaced.** A build program legitimately calls `git`, +`python3` or a shell, and none of those live in a SubOS. Front position makes +the declared environment the default answer; everything else stays reachable +behind it. + +### 2.1 The version pins that apply (2026.9.3+) + +Naming an environment also changes where a tool's version comes from. A +project's own `[xlings.workspace]` entries always win — over the environment +here, and over a dependency's declaration by the rule in section 3 — and what +differs is what they are laid over: + +| The project declares | The version of a tool it did not name comes from | +|---|---| +| `[xlings.workspace]`, no `subos` | the machine's environment | +| `[xlings.workspace]` and `subos = ""` | that environment's own workspace; the machine's does not apply | + +The second row is what isolation means. A named environment has its own +installed set, and carrying the machine's versions into it would name versions +that are not there — so a project that relied on the machine's tools has to +declare them once it names an environment. + +An `xlings use` performed inside the project outranks both, until mcpp rewrites +the environment: it is the layer merged last, and an action a person took +should beat a file. + +## 3. Outside the declaration’s scope + +`[xlings.workspace]` names packages to be present in the environment, and each one's +payload directory is delivered separately as `MCPP_XPKG__DIR`. That is a +different question from `PATH` and stays a different answer: a build program +that needs a package's data files (protoc's well-known `.proto` files, say) +asks for the directory, and one that needs to *run* a program asks `PATH`. + +**A workspace member's declaration is not the workspace's.** In a workspace +build the workspace root owns the selection; a member's `[xlings]` applies only +when that member is built as an independent root. + +**A dependency's declaration is a different matter, and it is honoured** +(2026.9.5.4+ for `[xlings] deps`, 2026.9.6.6 for the version rule below). A +board-support package knows which emulator reaches its machine, and a rule +package knows which toolkit its rule drives; a consumer that had to repeat +either is the duplication such packages exist to remove. What the dependency +declares is installed, and `MCPP_XPKG__DIR` answers for it in that +dependency's own build program. + +Where a project and a dependency name **one package**, one version of it is +installed: identity is `(namespace, name)` and the version is a constraint on +it. The declaration nearer the artifact wins and the override is reported; +a pin that fails a requirement the other side stated is refused naming both. +See *One package, one version* in [23 — The Project Environment](23-the-project-environment.md). + +## 4. Reading an environment, never creating one + +mcpp resolves a declared name and reads what it finds. A name that does not +resolve is a hard error: + +``` +error: selected SubOS 'tools' does not exist at …/.mcpp/.xlings/subos/tools; +create/bootstrap that environment instead of falling back to active/default +``` + +Falling back to the default or to whatever is active would substitute a +different environment for the one the manifest named, which is precisely what +would make one `mcpp.toml` mean two different builds. Creating and populating +a SubOS is xlings' layer — `xlings subos new` — and mcpp managing SubOS state +would invert that layering. + +An environment that exists but carries no `subos_info` block **degrades rather +than fails**: the runtime binding reports `inconclusive`, no payload-first +binding is available, a note is printed, and the build continues. Chapter 8 +gives the full rule. + +## 5. The case for a private environment + +- **A generator whose version changes what it emits.** `protoc`, `flatc`, a + shader compiler: the output is an input to everything downstream, so the + project pins the producer instead of hoping the machine has a compatible one. +- **An emulator a build program runs.** Several bare-metal packages boot an + artefact under QEMU as part of proving it works; which QEMU is part of what + was proven. +- **A project whose CI and developer machines differ**, where neither is wrong + and the build must not notice. +- **Two projects on one machine that need different versions of one tool.** + Sharing a directory means one of them loses; a private environment means the + question does not arise. + +Against that: an isolated environment is a directory that has to be created and +populated, and the first build pays for it. Since 2026.8.29 mcpp does that +work — a declared `[xlings.workspace]` entry is provisioned on first use, and a named +`[xlings] subos` that does not exist yet is created rather than refused — but +the cost is real: the first build on a clean machine downloads and installs +before it compiles anything. A project whose tools are ordinary and whose +versions do not matter is better off declaring nothing and inheriting the +machine's. + +Under `--offline` / `MCPP_OFFLINE` or `MCPP_NO_AUTO_INSTALL`, mcpp refuses +instead of installing, and names the packages so they can be provisioned +out of band — the same two knobs `[toolchain]` honours, for the same reason: an +unasked-for download is not something a build decides on a project's behalf. + +The declaration is provisioned on every host that builds the project, and a +package the host cannot install is an error, not a skipped entry. A tool that +exists for one host platform only is therefore declared for that platform +(2026.9.2.1): `deps = [{ linux = "qemu-user-aarch64" }]` declares the emulator +on Linux and nothing elsewhere. The keys and the resolution rule are in +this chapter. + +**Which verbs install it.** An entry may name a tier — +`{ version = "0.24.0", when = "run" }` — and a `[feature-xlings.]` +table gates one on a feature. A tool the project will not use is then not +downloaded: this chapter. Omitting the tier is the historical behaviour. + +**The runner.** A program under `[xlings.workspace]` is also where +`[target.].runner` looks first for its first element, before `PATH` +([04 §2.7.3](04-mcpp-toml.md)). The two keys together provision a user-mode emulator on a +CI host and execute a cross-built artifact through it, without the manifest +naming the payload's path. + +## 6. Declarations that belong elsewhere + +| Need | Declared in | +|---|---| +| a library the program links | `[dependencies]` | +| the compiler | `[toolchain]`, chapter 3 | +| a host tool a dependency produces | `tools = [...]`, chapter 7 | +| a tool present in the environment | `[xlings.workspace]` | +| a tool only one verb or one feature needs | `when = "run"`, `[feature-xlings.]` | +| which environment | `[xlings] subos` | + +## 7. `[xlings]` — the manifest keys + +```toml +[xlings.workspace] # what this project's environment contains +cmake = "3.28" +"xim:picolibc-riscv" = "1.8.12" # a namespaced package - quotes required +code = "" # present; version unconstrained +llvm = { macosx = "20", default = "22" } +``` + +```toml +[xlings] +subos = "dev" # a named, isolated environment +``` + +`[xlings]` is mcpp's surface for **xlings' local project mechanism**: the +project `.xlings.json` that gives a directory its own environment. The +subsection names and their meanings are that file's, and mcpp materializes them +into `/.mcpp/.xlings.json` with no translation layer. + +**`[xlings.workspace]` is the one table.** An entry names a package and the +version this project uses it at. mcpp provisions it — installing it when the +machine does not have it, mapping it when it does — and materializes it as a +resolution pin, so the version the project named is the one its tools resolve +to. + +### Writing an entry + +| Form | Means | +|---|---| +| `cmake = "3.28"` | that version | +| `llvm = "22"` | the highest installed `22.*`; a version prefix resolves | +| `code = ""` | present, version unconstrained | +| `"xim:picolibc-riscv" = "1.8.12"` | a package from the `xim` index | +| `llvm = { macosx = "20", default = "22" }` | per host platform | + +**A namespaced package is written `":" = ""`, and the +quotes are required** — a TOML bare key cannot contain a colon. That is the +recommended form and the one every official package uses: an entry names a +package and then says which version of it, so the namespace belongs to the +name. + +The namespace is also accepted on the version (`picolibc-riscv = "xim:1.8.12"`), +because that is what the materialised `.xlings.json` carries — a key there is an +xvm target and the scope qualifies the version. Two vocabularies, one entry. +Writing it on both halves with different values is an error, and so is naming +one package twice under two spellings. + +Platform keys are xlings' own — `linux`, `macosx`, `windows` — plus `default`. +`macos` and `macosx` are the same platform written in two vocabularies (mcpp's +triples say one, descriptors and xlings' project file say the other) and both +are accepted wherever a platform is named. A table with no key for this host +and no `default` declares nothing here. + +### Two resolution axes — host and target (mcpp 2026.9.6.4+) + +A tool entry answers one of two different questions, and the table it is written +in decides which: + +| Written | Axis | Resolved against | +|---|---|---| +| `[xlings.workspace]`, platform keys in the value | host | the machine running the build | +| `[target..xlings.workspace]` | target | the resolved target (`--target`, else the host) | + +Both are correct spellings and neither supersedes the other. A tool that +executes on the build machine belongs on the host axis; a payload the produced +code is compiled or linked against belongs on the target axis. + +```toml +[xlings.workspace] +"xim:dpcpp" = "7.1.0" # a compiler, and it runs here + +[target.'cfg(os = "linux")'.xlings.workspace] +"xim:glibc" = "" # what the device units are compiled against +"xim:linux-headers" = "" +``` + +On a native build the two axes name the same platform, so a project that states +target facts on the host axis is right by accident and keeps working. It stops +being right the first time that project is cross-compiled. **For anything the +produced code is compiled or linked against, the target axis is the recommended +form.** + +`[target..feature-xlings.]` composes the condition with the +gate, exactly as `[target..feature-deps.]` does: the selector +says which targets, the feature says whether at all. + +```toml +[target.'cfg(os = "linux")'.feature-xlings.backend-vulkan] +"xim:shaderc" = "2026.3" +``` + +**A selector here must not name a RESOLVED layer.** `c-abi`, `c++-abi`, +`compiler`, `compiler-runtime` and `kernel-abi` are answered by dependency +resolution, which happens after tools are installed and after build programs +run. A tool conditioned on one would be declared and never installed — a build +that succeeds with the tool simply absent — so such a manifest is refused, +naming both the tool and the predicate. Condition it on the target, or gate it +on a feature: `[feature-xlings.]` is known before anything is +provisioned. + +**`accelerator` is the exception, and is admitted** (mcpp 2026.9.6.5). It is +not resolved from anything: it is `--accel`, or `[build] accel`, read before the +first package is looked up. A payload predicated on it is merged in the same +pass as a triple predicate and installed like any other. + +```toml +[target.'cfg(accelerator = "cuda")'.xlings.workspace] +"xim:cuda-nvcc" = "12.9.86" +"xim:cuda-cudart" = "12.9.79" +``` + +This is the form a project with a device island should use. Without it the +vendor toolkit is declared unconditionally or not at all, so `mcpp build` with +no accelerator — the cheapest build, and the one CI usually runs — downloaded +gigabytes for a device it was not compiling for. + +The same rule governs dependencies: `[target.'cfg(accelerator = "cuda")'.dependencies]` +is honoured, while a dependency conditioned on a resolved layer is not, because +that one would decide the answer it is asking for. Nothing about the +accelerator is circular. + +The selector is the only place the condition is written. A value under a +selector that also carries platform keys states one fact twice, and is refused +naming both halves: + +``` +[target.cfg(os = "linux").xlings.workspace] xim:tool: the value carries platform +keys (linux, macosx), but [target.cfg(os = "linux")] already says which targets +this applies to. +``` + +`subos` is not conditional on a target: a project has one environment, so +`[target..xlings]` refuses the key rather than dropping it. + +**A published descriptor carries no edge for a target-axis entry**, and +`mcpp publish` says so. A descriptor has one block per platform, and a selector +is not a platform — `cfg(target_arch = "aarch64")` names no block that file has. +What a CONSUMER of the package gets installed comes from the top-level +`[xlings.workspace]`; the target axis stays correct for what the package's own +build compiles against. + +See [SPEC-004](specs/manifest-semantics.md) for the general rule these two axes +are an instance of. + +### `when` — the verbs that need this tool (mcpp 2026.9.4.2+) + +```toml +[xlings.workspace] +"xim:qemu-arm" = "9.2.4-1" # every build, as before +"xim:codegen" = { version = "1.0", when = "build" } +"xim:probe-rs" = { version = "0.24.0", when = "run" } +"xim:clang-tidy"= { version = "20", when = "dev" } +``` + +Package dependencies have had this axis since the beginning — +`[dependencies]`, `[build-dependencies]`, `[dev-dependencies]`. Tools had one +list, so a board-support package that named both an emulator and a debug probe +installed both for every consumer, including one that only wanted the library +to compile. + +| `when` | Installed by | Reaches a consumer | +|---|---|---| +| *(omitted)* | every verb that builds | yes | +| `build` | every verb that builds | yes | +| `run` | `mcpp run`, `mcpp test` | yes | +| `dev` | only the package that declared it, as the root | **no** | + +**Omitting `when` is the pre-2026.9.4.2 behaviour exactly**, so no manifest has +to change. Narrowing is optional; it is not a question an author has to answer. + +`dev` is the only tier that does not propagate. It means *"while the package +that declared this is itself being developed"*, so a dependency's `dev` entry is +never installed for a consumer. Every other tier does reach one, which is the +point of a board package knowing its own machine: it declares the emulator once +and every consumer gets it. + +The tier is written on the entry rather than as a second table, on the same +reasoning that makes `[dependencies]` accept both `dep = "1.0"` and +`dep = { version = "1.0", features = [...] }`. A scoped entry must name +`version` even to leave it empty (`version = ""` means *present, any version*), +because `{ when = "run" }` and a misspelt `version` key would otherwise be +indistinguishable. + +### `[feature-xlings.]` — a tool a feature needs + +```toml +[features] +default = ["emulator"] +emulator = {} +hardware = {} + +[feature-xlings.hardware] +"xim:probe-rs" = "0.24.0" +``` + +The same table, gated on a feature, spelled the way `[feature-deps.]` +is. A consumer who never asks for `hardware` never downloads a probe driver. +Entries here accept `when` exactly as the unconditional ones do. + +A feature name no `[features]` table declares is reported as a schema warning: +it activates for nobody and installs nothing, and a tool whose absence is only +visible as *"the device is never reachable"* is the hardest kind to diagnose. + +### A rule package brings its own environment (2026.9.6.6+) + +The table above is what a project writes when it has an opinion. Most projects +have none, and nothing is what they write: + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.3.0", features = ["rules-cuda"], host-module = true } +``` + +That one edge is the whole declaration. The rule package names the packages its +rule needs and the version it needs them from, under the feature that selects +it and the accelerator it is for: + +```toml +# in the rule package, not in your project +[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] +"xim:cuda-nvcc" = ">=12.9.86" +"xim:cuda-cudart" = ">=12.9.79" +``` + +Two gates, and both must open. The feature says *whether this rule is wanted*; +the selector says *which builds actually download it*. A CPU-only build of the +same project opens neither and installs nothing. + +Which package, and how old it may be, is the rule author's knowledge. Repeating +it in every project that uses the rule is a copy that goes stale silently — the +rule moves and the projects do not. + +### One package, one version (2026.9.6.6+) + +A tool address is `[:][@]`, and its **identity is the +`(namespace, name)` pair**. The version is a constraint on that package, never +part of its name, so `xim:glibc`, `xim:glibc@2.40` and `xim:glibc@>=2.38` all +name one package. One version of it is installed per build. + +Which one is decided in two steps. + +**Adjudication — the declaration nearer the artifact wins.** Your project +outranks a package it depends on, so a pin overrides a rule's requirement: + +```toml +# the project, when it does have an opinion +[target.'cfg(accelerator = "cuda")'.xlings.workspace] +"xim:cuda-nvcc" = "13.3.33" +``` + +A declaration that names no version abstains: it says the package is wanted and +nothing about which version, so it cannot outrank a floor merely by being +nearer. When two declarations disagree and both name a version, mcpp reports +which was used — an override visible only as *"two versions were declared and +one directory exists"* is a fact the reader has to reconstruct from the +filesystem. + +**Validation — the winner must satisfy every requirement that lost.** `>=`, +`^`, `~` and comma-combined forms are requirements. A pin that fails one is +refused, naming both sides: + +``` +error: `xim:cuda-nvcc` is pinned to 12.0.0 by this project, and mcpp:plugins + requires >=12.9.86. + One version of a package is installed, so the two cannot both hold. + fix: pin a version satisfying >=12.9.86, or drop the pin and let the + requirement decide. +``` + +A bare version is a *choice*, not a requirement: two exact pins that differ are +adjudicated and reported, not refused. Only a stated requirement can be +violated. + +**This is a comparison, not a search.** The version is chosen by adjudication +and then checked, so mcpp never has to ask the index which versions exist and +carries no constraint solver. The cost is stated rather than hidden: a +combination a solver could satisfy — a project's `>=8.0`, a rule's `8.5.0`, and +8.3 as the newest in the index — is refused instead, and the refusal says how to +proceed. + +Ranges are resolved in both directions. `>=2026.1` installs the highest +published version satisfying it, `>=2099.1` is refused as unsatisfiable, and +`mcpp::xpkg_dir` answers with the highest **installed** version satisfying the +range — a rule that declares a floor can find what the floor brought in. + +### Which version a tool the project did not name resolves to + +| The project declares | The version comes from | +|---|---| +| `[xlings.workspace]`, no `subos` | the machine's environment, with the project's own entries laid over it | +| `[xlings.workspace]` and `subos = ""` | that environment's own workspace; the machine's does not apply | +| neither | the machine's environment | + +The middle row is not an omission. A named environment has its own installed +set, and carrying the machine's versions into it would name versions that are +not there. Naming one is how a project asks for isolation; leaving it out is +how it asks for the machine's environment with its own entries on top. + +An `xlings use` performed inside the project outranks this table until mcpp +rewrites the environment, because it is the layer merged last. + +### `deps`, superseded + +`deps = ["xim:qemu-riscv@9.2.4-1"]` is the pre-2026.9.3 spelling of the same +statement. It is still honoured and is reported once, with the +`[xlings.workspace]` line to write instead. It is not refused, because a +refusal would reach a *dependency's* manifest, which a project that pinned an +exact version of that package cannot edit. + +### `envs`, removed + +`[xlings.envs]` was materialized into `.xlings.json` and read by nothing: a +program's environment is declared by its own package, and an environment's by +that environment. The key is now an error naming both. Nothing in the index +used it. + +## 8. Related chapters + +- [30 - build.mcpp](30-build-mcpp.md) — the contract a build program receives, + including the `PATH` it runs with. +- [91 - Toolchain Internals](91-toolchain-internals.md) — runtime selection, + the `RuntimeBinding` snapshot, and the degradation rules. +- [04 - mcpp.toml](04-mcpp-toml.md) — the rest of the manifest. + +## Current limitations + +- **A tool cannot be conditioned on the accelerator.** The accelerator is + resolved after the dependency graph, so such a tool would be declared and never + installed — a build that succeeds with the tool simply absent. A manifest that + writes one is refused. +- The declaration nearer the artifact wins, and the override is reported. A pin + that fails a requirement the other side stated is refused naming both sides + rather than installed alongside it. diff --git a/docs/15-openkal-cross.md b/docs/24-openkal-cross.md similarity index 95% rename from docs/15-openkal-cross.md rename to docs/24-openkal-cross.md index 67d7a870..14ac6f31 100644 --- a/docs/15-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -1,4 +1,13 @@ -# Cross-Compilation Over openkal +# 24 — Cross-Compilation Over openkal + +**Reader:** someone cross-building for another operating system from this one. + +**The question this chapter answers:** how does one source tree build for +several hosted targets without the machine having their toolchains. + +**Not here:** targets with no operating system, which are +[40 — Bare-Metal](40-baremetal.md), and how targets are named, which is +[21 — The Target Triple](21-the-target-triple.md). Conventional cross-compilation is served by a payload. A toolchain is built for one target, its driver has exactly one answer, and reaching a second target @@ -65,7 +74,7 @@ preprocessor directive is required in the source. A worked example is A project names the last of these. The others follow from its dependencies. -## Why The Compiler Must Be LLVM +## The Reason The Compiler Must Be LLVM `openkal-llvm-runtime` declares the requirement rather than leaving it to be discovered: @@ -86,7 +95,7 @@ With the requirement declared, the build refuses the combination before it compiles anything, and names the command that selects a compiler which satisfies it. -## How The Target Is Chosen +## Target Selection The target row of mcpp's own vocabulary may carry a toolchain convention. That convention names the payload which supplies **that target's C library**, and it @@ -207,7 +216,7 @@ runner = ["qemu-aarch64-static"] Without the key, `mcpp run` reports the kernel's refusal (`Exec format error`) and the key to write, and `mcpp test` reports every test as not run and exits 2. A host that executes the artifact natively passes `--no-runner`. The rules -are in [5 — mcpp.toml](05-mcpp-toml.md), §2.7.3. +are in [04 — mcpp.toml](04-mcpp-toml.md), §2.7.3. ### The Source Is The Same, The Program Is Not @@ -266,7 +275,7 @@ execution contexts, traps, per-CPU state and address spaces — presented as one interface over several instruction sets, with a backend package per instruction set. A kernel depends on it and supplies its own platform layer, or none. -### Why x86_64 Bare Metal Required Engine Work +### The Engine Work x86_64 Bare Metal Required `riscv64-none-elf` and `aarch64-none-elf` are rows in a table and nothing more: Clang has a BareMetal toolchain for both, drives their links itself and reaches @@ -304,6 +313,7 @@ the paths where a project overrides the contract explicitly. ## Reference -[docs/14 — The Target Side](14-target-side.md) for the five layers, the four +[docs/14 — The Target Side](22-target-side.md) for the five layers, the four origins and the rules. [SPEC-002](specs/target-side.md) for the normative statement of the capability grammar. + diff --git a/docs/07-build-mcpp.md b/docs/30-build-mcpp.md similarity index 77% rename from docs/07-build-mcpp.md rename to docs/30-build-mcpp.md index bcbc508d..56e7b919 100644 --- a/docs/07-build-mcpp.md +++ b/docs/30-build-mcpp.md @@ -1,6 +1,16 @@ -# `build.mcpp` — a native build program +# 30 — Build Programs: `build.mcpp` -**English** | [简体中文](zh/07-build-mcpp.md) +**English** | [简体中文](zh/30-build-mcpp.md) + +**Reader:** an author whose build needs a step mcpp has no rule for — code +generation, an embedded asset, a check, a second compiler. + +**The question this chapter answers:** how do I add work to the build graph, so +that it is ordered, fingerprinted and incremental like everything else. + +**Not here:** packaging that step so other projects can use it, which is +[31 — Authoring a Rule Package](31-authoring-a-rule-package.md), and the tools +the step runs, which are [23 — The Project Environment](23-the-project-environment.md). Most projects need nothing more than `mcpp.toml`. When build-time logic is required — probe the host, generate a source, decide a flag from the environment — put a @@ -279,7 +289,7 @@ the first one sees. and is empty on a hosted target, which is exactly the case this pair exists for. Either of these two is empty when mcpp passes no such flag. -### Which C++ standard library resolved: `cxx_stdlib` (2026.9.6.3+) +### The resolved C++ standard library: `cxx_stdlib` (2026.9.6.3+) ```cpp const char* impl = mcpp::cxx_stdlib(); // "libstdc++" | "libc++" | "msvc-stl" | "" @@ -345,7 +355,7 @@ could not state a floor and every project repeated its rule's package list. version this build actually installed rather than the one the local manifest wrote. One package means one version: where a project and a rule both name it, the declaration nearer the artifact wins and both sides are told the same -answer. See *One package, one version* in [05 — mcpp.toml](05-mcpp-toml.md). +answer. See *One package, one version* in [23 — The Project Environment](23-the-project-environment.md). **`[feature-xlings.]` is answered too, while `` is active** *(2026.9.6.2+)*. That table has provisioned its packages since it existed -- @@ -376,7 +386,7 @@ mcpp builds that `kind = "bin"` target **for the build machine** (even under `--target`), caches it globally, and returns the path. The request lives in `mcpp.toml` rather than here for the same reason a dependency does: asking the graph for an extra artifact is a graph-level request, and the graph stays -statically analysable. See [05 §2.14](05-mcpp-toml.md) for the full contract, +statically analysable. See *Host tools from a dependency* in this chapter for the full contract, including `[tools.overrides]` and `reexport = true` (which is how a library provides the whole toolchain, so a project declares **one** dependency instead of four). @@ -524,7 +534,7 @@ link the command would never run and the build would say nothing. > should not be used for anything the build produces: ldflags is a flat string > in the link command, not a file in the graph, so nothing tracks it and editing > it reports `ninja: no work to do`. For Windows resources specifically, use -> [`[resources]`](05-mcpp-toml.md) — +> [`[resources]`](04-mcpp-toml.md) — > `object` is the escape hatch for everything else. **You must name the output files.** mcpp fixes the source set, the fingerprint @@ -701,10 +711,10 @@ measured on `qemu-system-riscv64`, where the answer was a shim that reports "is not installed in this subos" when executed, while a working copy sat in the project's own environment and was not on `PATH` at all. -The selection is the one [chapter 8](08-toolchain-internals.md) already +The selection is the one [chapter 8](91-toolchain-internals.md) already describes — the same declaration that decides which C library the project links against, delivered to one more consumer. See -[chapter 17](17-the-project-environment.md) for what a declared environment is +[chapter 17](23-the-project-environment.md) for what a declared environment is and when to want one; `examples/07-project-subos/` is a working project. ## Writing a rule package @@ -712,7 +722,7 @@ and when to want one; `examples/07-project-subos/` is a working project. A rule — "run protoc over these `.proto` files", "run clang-tidy over these sources" — belongs in a package, not copy-pasted into every consumer's `build.mcpp`. The mechanism is -[`host-module = true`](05-mcpp-toml.md); this section is about the shape of +[`host-module = true`](04-mcpp-toml.md); this section is about the shape of what goes inside. The guidance below generalises from `mcpplibs.grpcgen`, the first such package, @@ -764,7 +774,7 @@ under the package name, so `export module mcpp.rules.spirv;` is what a consumer then imports. Official plugins live in one package, `mcpp:plugins` (repository `mcpp-community/mcpp-plugins`): rule packages are named `mcpp.rules.`, build-time utilities `mcpp.tools.`, and each member is selected by a feature -of that package (see [`host-module = true`](05-mcpp-toml.md)). `mcpp.build.*` +of that package (see [`host-module = true`](04-mcpp-toml.md)). `mcpp.build.*` is the engine's own module family and is not used for plugins. The engine cannot tell who is official, so it keys the check on the package *namespace* and warns when the two disagree — @@ -807,7 +817,7 @@ knobs produced the command — and that belongs in each edge's `description`. **Failure and advice use different channels.** mcpp prints what it captured from a build program only when the program exits non-zero, so a failure writes to stderr and returns non-zero. A message that must be seen on a *successful* -build has to go through [`mcpp::warning`](#warning--succeeding-and-still-being-heard-2026821-2); +build has to go through [`mcpp::warning`](#warning--succeeding-and-still-being-heard-20268212); stderr on success is discarded, which means the wrong channel is silent on exactly the builds that needed the message. @@ -886,13 +896,304 @@ explicit input/output contract — incremental builds stay correct. When nothing changed the output is `build.mcpp up to date (cached)`; otherwise `build.mcpp compiling` / `running`. -## Notes & limits +## Host tools from a dependency (mcpp 2026.8.5.1+) + +A package can build a binary its consumers need *at build time* — `protoc`, a +`grpc_cpp_plugin`, `flatc`, `moc`, a transpiler. Ask for it on the dependency: + +```toml +[dependencies] +protobuf = { version = "35.1", tools = ["protoc"] } +grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] } +``` + +Each name must be a `kind = "bin"` target of that package. mcpp builds it **for +the build machine** and hands `build.mcpp` its absolute path as +`MCPP_DEP__BIN_` — read it with `mcpp::dep_bin("protobuf", "protoc")` +(see [30 — build.mcpp](30-build-mcpp.md)). + +Four properties worth knowing: + +- **Always a host binary.** Under `mcpp build --target ` the tool is + still built for *this* machine, because a code generator has to run here. It + is a separate, host-targeted sub-build — the tool package's own `[toolchain]` + and its own dependency resolution apply, and none of it has to agree with + the consuming build. That is safe precisely because an executable has no ABI + contact with the consuming code. +- **One version axis.** The tool's version *is* the dependency's version, so + a `protoc` that does not match its runtime is not expressible. (This is the + problem with packaging the tool separately, and it is the failure mode that + bites at run time rather than compile time.) +- **Default off.** Nothing is built unless someone asks; the cost is the + consumer's to pay. A package gates the expensive part with + `[features]` + `required_features` (protobuf's `protoc` needs libprotoc's + ~157 extra TUs, which the runtime's users must not compile). +- **Cached globally**, keyed on package version × host toolchain × features × + its own dependency closure — built once per machine, not once per project. + +**The key holds no source content, and for a `path` dependency that is visible.** +A published version is immutable, so for a tool that arrives from an index the +key is exact. A tool being edited next door has the same version from one build +to the next, and the cached binary stays: measured on +[`examples/12-a-new-device-language`](../examples/12-a-new-device-language/), +a change to the tool's emitter left `mcpp run` printing the previous answer, +while bumping the tool package's version rebuilt it and changed the artifact. +Bump the version, or empty the build cache with `mcpp cache clean` — the tool +store lives inside it, at `/tool//@/`. + +This is a gap in the rebuild, not in the tracking. An action that declares the +tool among its inputs does re-run when that file's bytes change, measured by +overwriting the binary in the store: the artifact followed. What does not happen +is the rebuild that would change those bytes. + +### `[tools.overrides]` — use an existing binary + +```toml +[tools.overrides] +"compat.protobuf:protoc" = "/usr/bin/protoc" +``` + +or, without editing the manifest (CI, distro packaging): + +```bash +MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build +``` + +An override **skips the build entirely**. Every comparable system provides this +escape hatch (vcpkg's `VCPKG_HOST_TRIPLET`, CMake's `LLVM_NATIVE_TOOL_DIR`, +Qt's `QT_HOST_PATH`), and for the same reason: a tool that cannot be built from +source on this machine must not be a dead end. It is deliberately **not** part +of the cache key — an override is an escape hatch, not a reproducible input. + +### `host-module = true` — reusable build rules as packages + +A rule (say "run protoc over these `.proto` files") should be written once, not +copy-pasted into every consumer's `build.mcpp`. Ship it as an ordinary mcpp +library package and import it: + +```toml +[dependencies] +protobufgen = { version = "0.1.0", host-module = true } +``` + +```cpp +// build.mcpp +import mcpp; +import protobufgen; +int main() { return protobufgen::generate({"schema"}) ? 0 : 1; } +``` + +mcpp compiles that package's lib-root module **for the host, in the same +command as `build.mcpp`** — which is what makes the BMI usable at all, since a +module interface is only importable by a compile that agrees with it on +standard, dialect and compiler identity. + +Rules are therefore versioned, testable and distributable through the package +manager already in use, written in **C++** — no second language, which is the +whole point of `build.mcpp` existing. + +**The module name is what the rule's source declares** (mcpp 2026.8.29.1+). +`export module acme.rules.protobuf;` is imported as `acme.rules.protobuf`, +whatever the package is called. Module names are authored API and do not mirror +package identity — the rule ordinary library packages have always followed. + +Until 2026.8.29.1 the host-module path registered the bare `package.name` +instead, which made a divergent name build under GCC and fail under Clang and +MSVC: GCC's BMIs are implicit under `gcm.cache` and keyed by the declared name, +while the other two are handed an explicit `=` mapping. Package names +carry no C++ naming constraint as a result, and `grpc-rules` is a legal package +name again. + +**Two rules may not declare one module name.** `import` addresses the module, +so two such packages are indistinguishable to the compiler, and their BMIs and +objects share a filename — the second overwrites the first and the surviving +object reaches the link twice. mcpp refuses this, naming both packages and both +interface paths. The check covers the rules one `build.mcpp` can see; it is not +an index-wide uniqueness rule, which `path` dependencies and private registries +would escape anyway. + +**`mcpp.` is reserved for rules maintained by the mcpp project.** A module name +under that prefix from a package outside the `mcpp` namespace produces a +warning naming both, and the build proceeds. It is a warning because the engine +cannot decide who is official: a `path` dependency, a private mirror and an +internal fork are all legitimate and indistinguishable from here. + +The lib root must be at `src/.cppm` (or wherever `[lib] path` points); a +missing one is reported as *"host module 'x': no interface unit at …"*. + +**A package may offer several rules, selected by features** (mcpp 2026.9.5.3+). +Every module interface unit among the package's resolved `[build] sources` — +including the sources a feature adds — is compiled as a host module under the +name it declares, the lib root first. A feature unit may import the lib root; +units are otherwise compiled alone, so they import `std`, `mcpp` and nothing +else. Only listed sources take part: the inferred `src/**` of a package that +declares no `sources` is not consulted, so a rule package published before this +release exposes exactly what it exposed then. + +```toml +# the collection's manifest +[build] +sources = ["src/plugins.cppm"] # export module mcpp.plugins; + +[features] +rules-cuda = { sources = ["rules/cuda.cppm"] } # export module mcpp.rules.cuda; +rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spirv; +``` + +```toml +# a consumer +[build-dependencies.mcpp] +plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } +``` + +**`[build-dependencies]`, not `[dependencies]`** — a rule package is the case +[04 §2.6.1](04-mcpp-toml.md) describes exactly: its library must never reach the target while its +rule is still wanted. The two axes are separate, so `host-module = true` says +*which build-time product* is wanted and the section says *whether the package +reaches the target*; a rule package answers "no" on the second axis, and the +section is where that is said. Written in `[dependencies]` it still works, and +that is precisely why the distinction has to be stated rather than enforced by +a failure. + +The module set is the feature set: a unit whose feature is not active is not +compiled, and importing it fails as an unknown module. `mcpp:plugins` is the +collection the mcpp project maintains (repository `mcpp-community/mcpp-plugins`); +its members are named `mcpp.rules.` for rule packages and `mcpp.tools.` +for build-time utilities. + +*Build-time only:* a `host-module = true` dependency is **not** compiled into +or linked with the target, and neither is anything it depends on. It exists to +run during `build.mcpp` and nowhere else. (Before 2026.8.5.2 it was also built +as an ordinary library, which made `import mcpp;` inside a rule fail: the +bundled module does not exist in that second compile. Until 2026.8.29.1 the +rule itself was excluded but its own `[dependencies]` were not, so they were +compiled and linked into the consumer's binary while the rule could not import +them.) + +### A rule that depends on another rule (mcpp 2026.8.29.1+) + +A rule declares what it needs in its own `[build-dependencies]`, and may import +any entry there marked `host-module = true`: + +```toml +# inside the rule package's manifest +[build-dependencies] +globbing = { path = "../globbing", host-module = true } +``` + +```cpp +// the rule's own interface +export module tidyrule; +import std; +import mcpp; +import globbing; +``` + +mcpp compiles the inner rule first, in the same command and with the same +flags, so BMI agreement stays structural rather than checked. + +The consumer may **not** import `globbing`: build-time provisions cross one +further edge only on a `reexport = true` edge, and mcpp enforces that rather +than leaving it to the compiler, which on GCC would allow the import and then +fail on someone else's machine. + +*Limit:* one interface unit per host module. A library with implementation +units or several modules cannot yet be a rule's build dependency. + +### `reexport = true` — a library standing up a toolchain for its user (2026.8.6.2+) + +Everything above is declared by whoever *uses* the tool. That is the wrong +place when the knowledge belongs to a library: gRPC's code generation needs +protobuf's `protoc`, and no user of a gRPC package should have to know that. + +`reexport = true` hands an edge's build-time provisions — its `tools`, its +`host-module`, and the dependency's directory — to **this package's own +consumers**: + +```toml +# inside the grpc package's manifest +[feature-deps.codegen] +"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } +grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"], reexport = true } +grpcgen = { version = "1.83.0", host-module = true, reexport = true } +``` + +Its user then writes one line, and imports the rule: + +```toml +[dependencies] +grpc = { version = "1.83.0", features = ["codegen"] } +``` + +```cpp +// build.mcpp +import mcpp; +import grpcgen; +int main() { return grpcgen::generate_all() ? 0 : 1; } +``` + +- **Off by default, and deliberately not the edge's `visibility`.** `visibility` + already defaults to `"public"`, so riding it would let any dependency at any + depth put entries into the build program's tool namespace without saying so. + Handing something to consumers is a supply-chain statement; it has to be + written down. +- **One hop per declaration.** A re-exported provision reaches the consumers of + the package that declared it. For it to travel further, the next package must + re-export in turn — each package decides only what *it* hands on. +- **A feature may add a request to an already-declared dependency.** gRPC + depends on protobuf unconditionally and its `codegen` feature adds + `tools = ["protoc"], reexport = true` to that same edge. `tools` and + `features` union, `host-module` and `reexport` OR together; `version` / + `path` / `git` do not merge, so a feature still cannot silently override the + unconditional entry's identity. +- **Visibility, not execution.** `dep_bin()` returns a path; whether anything + runs is still the consumer's `build.mcpp`'s decision. Nothing changes about + who builds the tool or how the tool store is keyed. +- **Unqualified names are resolved by a ladder, not by luck.** Once two + libraries can re-export, both may offer the tail `protobuf`. The + fully-qualified `MCPP_DEP___BIN_` is always published; the + bare spelling is bound to `mcpplibs.`, else `compat.`, else an + unnamespaced ``, else the single remaining candidate — and when it is + contested mcpp says so instead of picking silently. + +#### Older mcpp reading a manifest that uses this + +An unrecognized dependency key is reported as a **degradation** and ignored +(mcpp 2026.8.6.2+), so a package written for a newer mcpp still loads and the +parts this reader understands still apply. Before that release it was a hard +load failure with a misleading message, which is why a published package could +not adopt a new key at all — the same property the index floor establishes: +data must not decide whether the program works. + +Consequently a package that *relies* on `reexport` for its ergonomics still +needs a client new enough to implement it; what changed is that everything else +about that package keeps working on an older one. + +#### Scoping a provision per platform + +A package may declare a `bin` target on some platforms only. Because the +*library* now decides what is requested, an unconditional request turns an +unsupported platform into an error its user cannot edit away. Scope it: + +```toml +[target.'cfg(not(windows))'.feature-deps.codegen] +"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } +``` + +`[target..feature-deps.]` (2026.8.6.2+) follows the same rules as +the other conditional dependency tables ([22 — The Target Side](22-target-side.md)). The **feature itself is +registered on every platform** — only what it pulls in is conditional — so +requesting it where no predicate matches is not an unknown-feature error. + + + +## Current limitations - **Runs on the host — including under cross** (mcpp 0.0.95+). Under `mcpp build --target ` the program is compiled with a host-resolved toolchain, runs on the host, and sees `MCPP_TARGET` = the cross triple. For purely declarative target gating, `[target.'cfg(...)']` tables remain - the first choice — see [05 - mcpp.toml Manifest Guide](05-mcpp-toml.md). + the first choice — see [04 - mcpp.toml Manifest Guide](04-mcpp-toml.md). - **CWD is the project root**, so relative paths (`src/generated.cpp`) land where expected. - A non-zero exit from `build.mcpp` aborts the build and prints its output. diff --git a/docs/31-authoring-a-rule-package.md b/docs/31-authoring-a-rule-package.md new file mode 100644 index 00000000..6e68733e --- /dev/null +++ b/docs/31-authoring-a-rule-package.md @@ -0,0 +1,331 @@ +# 31 — Authoring a Rule Package + +**Reader:** an ecosystem author packaging a build step so that other projects +can use it — a device language, a shader compiler, a generated interface, a +check. + +**The question this chapter answers:** how does a package supply a rule, and +what does a consumer have to write to use it. + +**Not here:** adding a step to one project's own build, which is +[30 — Build Programs](30-build-mcpp.md) and is the same primitives at a smaller +scale; the feature keys themselves, which are +[06](06-features-and-capabilities.md); and the spellings of the shipped rules, +which belong to `mcpp:plugins`. Examples: +[`08-build-rules`](../examples/08-build-rules/) checks and embeds; +[`12-a-new-device-language`](../examples/12-a-new-device-language/) adds a +language. + +## The extension model + +mcpp's build surface is extended from packages rather than from releases. Five +points do the extending, and the engine holds no name that comes through any of +them. + +| extension point | effect | declared on | +|---|---|---| +| `mcpp::action` | one edge in the build graph: a command with declared inputs and outputs | a build program, or a rule module it imports | +| `device_extensions` | an extension the engine classifies as a **device source** instead of refusing it | a feature of a package | +| `rule_module` | the module a consumer's build program imports to reach the rule | the same feature | +| `tools = [...]` | a generator or compiler **built from source for the build machine**, reached with `mcpp::dep_bin` | a dependency edge | +| `[xlings]`, `[feature-xlings]` | a prebuilt tool the rule runs, installed on demand | the package, or one of its features | + +What the ecosystem has built out of them: + +| surface | package | points used | +|---|---|---| +| CUDA, HIP, SYCL, Ascend C | `mcpp:plugins`, one feature each ([42](42-heterogeneous-builds.md)) | actions driving a vendor compiler, plus payloads gated on the accelerator | +| Slang | `mcpp:plugins`' `rules-slang` | `device_extensions = [".slang"]` — the first language mcpp supports without naming it in the engine | +| GLSL and HLSL to SPIR-V, and the module over the result | `mcpp:plugins`' `rules-spirv` | one action per shader, plus a generated module | +| the island boundary between a device and C++ | `mcpp.tools.island` | a generator, plus `mcpp::generated` | +| an asset as a linkable object | [`08-build-rules`](../examples/08-build-rules/) | `role = "object"` | +| a check that can fail the build | [`08-build-rules`](../examples/08-build-rules/) | `role = "check"` | +| a language the engine has never heard of | [`12-a-new-device-language`](../examples/12-a-new-device-language/) | `device_extensions`, plus a compiler built through `tools = [...]` | + +### The shapes the model expresses + +**A new language, whatever compiles it.** A rule claims the extension, submits +one action per source, and declares the compiler among that action's inputs. +The compiler may be a vendor toolkit, an LLVM front end, an interpreter that +emits a device binary, or a program the rule package builds from source. Whether +it produces a device binary, an object or C++ is the action's `role` and nothing +else. The engine never learns the language: it learns that an extension is a +device source and that an action claims it. + +**Preprocessing and code generation.** An action with `role = "source"` produces +C++ that the declaring package then compiles, and every compile edge of that +package waits for it. The input can be a template, an interface definition, a +table, or another action's output — chaining is ordinary, because actions are +ordered and fingerprinted by their files. + +**A file that is partly C++ and partly another language.** Classification +happens before any rule runs, so a file with an extension the engine owns is +compiled as C++ and never reaches a rule. A source carrying a foreign block +therefore uses an extension the rule claims, and the rule splits it: the C++ it +extracts goes through `role = "source"`, the foreign half through its own +compiler, and the seam between the two is the `extern "C"` boundary of +[42 — Heterogeneous Builds](42-heterogeneous-builds.md). No package in this +repository ships that shape today. + +### The boundary + +**A declaration cannot reclassify what the engine already owns.** A dependency's +`device_extensions` is consulted *after* the built-in roles, so a rule package +cannot claim `.cpp`, `.cppm`, `.c` or `.S`. Those are the engine's own +vocabulary, and a package must not be able to move a file out of it. Claiming +one is not diagnosed and has no effect: measured by adding `".cpp"` to a rule's +`device_extensions`, after which the consumer's `main.cpp` was still compiled as +C++ and the build succeeded. + +**Module-interface extensions are the project's axis, not a rule's.** A project +that spells its interfaces `.ixx` declares `[build] module_extensions` +([04 — The mcpp.toml Project File](04-mcpp-toml.md)). No rule-package key adds +one, because a module interface is scanned for imports, produces a BMI and joins +the link — three engine behaviours rather than a command to run. + +**An extension in neither table is refused by name**, which is why a mistyped +`device_extensions` surfaces at once instead of dropping a source: + +``` +error: scanner errors: + .../orphan.zzz: 'orphan.zzz' is listed in [build] sources, and mcpp has no role + for the extension '.zzz'. + Its object would be compiled and then linked by nothing, so this is refused rather + than built. +``` + +## The definition of a rule package + +Three parts, and none of them is special to mcpp: + +| part | content | +|---|---| +| a package | an ordinary `mcpp.toml` with a version and a licence | +| a module | a `.cppm` exporting `options` and a function that submits build edges | +| a feature | the switch that selects it, and the place its own dependencies hang | + +The engine holds no rule names and no vendor names. A consumer names the +package, activates a feature, and the build program imports the module. + +## The manifest: three keys + +```toml +[features] +default = [] + +[features.rules-toy] +sources = ["src/rules-toy.cppm"] +rule_module = "example.rules.toy" +device_extensions = [".toy"] +``` + +| key | effect on a consumer | +|---|---| +| `sources` | the module is compiled only when the feature is active | +| `rule_module` *(2026.9.7.1+)* | the module the consumer's build program imports. It implies `host-module = true` | +| `device_extensions` *(2026.9.7.1+)* | those extensions are classified as **device sources**: never scanned for imports, never producing a BMI, and refused if no action claims them | + +A rule that generates or checks rather than compiling a language declares no +`device_extensions`; `08-build-rules` is that shape and its consumer writes +`host-module = true` on the dependency edge itself. + +Device extensions are not in the default source glob. A consumer opts in by +naming the files: + +```toml +[build] +sources = ["src/*.cpp", "src/kernels/*.toy"] +``` + +## The module a consumer imports + +The convention every shipped rule follows is an `options` struct with defaults, +and a `compile` (or `generate`) function that submits the edges: + +```cpp +export module example.rules.toy; +import std; +import mcpp; + +export namespace example::rules::toy { + +struct options { + std::string out_dir = std::string(mcpp::out_dir()); + std::string rule_dir = std::string(mcpp::dep_dir("rules-toy")); +}; + +bool compile(options opt = {}); + +} +``` + +A rule that plans its edges in one function and submits them in another gives a +consumer a way past its last knob without hand-writing the action: +`08-build-rules`'s `plan` / `submit` pair is that shape. + +**A rule takes the extensions it claims and leaves the rest.** +`mcpp::device_sources()` is one string, one package-root-relative path per line, +and it holds the package's **whole** device set. A build with two backends puts +both backends' sources in that one list, and every rule in the build program +reads the same value. + +## Declaring work: `mcpp::action` + +```cpp +mcpp::action a; +a.id = id.c_str(); // stable, unique within the package +a.role = "source"; +a.description = desc.c_str(); +a.arg("sh").arg(script.c_str()).arg(input.c_str()).arg(output.c_str()); +a.input(input.c_str()); +a.input(script.c_str()); +a.output(output.c_str()); +a.submit(); +``` + +The strings must outlive the action. `a.id = ("toy:" + stem).c_str()` hands it a +pointer into a temporary that is gone by `submit()`. + +### The four roles + +`role` decides where the edge's outputs go and when the edge runs. + +| `role` | outputs | ordering | +|---|---|---| +| `source` | compilable ones join the compile set | every compile edge of the declaring package waits for them | +| `check` | a stamp file | runs alongside compilation; `blocking = true` makes compiles wait | +| `object` | join the **link** set | the link edge consumes them | +| `artifact` | a new file | its inputs are link outputs, so it runs after the link | + +### Declared inputs, and the compiler among them + +An action re-runs when a declared input changes. **The tool the command invokes +is an input.** Without it, editing the rule's own compiler leaves every edge +clean and the artifact keeps the bytes the previous compiler produced — a green +build over a stale result. + +An action's command runs from the **build directory**, not from the package +root. `mcpp::device_sources()` answers package-root-relative, so a rule joins +`mcpp::manifest_dir()` to each path before putting it on a command line. + +### A depfile, when the command discovers its own inputs + +A shader or a kernel that includes another file has inputs the rule cannot +enumerate. Every compiler involved emits a depfile — +`glslangValidator --depfile`, `glslc -MD -MF`, `slangc -depfile`, and +`-MD -MF` for the clang-family drivers: + +```cpp +a.depfile = dep.c_str(); // a path the command writes +a.arg("--depfile").arg(dep.c_str()); +``` + +The depfile must not also be declared as an `output()`. + +### Chaining actions + +One action may consume what another produced. The engine orders and fingerprints +them, and that is the whole engine-side content of a device link: N `artifact` +actions whose outputs stay out of the link, and one `object` action that reads +them and produces the object that joins it. + +## The environment a rule brings with it + +A rule owns the list of packages it drives, because it is the code that runs the +compiler and puts the library directory on the link line. + +```toml +[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] +"xim:cuda-nvcc" = "12.9.86" +"xim:cuda-cudart" = "12.9.79" +``` + +Two gates, and both must open before a byte is downloaded: the feature says +whether the rule is wanted, and the `cfg(accelerator = ...)` selector says +whether this build compiles for the device. A build with no accelerator opens +neither. + +A bare version is a **choice** a project may override; `>=` is a +**requirement** a project may not go below. See *One package, one version* in +[23 — The Project Environment](23-the-project-environment.md). + +## Generating an island's boundary + +A device translation unit cannot import a module, so the boundary between it and +the C++ side is an `extern "C"` header. `mcpp.tools.island` from `mcpp:plugins` +reads the entry points marked `MCPP_EXPORT_C` out of both implementations and +writes that header and a module over it, so each signature exists once. + +```cpp +mcpp::tools::island::options opt; +opt.module_name = "myapp.kernels"; +opt.out_dir = std::string(mcpp::out_dir()) + "/island"; + +const auto entries = mcpp::tools::island::scan(halves, opt); +const auto out = mcpp::tools::island::emit(*entries, opt); +mcpp::generated(out->interface_file.c_str()); +``` + +Four rungs are available, and each overrides the one above: + +| rung | written by hand | the consumer writes | +|---|---|---| +| L0 | nothing but the marked entry points | `import myapp.kernels` — the island's own C-shaped interface | +| L1 | a seam module over the generated one | `import myapp.saxpy` — the interface the project designed | +| L2 | a seam, plus the entry list passed to `emit` directly | the same, for entry points a scan cannot see | +| L3 | the header and the module | the same, with the signature written twice | + +The generated header reaches the island through +`mcpp::tools::island::force_include_flags`, whose flags go to the **rule** that +drives the device compiler rather than through `mcpp::cxxflag` — forcing a +header into every C++ translation unit puts declarations ahead of a module +interface's `export module` line, which is ill-formed. + +[`examples/09-heterogeneous/boundary`](../examples/09-heterogeneous/boundary/) +is L0 and states what each rung costs. + +## Reporting what the build should know + +### `warning` — succeeding and still being heard + +`mcpp::warning` is the channel for a rule that finished its job and found +something worth saying: a host compiler it had to choose, a payload it fell back +to. A build program's output is otherwise printed only on a non-zero exit. + +### `fact` / `floor` — the probe channel + +A rule measures, and the engine compares before anything is compiled. + +```cpp +mcpp::fact("cuda.driver", driver_version); +mcpp::floor("cuda.driver", runtime_needs); +``` + +The engine reads a name, a relation and a version; the name is data flowing +through it. A probe that reaches no answer states none. + +## Finding the rule's own files + +| object | accessor | +|---|---| +| the rule package's own tree | `mcpp::dep_dir("")` — under the name the **consumer** declared in `[dependencies]` | +| a payload declared under `[xlings.workspace]` | `mcpp::xpkg_dir("")` | +| a host tool built from a dependency | `mcpp::dep_bin("", "")` | + +`dep_dir` answers under the spelling in the consumer's manifest. A rule that +ships files beside its module exposes the directory as an option so a consumer +that declares the edge under another key can supply it, and refuses with a +message naming what it looked for rather than running a command with an empty +path. + +## Current limitations + +- A rule feature that is in the package's own `[features] default` does not + imply `host-module`. `mcpp:plugins` declares `default = []`; a rule package + whose rule is on by default is refused with a message naming the module and + the key to add. +- `mcpp emit xpkg` writes `manifest = "mcpp.toml"` into the `mcpp` segment, and + `mcpp xpkg parse` reports that key as unknown and exits 1. No descriptor in + `mcpp-index` uses it (0 of 218); a package keeping its own `mcpp.toml` omits + the `mcpp` field entirely. See *Current limitations* in + [09 — Commands by Scenario](09-commands-by-scenario.md). diff --git a/docs/32-authoring-a-payload.md b/docs/32-authoring-a-payload.md new file mode 100644 index 00000000..1c86104b --- /dev/null +++ b/docs/32-authoring-a-payload.md @@ -0,0 +1,139 @@ +# 32 — Authoring a Payload + +**Reader:** someone packaging a tool or a prebuilt library so that mcpp projects +can declare it and mcpp installs it. + +**The question this chapter answers:** what is an `xim:` payload made of, and +what must its descriptor say for a consumer to name it and get a working +program. + +**Not here:** publishing a **source package** others `import`, which is +[11 — Publishing a Library](11-publishing-a-library.md); making a **host** +library reachable from an artifact, which is +[33 — Authoring a Runtime Adapter](33-authoring-an-adapter.md); and consuming a +payload, which is [23 — The Project Environment](23-the-project-environment.md). + +Before: [31 — Authoring a Rule Package](31-authoring-a-rule-package.md), whose +rules declare the payloads they drive. After: +[33 — Authoring a Runtime Adapter](33-authoring-an-adapter.md). + +## The definition of a payload + +Everything mcpp installs and does not compile: a compiler, a shader compiler, a +device toolkit, an emulator, a probe driver, a prebuilt C library. A project +names one in `[xlings.workspace]`, or a rule package names it in +`[feature-xlings.]`, and mcpp provisions it before the build runs. + +A payload lives in `xim-pkgindex` as one Lua file: a `package` table that +describes it, and two functions that place and register it. + +The shortest one that works: + +```lua +package = { + spec = "2", + name = "glslang", + description = "Khronos reference GLSL/ESSL front end and validator", + licenses = {"BSD-3-Clause", "Apache-2.0", "MIT"}, + type = "package", + archs = {"x86_64"}, + + xpm = { + linux = { + ["latest"] = { ref = "15.1.0" }, + ["15.1.0"] = { + url = { + GLOBAL = "https://github.com/…/glslang-15.1.0-linux-x86_64.tar.gz", + CN = "https://gitcode.com/…/glslang-15.1.0-linux-x86_64.tar.gz", + }, + sha256 = "87167c9cb32f258addbedb607639b2c1f484c029ba91542a92f19ead21d65d13", + }, + }, + }, +} + +function install() + local dir = pkginfo.install_dir() + os.tryrm(dir) + os.mv("glslang-15.1.0", dir) + return true +end + +function config() + xvm.add(package.name) + return true +end +``` + +`install()` places the extracted tree where mcpp will look for it; `config()` +registers what the payload offers. Everything else in this chapter is one of +those two doing more. + +## The four things a descriptor must get right + +**One version, two URLs.** Every version carries a `GLOBAL` and a `CN` URL and +one `sha256`. The two mirrors serve the same bytes; a consumer behind either +mirror resolves the same hash, and a descriptor with one URL is unusable for +half the ecosystem. + +**`latest` is a reference, not a version.** `["latest"] = { ref = "15.1.0" }`. +It is what a consumer gets when it names no version, and moving it is a +deliberate act — a consumer pinned to `15.1.0` is unaffected. + +**`archs` and the platform table are what a resolution reads.** A payload +published only for `linux` and `x86_64` says so, and a consumer on another +platform is refused by name rather than handed something that will not run. + +**Dependencies are `xim:` addresses with floors.** + +```lua +deps = { "xim:gcc-runtime@>=15", "xim:glibc@>=2.38" }, +``` + +## Making the payload reachable + +A payload that only unpacks is not usable. Three declarations turn a directory +into something a build can consume. + +**A program on the path.** `xvm.add(package.name)` registers the payload's +`bin/` so mcpp can find the program by its bare name. A rule package should +**name the program, not a path** — mcpp searches the `bin/` of every declared +payload and then `PATH`, and can report exactly which directories it searched. + +**Libraries a consumer will link or load.** + +```lua +exports = { + runtime = { libdirs = { "lib" } }, +}, +``` + +`elfpatch` reads this from each dependency and writes the consumer's `RPATH`, +which is what makes a stack of payloads resolve without anyone setting +`LD_LIBRARY_PATH`. + +**Headers, so a compiler in this environment can build against it.** +`sysroot.declare_libs(...)` and the header declaration place the payload into +the SubOS sysroot view. **Declared rather than copied** — xlings removes them +with the package, and a copy would outlive its owner. + +## The tier: the commands that need a payload + +```toml +"xim:qemu-arm" = { version = "9.2.4-1", when = "run" } +``` + +`when` is a second, independent gate beside the feature that selects the +payload. The feature says **who** needs the tool; the tier says **when**. An +emulator is needed to run and not to compile, so a CI job that builds firmware +and never flashes it downloads nothing. + +## Current limitations + +- A payload is published to `xim-pkgindex`, which is a separate repository with + its own review; nothing in mcpp publishes one. +- `latest` and "the highest version in the table" are two different questions, + and a consumer that wants the newest published version names `latest`. +- A payload's own CI cannot verify that a consumer resolves it: that is what a + sandbox check against the published descriptor is for, and it is the only + thing that verifies the published bytes rather than the working tree. diff --git a/docs/33-authoring-an-adapter.md b/docs/33-authoring-an-adapter.md new file mode 100644 index 00000000..0abe6b20 --- /dev/null +++ b/docs/33-authoring-an-adapter.md @@ -0,0 +1,92 @@ +# 33 — Authoring a Runtime Adapter + +**Reader:** someone making a library the **host** supplies reachable from an +mcpp-built artifact on Linux — a graphics driver, a Vulkan ICD, a proprietary +runtime. + +**The question this chapter answers:** why an artifact cannot see a library that +is plainly installed on the machine, and what a package has to declare to fix +it. + +**Not here:** packaging a library mcpp installs, which is +[32 — Authoring a Payload](32-authoring-a-payload.md) and is the right answer +whenever the library *can* be redistributed; and the runtime contract's fields, +which are [04 — The mcpp.toml Manifest](04-mcpp-toml.md) §`[runtime]`. + +Before: [32 — Authoring a Payload](32-authoring-a-payload.md). After: +[34 — Authoring a Board-Support Package](34-authoring-a-bsp.md). + +## The failure this exists for + +The library is installed. The loader finds its manifest. The `dlopen` fails +anyway: + +``` +DRIVER: Found the following files: /usr/share/vulkan/icd.d/lvp_icd.json … +ERROR: libvulkan_lvp.so: cannot open shared object file +``` + +The libraries are in `/usr/lib/x86_64-linux-gnu`. What cannot reach them is the +**process**: an mcpp-built binary runs under mcpp's own glibc, with its own +search path, + +``` +interp: …/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2 +rpath : …/xim-x-glibc/2.39/lib64:…/xim-x-gcc/…/lib64:$ORIGIN +``` + +so a bare-soname `dlopen` from inside that process does not search the host's +library path at all. Nothing is broken; the artifact is simply not looking +there, which is the property that makes an mcpp build reproducible in the first +place. + +## The definition of an adapter + +**A symlink farm plus the metadata that makes it reachable.** Nothing is +vendored, nothing is redistributed, and the package carries no upstream bytes. +`runtime.library_dirs` puts a package-owned directory of symlinks on the +artifact's runtime search path, and the chain resolves. + +A project declares the adapter as an ordinary dependency and does nothing else. + +## The reason the driver itself is not a package + +A proprietary driver's userspace is in **ABI lockstep with a kernel module**, +and its licence forbids redistribution. Neither is a packaging problem that +effort solves, so such a driver is modelled as a **host capability** — something +the machine either has or does not — and the adapter is how an artifact reaches +it. + +An **open** driver is a different case and takes the other answer: it is a +payload (`xim:mesa-lavapipe` for the CPU, `xim:mesa` for AMD hardware), and a +machine using one needs nothing from the farm. Since 2026.09.05 an adapter also +prefers the payload when one is published and its symbol set covers the host +copy, so what the farm actually records is proprietary userspace and packaging +backlog. + +## The three things an adapter gets wrong + +**The pattern list must cover transitive dependencies.** The whole chain has to +resolve through the same directory. Mesa's software rasteriser pulls in LLVM; +an NVIDIA driver pulls its own family. Listing the ICD alone produces the same +`cannot open shared object file` one level down. + +**`libstdc++` belongs in the list, and it is not an oversight.** mcpp links +libstdc++ **statically** — it is absent from a built binary's `NEEDED` — so a +`dlopen`ed C++ driver has nothing to resolve against unless the host copy is +provided here. + +**Nothing may be required.** A machine with no such driver at all is a +legitimate configuration, and every CI runner in this ecosystem is one. The farm +is then empty and the program reports what it actually found. An adapter that +errors on a missing host library turns a supported configuration into a build +failure. + +## Current limitations + +- **Linux only, by construction.** macOS's dyld and the Windows PE loader have + no equivalent layer, so a project targeting them declares no adapter. +- An adapter cannot make a driver work that the machine does not have. It + removes one obstacle — reachability — and reports the rest as absence. +- The farm's contents are decided when the adapter is installed. A driver + installed afterwards is not picked up until the adapter is reinstalled. diff --git a/docs/34-authoring-a-bsp.md b/docs/34-authoring-a-bsp.md new file mode 100644 index 00000000..3bd11f6d --- /dev/null +++ b/docs/34-authoring-a-bsp.md @@ -0,0 +1,133 @@ +# 34 — Authoring a Board-Support Package + +**Reader:** someone bringing up a board so that projects can target it with +`mcpp build` and reach it with `mcpp run`. + +**The question this chapter answers:** what does a board-support package supply, +and how does one package serve both an emulator and the physical board. + +**Not here:** using a BSP, which is +[40 — Bare-Metal and Freestanding Targets](40-baremetal.md); the runner +mechanism a consumer sees, which is [41 — Reaching a Device](41-devices.md); +and packaging the emulator or the probe driver themselves, which is +[32 — Authoring a Payload](32-authoring-a-payload.md). + +Before: [33 — Authoring a Runtime Adapter](33-authoring-an-adapter.md). + +## The contents of a BSP + +A freestanding target has no operating system, so everything a hosted program +gets for free has to come from somewhere. A BSP is that somewhere, and it +supplies **the whole target world**: + +| | content | +|---|---| +| the memory map | a linker script — the one fact a program can neither derive nor guess | +| the startup code | what runs before `main`, and the vector table | +| an exported module | what the program imports to reach the board's console and peripherals | +| **the runner** | how `mcpp run` and `mcpp test` reach the board at all | + +The runner is the one that is easy to leave out, and without it every consumer +writes its own emulator invocation. + +## One package, two environments + +A board reached through an emulator and the same board reached through a debug +probe differ **in the argv of their runners and in nothing else**. The linker +script, the startup code, the memory map and the exported module are the same +board. Publishing two packages to vary four strings duplicates all of it and +lets the copies drift. + +So the environment is a **feature**: + +```toml +[features] +default = ["emulator"] +emulator = {} +hardware = {} + +[feature-xlings.emulator] +"xim:qemu-arm" = { version = "9.2.4-1", when = "run" } + +[feature-xlings.hardware] +"xim:probe-rs" = { version = "", when = "run" } +``` + +**`emulator` is the default, and that is a decision about who is reading.** +Someone meeting the package has no board on their desk; someone who does has a +reason to say so. A default that required hardware would make the first command +fail for everyone who has not bought anything yet. + +The two tables are symmetric on purpose: neither environment is the engine's +idea of normal, and a consumer downloads exactly what the feature it selected +needs. + +**And both are on the `run` tier, which is a second and independent gate.** The +feature says *who* needs the tool; the tier says *when*. Compiling firmware needs +neither — only reaching the board does — so a CI job that builds and never +flashes downloads nothing at all. + +## The C library is a feature too + +Every `thumb*-none-eabi*` row carries an empty C-library column, so a project +targeting one begins with **no libc** unless it asks. A BSP stays there: it +references no C library symbol, and its console goes through semihosting rather +than through `stdio`. + +```toml +libc = {} + +[feature-deps.libc] +picolibc.picolibc = "1.8.12.3" +``` + +A C library arrives as a **source** package compiled with the program's own +flags, so there is no multilib to match and no ABI convention to get wrong. +`mcpp run --features libc` is the whole of it, and not selecting it leaves the +zero-libc tier exactly as it was. + +## The build program + +```cpp +int main() { + mcpp::link_script("cortex-m.ld"); + mcpp::rerun_if_changed("cortex-m.ld"); + + const std::string target = mcpp::target() ? mcpp::target() : ""; + if (mcpp::has_feature("hardware")) { + for (auto a : {"probe-rs", "run", "--chip", "STM32L475VG"}) + mcpp::runner(a); + mcpp::run_exclusive(); + } else { + /* the emulator's argv for THIS target */ + } + return 0; +} +``` + +**Which machine models a target is a table, not a default.** An image built for +`thumbv6m` does not run on a model that implements `thumbv7em`, and a wrong +guess boots and then faults somewhere unrelated. A target with no row is an +**error that names it**, not a silent absence — a BSP that configured no runner +would leave `mcpp run` reporting a missing runner and advising a `runner` key, +which is true in general and not the cause here. + +**Name the program, not a path.** mcpp searches the `bin/` of every payload +declared by any package in the graph, then `PATH`, and reports exactly which +directories it searched. An absolute path computed from an install directory +introduces a failure mode where the lookup returns empty and nothing says why. + +**The engine knows none of the runner names.** `flash`, `serve` and `erase` are +this package's vocabulary; a different package supplies `serve` meaning +something else, and neither has to be known to mcpp. + +**One probe, one user.** `mcpp::run_exclusive()` states that this target's runs +cannot overlap, and `mcpp test` then serialises them. A project never has to +remember `-j1`. + +## Current limitations + +- A BSP declares its emulator and its probe driver as payloads, so a board + whose tooling is not published for a platform cannot be reached from it. +- The machine table is per-triple. A board that needs a model the table does not + carry is a change to the BSP, not a project-side override. diff --git a/docs/13-baremetal.md b/docs/40-baremetal.md similarity index 96% rename from docs/13-baremetal.md rename to docs/40-baremetal.md index 9a62ca5c..aa2c38c6 100644 --- a/docs/13-baremetal.md +++ b/docs/40-baremetal.md @@ -1,13 +1,23 @@ -# 13 — Bare-Metal and Freestanding Targets +# 40 — Bare-Metal and Freestanding Targets + +**Reader:** an embedded developer targeting a machine with no operating system. + +**The question this chapter answers:** how does a project build for a +freestanding target, what supplies the target's world, and what of the standard +library survives. + +**Not here:** running and testing the image, which is +[41 — Reaching a Device](41-devices.md), and hosted cross-compilation, which is +[24 — Cross-Compilation Over openkal](24-openkal-cross.md). This document describes how mcpp builds, runs and tests software for targets with no operating system underneath, and how a board-support package supplies the parts of such a target that the engine deliberately does not know. -Related documents: [05 — mcpp.toml Manifest Guide](05-mcpp-toml.md) §2.7.2 is -the reference for the `[target.]` keys used here; [07 — build.mcpp -Build Program](07-build-mcpp.md) is the reference for the directive protocol a -board-support package speaks; [08 — Toolchain Internals](08-toolchain-internals.md) +Related documents: [04 — mcpp.toml Manifest Guide](04-mcpp-toml.md) §2.7.2 is +the reference for the `[target.]` keys used here; [30 — build.mcpp +Build Program](30-build-mcpp.md) is the reference for the directive protocol a +board-support package speaks; [91 — Toolchain Internals](91-toolchain-internals.md) covers the hosted link model this chapter departs from. ## Overview @@ -505,10 +515,13 @@ picolibc present: a cold build links, and `nm` finds one definition. ### Running tests on the target -`mcpp test` builds one image per `tests/*.cpp`, runs each under the emulator -the board-support package supplies, and reads the exit code as the verdict. -Semihosting propagates the firmware's `main` return value to the emulator's -exit code, so the model is identical to a hosted test run. +Testing works here exactly as [08 — Testing](08-testing.md) describes it, and +this section adds only what is specific to a board: each `tests/*.cpp` becomes +its own image, and the runner the board-support package supplies executes it. + +What makes the verdict work is semihosting, which propagates the firmware's +`main` return value to the emulator's exit code. That is why the model is +identical to a hosted run rather than merely similar to one. ```bash mcpp test @@ -632,7 +645,7 @@ The key is not specific to bare metal. A hosted cross target — an `qemu-aarch64-static` in place of the system emulator; on such a target an absent runner is not an error until the kernel refuses the artifact. The rules for hosted targets, the `--no-runner` escape and the not-run reporting of -`mcpp test` are in [5 — mcpp.toml](05-mcpp-toml.md), §2.7.3. +`mcpp test` are in [04 — mcpp.toml](04-mcpp-toml.md), §2.7.3. ## Writing a board-support package @@ -671,7 +684,7 @@ if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) { Without that line the build succeeds, configures no runner, and `mcpp run` reports a missing runner with advice about writing a `runner` key — true in general, and not the cause here. See `mcpp:warning=` in -[07 — build.mcpp](07-build-mcpp.md). +[30 — build.mcpp](30-build-mcpp.md). ### The directives a board-support package emits @@ -798,3 +811,4 @@ targets, but that expectation is **not** covered by a test. | C library substitution | Expressible since 2026.8.20.2 through `[target.].sysroot`, and **verified only for the empty value** (the zero-libc tier). Pointing it at a different C library is accepted and installed through the same channel, but no second bare-metal C library is published, so that path is untested. | | `qemu-riscv` on `win32-arm64` | The upstream package publishes no asset for that host, so installation fails on it. The failure is correct rather than silent, but the host cannot run a bare-metal image. | | Ecosystem CI breadth | The two ecosystem packages run their own CI on `ubuntu-24.04` only. mcpp-index's `tests/examples/` workspace members run unconditionally on three platforms with no capability gate, so a package requiring an emulator and a target sysroot cannot be added there. This is a known coverage gap. | + diff --git a/docs/18-devices.md b/docs/41-devices.md similarity index 82% rename from docs/18-devices.md rename to docs/41-devices.md index ce9acd40..5dedf1ae 100644 --- a/docs/18-devices.md +++ b/docs/41-devices.md @@ -1,14 +1,24 @@ -# 18 — Reaching a Device +# 41 — Reaching a Device + +**Reader:** anyone whose artifact does not run on the machine that built it — +a board, an emulator, a remote host. + +**The question this chapter answers:** how is such an artifact run and tested, +and what does a package supply so that one command reaches it. + +**Not here:** how the target itself is built, which is +[40 — Bare-Metal](40-baremetal.md), and what mcpp considers a test, which is +[08 — Testing](08-testing.md). This document specifies how mcpp executes an artifact that runs somewhere other than the machine that built it, how a package supplies additional ways of reaching it, and how a project selects between an emulator and physical hardware. -Related documents: [13 — Bare-Metal and Freestanding Targets](13-baremetal.md) -covers the targets this most often applies to; [07 — build.mcpp](07-build-mcpp.md) -is the reference for the directive protocol a package speaks; [11 — Machine -Output](11-machine-output.md) is the interface a debugger client or IDE uses. +Related documents: [40 — Bare-Metal and Freestanding Targets](40-baremetal.md) +covers the targets this most often applies to; [30 — build.mcpp](30-build-mcpp.md) +is the reference for the directive protocol a package speaks; [50 — Machine +Output](50-machine-output.md) is the interface a debugger client or IDE uses. ## One command, and named exceptions @@ -45,7 +55,7 @@ and `logcat` are equally unknown to it: it knows only that a package may supply named runners, and performs the argv it finds. A fixed set of names in the engine would decide, in the engine, which domains are expressible. -## What a package supplies +## The package’s contribution ```cpp mcpp::runner("qemu-system-arm"); // the default: argv token by token @@ -68,7 +78,7 @@ failure mode — a declaration is not an install, so the lookup can return empty and leave no runner configured with nothing said about why. Naming the program lets mcpp report exactly which directories it searched. -## What a project overrides +## The project’s overrides ```toml [target.thumbv7em-none-eabihf] @@ -162,3 +172,13 @@ This required no engine mechanism. The engine reads runners and knows nothing about emulators or probes; `mcpp::has_feature` already existed. That the question is answerable without adding anything is the layering working as specified. + +## Current limitations + +- **Exactly one dependency may supply a given runner name.** A second is an + error naming both packages; there is no ordering rule that picks a winner. +- A runner the selected environment does not supply **stays absent**. Under an + emulator feature there is no debug probe, so `mcpp run --runner debug` reports + that no such runner exists and lists the ones that do. +- Termination is declared, not inferred. A long-lived runner that does not say + so is waited on until the operator ends it. diff --git a/docs/20-heterogeneous-builds.md b/docs/42-heterogeneous-builds.md similarity index 95% rename from docs/20-heterogeneous-builds.md rename to docs/42-heterogeneous-builds.md index 20419087..ba30b6a1 100644 --- a/docs/20-heterogeneous-builds.md +++ b/docs/42-heterogeneous-builds.md @@ -1,10 +1,20 @@ -# 20 — Heterogeneous Builds +# 42 — Heterogeneous Builds + +**Reader:** someone compiling part of a program for a GPU or an AI accelerator. + +**The question this chapter answers:** how does device code get compiled and +linked into an ordinary program, and how does a prebuilt artifact state which +devices it can run on. + +**Not here:** writing the rule that drives a device compiler, which is +[31 — Authoring a Rule Package](31-authoring-a-rule-package.md), and reaching +the device to run on it, which is [41 — Reaching a Device](41-devices.md). GPU and AI accelerator targets, and mixed host/device compilation: how mcpp builds device code, and how a prebuilt artifact states which devices it can run on. -## What is supported +## The support surface A build names the device backends it targets, and it may name **several**: @@ -31,7 +41,7 @@ Vulkan/SPIR-V and Ascend C -- and the table under "The lanes" says which compiler and which payloads each drives. Nothing in the engine holds a vendor name, so a sixth is a package rather than an engine change. -## Two shapes, and why one mechanism reaches both +## Two shapes, and the mechanism common to both Accelerator toolchains come in two shapes. They describe how a toolchain is normally used, not how many mechanisms a build system needs. @@ -107,7 +117,7 @@ and less clearly. The table above is what mcpp knows **without being told**: the languages whose support shipped before a package could declare one. A rule package adds to it, through `[features]..device_extensions` (see -[05 — mcpp.toml](05-mcpp-toml.md) §2.8), and that is how a NEW device language +[04 — mcpp.toml](04-mcpp-toml.md) §2.8), and that is how a NEW device language arrives -- with no engine change and no engine release. Slang is the first: `.slang` is not in the list above, and `mcpp:plugins`' `rules-slang` declares it. @@ -240,7 +250,7 @@ object rather than in an array, and there is then nothing to take the size of. The command that invokes a device compiler is not built into mcpp. It is supplied by a **build-rule package**, consumed with `host-module = true`, which emits build-graph edges whose outputs join the link. See -[07 — build.mcpp](07-build-mcpp.md) for the mechanism and +[30 — build.mcpp](30-build-mcpp.md) for the mechanism and `examples/09-heterogeneous/cuda` for a working CUDA rule. The division is deliberate. mcpp owns the graph, the artifact's identity and @@ -293,7 +303,7 @@ the machines it will run on: The nearer declaration wins, one version is installed, and mcpp says which. A pin that does not satisfy the rule's floor is refused naming both sides rather than installed alongside it. See *One package, one version* in -[05 — mcpp.toml](05-mcpp-toml.md) for the full rule; +[23 — The Project Environment](23-the-project-environment.md) for the full rule; `examples/09-heterogeneous/multi-backend` is the one example in this repository that takes the override path, and every other one writes only the edge. @@ -331,7 +341,7 @@ version is insufficient for CUDA runtime version"*. The rule reads the driver's version through the driver's own library (reached through the sentinel package, never through `/usr/lib`) and states it as a fact; it states the floor its runtime needs; and the engine compares the two before anything -is compiled -- see the probe channel in [07 — build.mcpp](07-build-mcpp.md). +is compiled -- see the probe channel in [30 — build.mcpp](30-build-mcpp.md). The engine reads a name, a relation and a version; `cuda.driver` is data flowing through. @@ -363,7 +373,7 @@ not a gate. It is a different field from an artifact's `accel` on purpose — a declaration is written by hand and may be aspirational, while an artifact's field is measured from the build that produced it. -## What a prebuilt artifact states +## The declarations of a prebuilt artifact An artifact that carries device code records it beside its compatibility tag: @@ -383,7 +393,7 @@ already contains a variable number of dashes. An absent `accel` means the artifact carries no device code and constrains nothing, which is why a CPU-only library is usable by every build. -### How a consumer is matched +### Consumer matching A build's request is satisfied by an artifact when, for each backend the build asks for, the artifact declares that backend, agrees on the toolkit's major @@ -570,7 +580,7 @@ in precisely the way the dimension exists to prevent. A publisher writes the field explicitly today, which is what index descriptors do; `mcpp pack` will emit it once `kind = "device"` puts the device compilation inside mcpp. -## The lanes, and what each one drives +## The lanes, and the toolchain each drives A rule package owns one compiler's spelling. The engine knows none of these names: `tests/unit/test_core_vendor_probes.cpp` asserts that no vendor tool @@ -611,7 +621,7 @@ unwinder symbols they share. Nothing may cross the seam: a SYCL exception is caught in the device translation unit and returned as a code, because the runtime that threw it is not the one the caller would unwind with. -## Which platforms each lane reaches +## The platforms each lane reaches A lane reaches a platform when three things hold there: the device compiler is published for it, the runtime the produced artifact needs can be reached, and @@ -622,7 +632,7 @@ it downloads nothing and asks only whether the modules compile); that fixture turned three latent host differences into compile errors on the runners that had them, and none of the three had been visible to a Linux build. -| lane | Linux | macOS | Windows | what decides it | +| lane | Linux | macOS | Windows | deciding factor | |---|---|---|---|---| | `rules-spirv` | yes | yes | yes | the shader compiler is published for all three: `xim:glslang` on Linux, `xim:shaderc` on macOS arm64 and Windows x86_64 | | `rules-cuda` | yes | no | yes | NVIDIA publishes the redistributable components for Linux and Windows and has published no macOS toolkit since CUDA 10.2 | @@ -676,7 +686,7 @@ onto the artifact's own search path. macOS (dyld) and Windows (the PE loader) have no such layer by construction, and a project targeting them declares no adapter. -## What a framework looks like on top of this +## The shape of a framework built on this The five lanes prove a rule package can drive five compilers, the newest of them a vendor outside the NVIDIA and Khronos lineages. A framework is @@ -719,23 +729,18 @@ past it is the framework's own selector rather than a change to the packaging. `ggml-org:llamacpp` carries this as its `backend-vulkan` feature. -## Not implemented - -Device targets and the device linking they imply for the island shape, OpenMP -offload and stdpar, the AMD platform of HIP, and Metal. - -Two further gaps are per-platform rather than per-model, and both are -publishing work rather than engine work. **HIP on Windows** needs Windows -sections for the NVIDIA-platform header package and for `cuda-profiler-api`, -plus a Windows form for the header-declaration step that today writes into a -Linux SubOS view. **The 13.x CUDA line on Windows** needs a Windows form for -the back-end reunification: on that line upstream splits `nvvm/` and `crt/` out -of `cuda_nvcc` into four separately published components, which the index -reunites with symlinks, and `ln` is not a command on that host. The 12.x line -keeps its back end inside the component and needs none of it, so that is the -line Windows carries, declared as a deliberate divergence in each recipe rather -than left to be read off the file. - -See -`.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md` for the -design these follow from and the reason each is open. +## Current limitations + +- Device targets, and the device linking (RDC) they imply for the island shape. +- OpenMP `target` offload, and stdpar. +- The AMD platform of HIP. `rules-hip` reaches the NVIDIA platform only. +- Metal (`.metal`) and OpenCL C (`.cl`). Both extensions are classified as + device sources and no published rule package claims either, so a build that + names one is refused naming the file. +- The 13.x CUDA line on Windows. Windows carries the 12.x line. +- `mcpp pack` does not emit the `accel` field. A publisher writes it into the + descriptor. + +Per-platform limits for each lane are in the table under *Which platforms each +lane reaches*. + diff --git a/docs/11-machine-output.md b/docs/50-machine-output.md similarity index 93% rename from docs/11-machine-output.md rename to docs/50-machine-output.md index 6116c789..3387159b 100644 --- a/docs/11-machine-output.md +++ b/docs/50-machine-output.md @@ -1,12 +1,18 @@ -# 11 — Machine-Readable Output +# 50 — Machine-Readable Output + +**Reader:** the author of a tool, an editor extension or a CI job that parses +mcpp's output. + +**The question this chapter answers:** what may a program rely on, how is it +versioned, and how is the protocol detected. + +**Not here:** the human-facing output, which carries no compatibility promise, +and the exit-code contract, which is [SPEC-003](specs/exit-codes.md). mcpp writes for two audiences. This chapter is the contract for the second one: programs. Editor extensions, CI scripts, and anything else that parses mcpp's output may rely on what is stated here. -Design and the measurements behind it: -`.agents/docs/2026-08-08-machine-readable-output-protocol-design.md`. - ## 1. Primary rule > **Detect the protocol by parsing stdout. Never by exit code, and never by @@ -227,7 +233,7 @@ sees a version number assumes there is a contract behind it. ## 8. Kinds -### `mcpp.env` — where mcpp keeps things +### `mcpp.env` — the locations mcpp uses ``` mcpp self env --format json @@ -279,7 +285,7 @@ mcpp cache list --format json `data` is `{root, entries[]}`, the same document `--json` prints bare. -### `mcpp.toolchain.list` — what is installed, and which targets this host serves +### `mcpp.toolchain.list` — the installed toolchains and the targets this host serves ``` mcpp toolchain list --format json @@ -298,7 +304,7 @@ convention, and is empty for a row that has none. `x86_64-linux-gnu` has an installed gcc and no convention at all, so selecting "rows whose convention is a gcc" must read `pin`. -### `mcpp.why.toolchain` — what a build for one pair would resolve to +### `mcpp.why.toolchain` — the resolution for one (target, toolchain) pair ``` mcpp why toolchain [--target ] [--toolchain ] --format json @@ -356,7 +362,7 @@ never changes. field's meaning is unchanged (it still names the implementation), so §7 holds; what changed is that it stopped reporting the triple's env segment, which is a request rather than an implementation and is not the name of any C library. The -values are now the ones [14 — The Target Side](14-target-side.md) has always +values are now the ones [22 — The Target Side](22-target-side.md) has always listed, and a package may compare against them in a `cfg(c-abi = …)` predicate. A client keying on the literal `gnu` needs updating; `musl`, `picolibc` and `libSystem` are unaffected. @@ -447,3 +453,14 @@ must also read `not_run`. `unrunnable_members` (members all of whose tests were `not_run`), alongside the existing `not_run` list, which continues to name members the `--workspace-timeout` stopped before they started. + +## Current limitations + +- **The exit-code table is scoped to the commands it names.** A code another + command returns is not in it, and adding one would document something those + commands cannot promise. +- A diagnostic without a location omits `path` and `range` rather than sending + zeros: `line: 0` would point at a position that does not exist. +- Absence of JSON on stdout means "not supported", whatever the reason — an + older engine, an unknown option, or a command that has no machine format. A + client cannot distinguish the three from the stream alone. diff --git a/docs/19-supported-versions.md b/docs/51-supported-versions.md similarity index 77% rename from docs/19-supported-versions.md rename to docs/51-supported-versions.md index fbbf7a69..f39fbd36 100644 --- a/docs/19-supported-versions.md +++ b/docs/51-supported-versions.md @@ -1,4 +1,14 @@ -# 19 — Supported Versions and Compatibility +# 51 — Supported Versions and Compatibility + +**Reader:** anyone with a policy question — a review process, an air-gapped +build, a pinned release. + +**The question this chapter answers:** which releases are supported, what may +change between them, and what may not. + +**Not here:** the exit-code table, which is [SPEC-003](specs/exit-codes.md), and +the machine-output schema, which is +[50 — Machine-Readable Output](50-machine-output.md). This document states which releases are supported, for how long, and what may change between them. It exists because a project adopting mcpp is asked these @@ -12,7 +22,7 @@ that day's release. The scheme carries no compatibility promise in its digits: `2026.9.4.1` is not "a minor release" of `2026.9.3.2`. What may and may not change is stated below rather than encoded in the number. -## What is supported +## Support scope | | | |---|---| @@ -24,7 +34,7 @@ A release is superseded rather than withdrawn. Published assets and index entries for older versions remain in place, because a project may have pinned one and removing it would break a build that was working. -## What may change between releases +## Changes permitted between releases The engine's own interfaces are not all equally stable, and the difference is worth stating precisely. @@ -33,7 +43,7 @@ worth stating precisely. |---|---| | `mcpp.toml` keys | Additive. An existing key keeps its meaning; an unrecognised key is reported, never silently ignored | | CLI commands and flags | Additive. A removed spelling is kept as an alias | -| Machine output (`--message-format json`) | Versioned by `schemaVersion`; see [11](11-machine-output.md) | +| Machine output (`--message-format json`) | Versioned by `schemaVersion`; see [50](50-machine-output.md) | | `build.mcpp` directive protocol | Versioned; see `kProtocolVersion`. An engine refuses a program declaring a **higher** version rather than guessing | | `mcpp.lock` format | Versioned by `schemaVersion`; older files are migrated on read | | Target table rows | Additive. A row's tier may rise; a row is not removed while a published package targets it | @@ -88,3 +98,14 @@ Defects and security reports go to the issue tracker of the repository that owns the component — the engine, the package index, or the package itself. A report that names the version, the host, the target and the command is actionable; one that does not usually results in a request for those four. + +## Current limitations + +- **`mcpp.lock` does not yet constrain resolution.** It records what a build + resolved and `--locked` verifies that a new resolution matches it; pinning a + resolution to the lock as an input is a separate change to the resolver. +- The SBOM describes the **recorded** resolution rather than a fresh one, so a + document written after a manifest edit and before an `mcpp update` describes + the older graph. +- A component whose licence mcpp does not know is emitted as `NOASSERTION` + rather than omitted, because an absent field reads as "not examined". diff --git a/docs/90-build-from-source.md b/docs/90-build-from-source.md new file mode 100644 index 00000000..76663392 --- /dev/null +++ b/docs/90-build-from-source.md @@ -0,0 +1,197 @@ +# 90 — Building from Source and Contributing + +**Reader:** a contributor who will build and change mcpp itself. + +**The question this chapter answers:** how is mcpp built from source, how are +its own tests organised, and what does a contribution have to satisfy. + +**Not here:** how a user builds their own project, which is +[01 — Getting Started](01-getting-started.md), and how a release is cut, which +is [92 — Releasing mcpp](92-release.md). + +> mcpp is self-hosting — mcpp builds mcpp from source using mcpp itself. +> Any environment that already has a working mcpp binary can build from source. + +## Prerequisites + +Follow [01 — Getting Started](01-getting-started.md) to install a working copy of mcpp, then clone the repository: + +```bash +git clone https://github.com/mcpp-community/mcpp +cd mcpp +``` + +## Building and Testing + +```bash +mcpp build # compile the current source with the existing mcpp → ./target/.../bin/mcpp +mcpp run -- --version # run the artifact you just built +mcpp test # build and run C++ tests discovered under tests/**/*.cpp (including tests/unit) +``` + +`mcpp test` does not run the shell end-to-end suite under `tests/e2e/`; run +those separately against the freshly built binary. + +The first build automatically fetches the default toolchain; see [20 — Toolchain Management](20-toolchains.md) for details. + +To produce a fully static binary identical to a release (the path taken by `release.yml`): + +```bash +mcpp build --target x86_64-linux-musl +# → target/x86_64-linux-musl/.../bin/mcpp is a fully static ELF +``` + +## Source Layout + +mcpp is a **workspace**. `modules/` holds nine packages that the engine imports; +`src/` is the engine itself. + +``` +modules/ workspace members, imported by src/ +├── manifest/ manifest and descriptor parsing +├── platform/ operating-system abstraction +├── toolchain-model/ triples, dialects, fingerprints, the link model +├── buildmcpp/ the build.mcpp contract: protocol, directives, provisions +├── source-kind/ source-file role classification +├── versioning/ this binary's version, and SemVer requirements +├── dyndep/ ninja dyndep emission +├── libs/ vendored text-format parsers +└── log/ leveled logging + +src/ +├── main.cpp entry point +├── cli.cppm cli/ command dispatch and the commands +├── build/ build orchestration and the ninja backend +├── modgraph/ P1689 module scanning and the dependency graph +├── pm/ the resolver and the package-management commands +├── toolchain/ detection, fingerprinting, the std module +├── pack/ publish/ mcpp pack, mcpp publish and xpkg generation +├── fetcher/ fallback/ download, installation, fallback resolution +├── bmi_cache/ the cross-project BMI cache +├── runtime/ xlings/ the runtime contract and the xlings bridge +├── freestanding/ bare-metal targets, link line and runner +└── scaffold/ `mcpp new` and templates + +tests/ +├── unit/ 108 C++ tests, discovered by `mcpp test` +└── e2e/ 370 shell scripts against a real binary +``` + +## Test Organization + +Two layers, and they answer different questions. + +**Unit tests** (`tests/unit/`, 108 files) are C++ programs `mcpp test` +discovers. They exercise a module's contract with no binary and no filesystem +state. + +**End-to-end tests** (`tests/e2e/NN_.sh`, 370 files) run a real `mcpp` +binary against a real project. `run_all.sh` is the CI entry point. `mcpp test` +does **not** run them. + +```bash +MCPP= bash tests/e2e/02_new_build_run.sh +``` + +**A capability gate decides which run.** The first lines of an e2e script +declare what it needs, and a runner without that capability skips it: + +```bash +#!/usr/bin/env bash +# requires: elf gcc +``` + +`gcc` (80 scripts), `elf`, `unix-shell`, `llvm`, `jq` and `fresh-sandbox` are +the ones in use. A script that requires a capability no CI job provides **never +runs anywhere**, and its greenness means nothing — check that some job supplies +what a new script asks for. + +## Writing a check that measures something + +The most transferable rule in this repository, and the one that fails silently +when it is skipped: + +> **After writing a check, remove the fix and run it once.** A check that has +> never been seen to fail is not known to measure anything. + +Three shapes it catches, all of which have shipped here at least once: + +| shape | form | +|---|---| +| the criterion never runs | a test gated on a capability no job provides | +| the criterion cannot fail | a substring search satisfied by any wording | +| the criterion measures the wrong object | a fixture whose directory accumulates across runs, so the search answers about an earlier build | + +State the denominator too. "Every host in the table was scanned" is a check; +"the scan found nothing" is not, because an empty enumeration also finds +nothing. + +## The checks CI runs beside the tests + +`.github/tools/` holds eighteen scripts. Four are worth knowing before a first +PR: + +| script | the condition it refuses | +|---|---| +| `check_docs_style.sh` | question headings, second person in a reference chapter, a 简体中文 page whose heading structure has fallen behind | +| `check_docs_structure.sh` | a chapter citing a design record, a `docs/NN-*.md` path that does not resolve, a translation missing a table | +| `check_version_pins.sh` | a version written in one place and not the others | +| `check_modules_wiring.sh` | a workspace member wired into some of the three places that must know about it and not the others | + +## Adding a manifest field: the admission criteria + +> **Closed syntax, open vocabulary**: whoever owns the parsing semantics defines the keys; whoever owns the domain knowledge defines the values. + +- mcpp only defines **mechanisms** (feature union/closure, capability + require/provide/override, profile→compiler flags, platform→triple); the keys and + shapes are fixed. Domain vocabulary such as feature names, capability names, and + backend names **appears only in values**, never in mcpp's code. +- **Package-custom toml keys are not supported**: key legitimacy must not depend on + "first parsing the target package," otherwise the manifest loses static + parseability (a prerequisite for lockfiles/LSP/auditing). A package's extension + point = open value domains within fixed mechanisms. +- Package-level knobs all converge into features; for sugar keys (such as `backend=`) + to enter the core syntax, they must satisfy: ① domain-neutral (a cross-ecosystem + general pattern) ② 1:1 desugaring with zero new parsing semantics. +- **A key that duplicates an answer another section already gives is not admitted.** + Two places to state one fact is two places that can disagree, and the failure + is silent — whichever reader loses the race is simply wrong. Library packaging + ([12](12-binary-distribution.md)) is the worked example: it added **zero** + manifest keys, because what to pack is `[targets.].kind`, which interface + to publish is `[lib]` plus the module graph, which headers are public is + `[build].include_dirs`, and the per-artifact evidence is `[[runtime.artifacts]]`. +- A field that describes what a *generated* package IS (rather than what a build + should DO) belongs on `[[runtime.artifacts]]` — see §2.11. `provenance` + beginning with `mcpp-pack` is what marks a directory as one, and mcpp refuses + to `build` inside it. + + +## Issue and PR Guidelines + + + +### Issues + +File issues at [github.com/mcpp-community/mcpp/issues](https://github.com/mcpp-community/mcpp/issues), ideally including the following: + +- The full output of `mcpp self env` +- The full output of the failing command (`MCPP_LOG_LEVEL=debug` gives more detail) +- Your operating system, distribution, and glibc version (check with `ldd --version`) + +### Pull Requests + +mcpp is in early iteration and its interfaces may change. Before submitting a PR, please note: + +1. For changes touching the CLI or the `mcpp.toml` schema, open an issue first to align on direction. +2. Keep each PR focused on a single change; write commit titles in English imperative form (`fix: ...` / `feat: ...`). +3. For behavior changes or test documentation, run `mcpp test` and the relevant + E2E scripts against a fresh binary before submitting. For documentation-only + changes, recheck the examples and links; use `gh pr checks ` for + the PR's actual required checks. + +## Community Resources + +- [Community forum](https://forum.d2learn.org/category/20) +- Chat group QQ: 1067245099 +- [mcpp-index](https://github.com/mcpplibs/mcpp-index) — the default package index +- [mcpplibs](https://github.com/mcpplibs) — the companion collection of modular C++ libraries diff --git a/docs/08-toolchain-internals.md b/docs/91-toolchain-internals.md similarity index 97% rename from docs/08-toolchain-internals.md rename to docs/91-toolchain-internals.md index 27621d57..b153c261 100644 --- a/docs/08-toolchain-internals.md +++ b/docs/91-toolchain-internals.md @@ -1,8 +1,18 @@ -# 08 — Toolchain Internals +# 91 — Toolchain Internals + +**Reader:** a contributor, or a user debugging why a toolchain resolved the way +it did. + +**The question this chapter answers:** how does mcpp actually resolve, install +and assemble a toolchain, and which layer decides each flag. + +**Not here:** how to *choose* a toolchain, which is +[20 — Toolchain Management](20-toolchains.md). This chapter is the mechanism +under it, and nothing here is a stable interface. > How mcpp's toolchain machinery works under the hood, and how to extend it > with new toolchains, new architectures, and (eventually) embedded targets. -> Companion to [03 — Toolchain Management](03-toolchains.md), which covers the +> Companion to [20 — Toolchain Management](20-toolchains.md), which covers the > user-facing CLI. This document is for contributors and maintainers. ## 1. The model in one picture @@ -60,12 +70,11 @@ frontend candidates — this is where host-split distribution names like `mingw-cross-gcc` (Linux host) vs `mingw-gcc` (Windows host) live; they are current distribution-layer identity, not user-facing spellings. The payload is resolved/auto-installed via the xlings backend into the sandbox -(`$MCPP_HOME/registry/data/xpkgs/xim-x-//`). See -`.agents/docs/2026-07-15-toolchain-target-naming-unification-design.md`. +(`$MCPP_HOME/registry/data/xpkgs/xim-x-//`). `detect`/`probe` (`src/toolchain/detect.cppm`, `probe.cppm`) then derive: -| Field | How | +| Field | Derivation | |---|---| | `targetTriple` | ` -dumpmachine` | | `sysroot` | `-print-sysroot` (validated: must actually carry libc headers), with a remap fallback for xlings-built GCC whose baked build-time path doesn't exist locally | @@ -75,7 +84,7 @@ is resolved/auto-installed via the xlings backend into the sandbox Note the probe deliberately does **not** mine the clang cfg for `--sysroot` anymore: the cfg is an output of this machinery, not an input (§5). -### 2.1 The runtime binding — which libc, decided once +### 2.1 The runtime binding — one libc, decided once A payload-first build links against a specific glibc, and *which* one is a fact about the root project's local development OS, not something to infer @@ -198,7 +207,7 @@ decision, or a link succeeds and the artifact cannot start. The closure is one ordered list, each entry tagged with where it came from: -| origin | example | mutable? | ships? | +| origin | example | mutability | redistributable | |---|---|---|---| | `payload` | `/xim-x-glibc/2.39/lib64` | no — written once at install | no | | `package` | a dependency's `[runtime]` dir | no | no | @@ -461,7 +470,7 @@ Post-install alignment follows the same identity rule: `glibc@2.44` resolves only `/xim-x-glibc/2.44/{lib64,lib}`. A missing/stale exact payload is an error; another installed version is never a fallback. -### 6.2 Where a runtime search path is allowed to live (`runtime_env_contract.cppm`) +### 6.2 The permitted locations of a runtime search path (`runtime_env_contract.cppm`) There are two ways to tell a loader where to look, and they differ by blast radius, not by convenience: @@ -531,7 +540,7 @@ The machinery is already arch-parameterized; the work is data: ### 7.3 Embedded and bare-metal toolchains `riscv64-none-elf` and `riscv32-none-elf` are implemented, and the user-facing -account is [13 — Bare-Metal and Freestanding Targets](13-baremetal.md). This +account is [40 — Bare-Metal and Freestanding Targets](40-baremetal.md). This section records how the resulting shape relates to the hosted model above. Three of this section's earlier predictions held: @@ -573,7 +582,7 @@ everything §3–§4 does for ELF. and the difference is not a flag spelling — it is what the artifact records about itself: -| format | what the producer emits | what the consumer links | +| format | the producer's output | the consumer's link input | |---|---|---| | ELF | `-Wl,-soname,` when declared | `-L` + `-l`, `-Wl,-rpath,$ORIGIN` | | Mach-O | `-Wl,-install_name,@rpath/` **always** | `-L` + `-l`, `-Wl,-rpath,@loader_path` | @@ -598,7 +607,7 @@ question "can this machine produce it", and `prepare.cppm` now asks it — with explicit `[target.X] toolchain = "…"` as the escape hatch for a cross toolchain supplied by the author. -### 7.5 Which axis decides a flag +### 7.5 The axis that decides a flag Four flags changed in the 2026.8.18 round, and each had been keyed on the wrong axis. Every one of those mistakes showed up the same way: an inexplicable @@ -608,7 +617,7 @@ the decision behind it. There are three axes, and the question that picks between them is **who finally reads this flag**. -| axis | the question | examples | how it is asked | +| 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 ABI** | which linker will consume this | `--out-implib` vs `/IMPLIB:`, `/DEF:`, the SONAME / install-name form | `is_msvc_target(tc)`, `triple->is_msvc_env()` | @@ -648,4 +657,3 @@ answer them. | hermetic link check | `src/build/hermetic.cppm` | | regression fences | `tests/e2e/86_llvm_hermetic_link.sh`, unit `test_linkmodel.cpp`, `test_post_install.cpp`; the no-host-toolchain CI job in `ci-linux-e2e.yml` | -Design history: `.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md`. diff --git a/docs/09-release.md b/docs/92-release.md similarity index 95% rename from docs/09-release.md rename to docs/92-release.md index 29871f24..22db5ba9 100644 --- a/docs/09-release.md +++ b/docs/92-release.md @@ -1,7 +1,16 @@ -# 09 — Releasing mcpp +# 92 — Releasing mcpp + +**Reader:** a maintainer cutting a release of mcpp itself. + +**The question this chapter answers:** what steps take a commit to a released +version users can install, and how is each one verified. + +**Not here:** publishing a package to the index, which is +[11 — Publishing a Library](11-publishing-a-library.md), and what may change +between releases, which is [51](51-supported-versions.md). How a release of **mcpp itself** reaches users. This is maintainer-facing; for -packaging an *ordinary* project see [02 — Packaging for Release](02-pack-and-release.md). +packaging an *ordinary* project see [10 — Packaging for Release](10-pack-and-release.md). Until now this process lived only in commit messages and workflow comments. One of those commit messages contains a misdiagnosis that is corrected in §5. @@ -177,7 +186,7 @@ clean-room that still reports the old `latest` has not failed — it has not cau up. `ci-fresh-install`'s `wait-index` job encodes exactly this with a bounded 15-minute wait. -## 4. The bootstrap pin: what it is, and when to bump it +## 4. The bootstrap pin: its definition and its update conditions `.xlings.json`'s `[workspace].mcpp` is the **starting point of self-hosting** — the released mcpp that `xlings install mcpp` puts in the workspace so CI can build @@ -215,7 +224,7 @@ curl -fsSL https://github.com/xlings-res/xim-index/releases/download/latest/xim- pushed. Nothing enforces this, and in the job log the resulting failure is indistinguishable from a genuinely wrong version name. -## 5. `MCPP_PIN` is derived, and why that matters +## 5. `MCPP_PIN` is derived, and the consequence `ci-fresh-install.yml` used to carry a second hand-edited copy of the pin. It was never the same thing: `MCPP_PIN` is the version **under test** — always the newest @@ -277,3 +286,4 @@ would again let the index guard and the installed version drift apart. [ ] clean-room XLINGS_HOME: xlings install mcpp@ succeeds [ ] (optional) bump .xlings.json — only now, never earlier ``` + diff --git a/docs/README.md b/docs/README.md index 90bdfab8..70206d3c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,34 +2,158 @@ **English** | [简体中文](zh/README.md) -- [00 - Getting Started](00-getting-started.md) -- [01 - Examples](01-examples.md) -- [02 - Packaging & Release](02-pack-and-release.md) -- [03 - Toolchain Management](03-toolchains.md) -- [04 - Building from Source & Contributing](04-build-from-source.md) -- [05 - mcpp.toml Manifest Guide](05-mcpp-toml.md) -- [06 - Workspaces](06-workspace.md) -- [07 - build.mcpp Build Program](07-build-mcpp.md) -- [08 - Toolchain Internals](08-toolchain-internals.md) -- [09 - Releasing mcpp](09-release.md) -- [10 - Publishing a Library to mcpp-index](10-publishing-a-library.md) -- [11 - Machine-Readable Output](11-machine-output.md) -- [12 - Distributing a Prebuilt Library](12-binary-distribution.md) -- [13 - Bare-Metal and Freestanding Targets](13-baremetal.md) -- [14 - The Target Side](14-target-side.md) -- [15 - Cross-Compilation Over openkal](15-openkal-cross.md) -- [16 - The Target Triple](16-the-target-triple.md) -- [17 - The Project Environment](17-the-project-environment.md) -- [18 - Reaching a Device](18-devices.md) -- [19 - Supported Versions and Compatibility](19-supported-versions.md) -- [20 - Heterogeneous Builds](20-heterogeneous-builds.md) -- [21 - Commands by Scenario](21-commands-by-scenario.md) +This tree is the **usage manual for what mcpp implements**. Each chapter states +what a capability does, how it is written, and what its current limits are. The +reasoning behind a design, the alternatives that were rejected, and work that is +planned rather than shipped are deliberately absent — they belong to the design +records, which are not user documentation. + +## Where a document lives + +| tree | reader | contents | +|---|---|---| +| `docs/**` | someone with a task in hand | how to use what mcpp implements | +| [`docs/specs/**`](specs/README.md) | someone implementing against a mechanism: index authors, downstream tools, contributors | semantics, constraints and matching rules, each tagged with its implementation status | +| `.agents/docs/**` | whoever made a change, and whoever later asks why it is that way | the reasoning, the measurements, and what was refuted | +| `.agents/skills/**` | a contributor or agent following a procedure | ordered steps with criteria | + +A chapter cites a specification for exact semantics. It does not cite a design +record: a record describes a moment and carries no stability promise, so +anything a reader needs is written here or in a specification instead. + +## Start here + +| To | Read | Run | +|---|---|---| +| write a program | [01](01-getting-started.md), [04](04-mcpp-toml.md) §1 | [`01-hello`](../examples/01-hello/), [`02-with-deps`](../examples/02-with-deps/) | +| write a library others import | [11](11-publishing-a-library.md), [06](06-features-and-capabilities.md), [04](04-mcpp-toml.md) §2.4 | [`04-workspace`](../examples/04-workspace/), [`11-features`](../examples/11-features/) | +| publish it | [10](10-pack-and-release.md), [11](11-publishing-a-library.md), [12](12-binary-distribution.md) | [`03-pack-static`](../examples/03-pack-static/), [`05-lib-distribution`](../examples/05-lib-distribution/) | +| build for another machine | [21](21-the-target-triple.md), [24](24-openkal-cross.md), [40](40-baremetal.md) | [`06-openkal-cross`](../examples/06-openkal-cross/), `mcpp new … --template riscv-virt-rt` | +| use a GPU or an accelerator | [42](42-heterogeneous-builds.md), [41](41-devices.md) | [`09-heterogeneous`](../examples/09-heterogeneous/), starting at [`boundary/`](../examples/09-heterogeneous/boundary/) | +| add a rule, a language or a generator | [31](31-authoring-a-rule-package.md), [30](30-build-mcpp.md) | [`08-build-rules`](../examples/08-build-rules/), [`12-a-new-device-language`](../examples/12-a-new-device-language/) | +| add a package to the index | [11](11-publishing-a-library.md), [SPEC-001](specs/package-identity.md) | [09](09-commands-by-scenario.md) — the publishing scenarios | +| package a tool, a driver or a board for others | [32](32-authoring-a-payload.md), [33](33-authoring-an-adapter.md), [34](34-authoring-a-bsp.md) | the descriptors in `xim-pkgindex` and `mcpp-index` | +| change mcpp itself | [90](90-build-from-source.md), [92](92-release.md), [51](51-supported-versions.md) | — | + +Lessons also arrive as project templates, which a package ships and `mcpp new +--template` instantiates. `riscv-virt-rt` (bare metal) and `ocornut.imgui` +(a graphical application) are the two documented today; the chapter that uses +one names it. + +## Chapters + +The first digit is the part, so a number says where a chapter belongs: + +| | | +|---|---| +| `0x` | the fundamentals | +| `1x` | publishing | +| `2x` | toolchains and targets | +| `3x` | extending mcpp and its ecosystem | +| `4x` | devices and accelerators | +| `5x` | the contracts a program may parse | +| `9x` | mcpp itself | + +Within a part the order is a reading order, not an alphabet. + +### 0x — Fundamentals + +- [00 — What mcpp Is](00-what-mcpp-is.md) — what it is, what it does, and one session that has been run +- [01 — Getting Started](01-getting-started.md) — install, create, build, run +- [02 — Scenarios](02-scenarios.md) — what mcpp is used for, and which features each kind of work uses +- [03 — Examples](03-examples.md) — which example teaches what +- [04 — The mcpp.toml Manifest](04-mcpp-toml.md) — what a manifest may say +- [05 — Dependencies and Resolution](05-dependencies.md) — where a dependency comes from, and which version wins +- [06 — Features and Capabilities](06-features-and-capabilities.md) — making part of a package optional +- [07 — Workspaces](07-workspace.md) — several packages, one build +- [08 — Testing](08-testing.md) — including what does not run on this machine +- [09 — Commands by Scenario](09-commands-by-scenario.md) — the lookup, once the nouns are known + +### 1x — Publishing + +- [10 — Packaging an Application for Release](10-pack-and-release.md) +- [11 — Publishing a Library to mcpp-index](11-publishing-a-library.md) +- [12 — Distributing a Prebuilt Library](12-binary-distribution.md) + +### 2x — Toolchains and targets + +- [20 — Toolchain Management](20-toolchains.md) +- [21 — The Target Triple](21-the-target-triple.md) +- [22 — The Target Side](22-target-side.md) +- [23 — The Project Environment](23-the-project-environment.md) +- [24 — Cross-Compilation Over openkal](24-openkal-cross.md) + +### 3x — Extending mcpp and its ecosystem + +- [30 — Build Programs: `build.mcpp`](30-build-mcpp.md) — a project that needs a step mcpp has no rule for +- [31 — Authoring a Rule Package](31-authoring-a-rule-package.md) — packaging that step for other projects +- [32 — Authoring a Payload](32-authoring-a-payload.md) — a tool or a prebuilt library mcpp installs +- [33 — Authoring a Runtime Adapter](33-authoring-an-adapter.md) — reaching a library the host supplies +- [34 — Authoring a Board-Support Package](34-authoring-a-bsp.md) — a board, and the way in + +### 4x — Devices and accelerators + +- [40 — Bare-Metal and Freestanding Targets](40-baremetal.md) +- [41 — Reaching a Device](41-devices.md) +- [42 — Heterogeneous Builds](42-heterogeneous-builds.md) + +### 5x — Contracts for programs + +- [50 — Machine-Readable Output](50-machine-output.md) +- [51 — Supported Versions and Compatibility](51-supported-versions.md) + +### 9x — mcpp itself + +- [90 — Building from Source and Contributing](90-build-from-source.md) +- [91 — Toolchain Internals](91-toolchain-internals.md) +- [92 — Releasing mcpp](92-release.md) + +## Look it up + +The chapter list above is a **reading order**. This is the other index: from a +token in front of a reader to the chapter that owns it. + +**Manifest tables and keys** + +| | chapter | | chapter | +|---|---|---|---| +| `[package]`, `[targets.]`, `[build]`, `[lib]` | [04](04-mcpp-toml.md) | `[profile.]`, `[resources]`, `[runtime]` | [04](04-mcpp-toml.md) | +| `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]` | [05](05-dependencies.md) | `scan_overrides`, `module_extensions` | [04](04-mcpp-toml.md) | +| `[features]`, `[feature-deps.]`, `provides` / `requires` | [06](06-features-and-capabilities.md) | `[workspace]` | [07](07-workspace.md) | +| `[toolchain]`, `cxx_runtime` | [20](20-toolchains.md) | `[target.]`, `cfg(…)` | [22](22-target-side.md) | +| `[xlings]`, `[xlings.workspace]`, `[feature-xlings.]` | [23](23-the-project-environment.md) | `[pack]` | [10](10-pack-and-release.md) | +| `[build] accel`, `[package] accelerators`, `device_extensions` | [42](42-heterogeneous-builds.md) | `[hooks]` | [09](09-commands-by-scenario.md) | +| `[package] platforms`, `[build] cache` | [04](04-mcpp-toml.md) | `[targets.]`, `[profile.]` | [04](04-mcpp-toml.md) | +| `runner`, `[target..runners]` | [41](41-devices.md) | `rule_module` | [31](31-authoring-a-rule-package.md) | + +**Commands** + +| | chapter | | chapter | +|---|---|---|---| +| `build`, `run` | [01](01-getting-started.md) | `test` | [08](08-testing.md) | +| `new`, `new --template` | [01](01-getting-started.md) | `add`, `update`, `why` | [05](05-dependencies.md) | +| `pack` | [10](10-pack-and-release.md) | `publish`, `emit xpkg`, `xpkg parse` | [11](11-publishing-a-library.md) | +| `toolchain` | [20](20-toolchains.md) | `clean`, `cache`, `index`, `self …` | [09](09-commands-by-scenario.md) | + +**Concepts** + +| | chapter | | chapter | +|---|---|---|---| +| what mcpp is, and the guarantee | [00](00-what-mcpp-is.md) | `import std`, module interfaces, BMIs | [20](20-toolchains.md), [30](30-build-mcpp.md) | +| `mcpp::action`, a build program | [30](30-build-mcpp.md) | a rule package, `MCPP_EXPORT_C` | [31](31-authoring-a-rule-package.md) | +| a target triple, the support matrix | [21](21-the-target-triple.md) | a runner, a named runner | [41](41-devices.md) | +| an island, a seam, `accel` | [42](42-heterogeneous-builds.md) | a descriptor, an index | [11](11-publishing-a-library.md) | +| an ABI tag, a prebuilt artifact | [12](12-binary-distribution.md) | exit codes, JSON output | [50](50-machine-output.md) | +| an `xim:` payload, `[xlings.workspace]` | [32](32-authoring-a-payload.md), [23](23-the-project-environment.md) | a `compat:` adapter, `runtime.library_dirs` | [33](33-authoring-an-adapter.md) | ## Specifications -Normative documents — semantics, constraints and matching rules, each rule tagged -with its implementation status. For index authors, contributors and downstream tooling. +Normative documents — semantics, constraints and matching rules, each rule +tagged with its implementation status. For index authors, contributors and +downstream tooling. - [specs/](specs/README.md) — index of all specs - [SPEC-001 — Package identity, `[dependencies]` selectors and matching](specs/package-identity.md) + - [SPEC-002 — The target side: reserved namespace, five layers, three rules](specs/target-side.md) + - [SPEC-003 — The exit-code contract](specs/exit-codes.md) - [SPEC-004 — `mcpp.toml` planes, conditioning shape, resolution axes and naming](specs/manifest-semantics.md) diff --git a/docs/specs/exit-codes.md b/docs/specs/exit-codes.md index a50bab92..6d5975ba 100644 --- a/docs/specs/exit-codes.md +++ b/docs/specs/exit-codes.md @@ -11,7 +11,7 @@ | 相关设计文档 | `.agents/docs/2026-08-08-machine-readable-output-protocol-design.md` §R4、`.agents/docs/2026-08-31-issue540-seven-audit-findings.md` §4 | | 相关 issue | #379、#540 | -## 0. 这份规范为什么存在 +## 0. 这份规范存在的原因 机器可读输出协议的设计记录(§R4)在实测四个退出码之后写下: @@ -19,7 +19,7 @@ > 并覆盖异常边界 —— 否则客户端仍然要靠猜。**这条现在是 `docs/specs/` 的内容,不是 > 代码。** -被指派的那份契约一直没有写。`docs/11-machine-output.md` 落地了其中的 +被指派的那份契约一直没有写。`docs/50-machine-output.md` 落地了其中的 usage / internal 一半(`2`、`70`、`127`),runtime 的一半 —— 也就是命令**跑了并且 失败**时返回的 `1` —— 既不在那张表里,也不在别处。#540 由此把表读成「不完整」,并 提出补 `4`;而 `4` 恰恰是那几个带信封的命令给不出的码。 @@ -73,7 +73,7 @@ issue;`1` 通常不是。 ### 3.3 退出码禁止用于协议识别 已实现 客户端**禁止**用退出码判断「这个 mcpp 支不支持某项功能」。理由见 -`docs/11-machine-output.md` §1:在该协议出现之前发布的每个版本上,未知选项本身就是 +`docs/50-machine-output.md` §1:在该协议出现之前发布的每个版本上,未知选项本身就是 一次错误,而它当年走的是 stdout + 退出码 1。唯一跨版本成立的判据是**解析 stdout**。 ### 3.4 `1` 可以与 stdout 上的信封同时出现 已实现 @@ -100,7 +100,7 @@ issue;`1` 通常不是。 逐个归类: -| 数 | 是退出码? | 出处 | +| 数 | 属于退出码 | 出处 | |---|---|---| | `0` `1` `2` `127` | 是 | 全仓;`127` 在 `cli.cppm` | | `4` | 是,**11 处,全部同一个原因** | `config::load_or_init` 失败:`index_management.cppm`×6、`doctor.cppm`×2、`pack/pipeline.cppm`、`cli/cmd_toolchain.cppm`、`pm/commands.cppm` 各 1 | diff --git a/docs/specs/manifest-semantics.md b/docs/specs/manifest-semantics.md index 3b208143..2854fe0d 100644 --- a/docs/specs/manifest-semantics.md +++ b/docs/specs/manifest-semantics.md @@ -10,7 +10,7 @@ | **最低实现版本** | 条件化形状:mcpp **2026.8.29.1**(`[target..build-dependencies]` 起齐备);目标轴:mcpp **2026.9.6.4** | | **作者/维护** | mcpp-community | | **相关设计文档** | `.agents/docs/2026-09-07-mcpp-toml-unified-semantics-design.md`
`.agents/docs/2026-06-04-manifest-schema-ownership.md`
`.agents/docs/2026-09-03-xlings-workspace-as-the-one-table.md` | -| **相关使用文档** | [docs/05 —— mcpp.toml 字段参考](../05-mcpp-toml.md) | +| **相关使用文档** | [docs/05 —— mcpp.toml 字段参考](../04-mcpp-toml.md) | ## 规范用语 diff --git a/docs/specs/package-identity.md b/docs/specs/package-identity.md index 28f83a52..0da69892 100644 --- a/docs/specs/package-identity.md +++ b/docs/specs/package-identity.md @@ -227,7 +227,7 @@ error: dependency 'asio': no package found asio = "1.38.1" ``` -该 did-you-mean 扫描**仅**在已失败路径触发,结果**只进错误文案**,禁止回灌解析、lockfile 或安装层。 +该 `did-you-mean` 扫描**仅**在已失败路径触发,结果**只进错误文案**,禁止回灌解析、lockfile 或安装层。 迁移 release 对旧的 compact dotted 搜索做两件事:已有 lock 继续固定已记录身份;无 lock 时若旧的 `mcpplibs.` primary 确实存在,warning 同时显示旧/新完整 selector,但仍不回退。 @@ -347,7 +347,7 @@ warning,规范列举命令是 `mcpp new --list-templates pkg`。 2. 某个索引开始使用短名形态时,其 `index.toml` 的 `min_mcpp` **必须**同步抬到 **0.0.106**。这是硬性的:更低版本的客户端会静默 `E_NOT_FOUND`,无技术手段绕过。 3. 同一索引内**可以**长期混存两种形态。 -### 8.2 索引作者应当怎么写 +### 8.2 索引作者的写法 ```lua package = { diff --git a/docs/specs/target-side.md b/docs/specs/target-side.md index b5ddbde6..d17eac9c 100644 --- a/docs/specs/target-side.md +++ b/docs/specs/target-side.md @@ -8,7 +8,7 @@ | 最后修改 | 2026-08-24 | | 对应实现 | mcpp >= 2026.8.24.2 | | 相关设计文档 | `.agents/docs/2026-08-24-target-side-design.md` | -| 使用文档 | [docs/14 - 目标侧](../zh/14-target-side.md) | +| 使用文档 | [docs/14 - 目标侧](../zh/22-target-side.md) | 本规范定义一次构建的目标侧由哪些层构成、每一层可以由谁供给、 供给者与需求者如何声明,以及引擎据此执行的规则。 diff --git a/docs/zh/00-what-mcpp-is.md b/docs/zh/00-what-mcpp-is.md new file mode 100644 index 00000000..a572265b --- /dev/null +++ b/docs/zh/00-what-mcpp-is.md @@ -0,0 +1,180 @@ +# 00 —— mcpp 是什么 + +## 背景:C++ 工程侧的工具现状 + +一个 C++ 工程需要四样东西同时成立:一份构建描述、一组依赖、一个新到足以编译这份 +代码的编译器,以及一个能让产物真正运行起来的环境。C++ 没有任何一个工具同时负责 +这四样,它们由四类互不隶属的工具分别承担,而把它们对齐是工程自己的工作。 + +CMake 是其中第一样的事实标准。事实标准陈述的是采用率,不是使用体验:CMake 不解析 +依赖、不安装编译器、也不描述运行期环境,而这三件恰好是新参与者在第一天遇到的。 +补齐它们意味着再引入一个包管理器、发行版的软件包,以及 README 里的一段环境说明 +—— 三套新的模型,而它们之间的对齐仍然落在工程身上。 + +最经常失效的是环境这一层,因为只有它没有任何东西在检查。构建描述会在配置阶段 +报错,包管理器会解析失败,而「机器上的库不是这份代码构建时所依据的那一份」在 +链接之前不产生任何信号,有时到程序运行时才产生。 + +模块给这个结构再加一条约束。C++20 的模块改变的是一个翻译单元的代价:接口只声明 +一次并被 import,而不是被每个需要它的文件从头文件里重新解析一遍;消费者看到的是 +作者导出的东西,不是头文件恰好 include 进来的一切。代价是这个语言特性由构建系统 +实现:构建要扫描源里的 `import`、据此给编译定序、缓存编译好的接口并正确地让它 +失效,而且要在一个新到具备这个特性的编译器上完成这些。`import std` 再加一条: +标准库自己的模块必须先被构建出来,别的东西才能用它。 + +于是采用模块的工程需要上述四层全部成立,而不是其中三层。这个落差是具体的。在写 +这一章的这台机器上: + +```console +$ g++ --version +g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 +``` + +这个编译器编不了 `import std`。工程本身没有任何问题;是这台机器不是这个工程需要 +的那台机器。 + +## 这一现状的代价 + +**对人,代价是一次搭建**,每台机器一次、每个新参与者一次:装一个更新的编译器、 +确定哪些构建旗标把模块打开、取得依赖,然后在链接失败时判断坏的是这三者中的哪一 +件。这个代价不随工程成熟而下降,它按人数和机器数重复。 + +**对 agent,代价是上下文**,并且在写下第一行代码之前就消耗在三处:读构建描述以 +确定它究竟做了什么、重建它所假定的环境,以及顺着一个头文件穿过传递 include 去 +确定究竟声明了什么。 + +模块消掉了第三处,因为接口是显式的,`import` 陈述了用到什么。mcpp 消掉另外两处, +并把第一处收敛为一条命令。 + +## mcpp 的组成 + +``` +mcpp = 通用构建系统 + + 构建插件 + + 包管理 + + 工具链管理 + + 环境与运行时(xlings) +``` + +多数 C++ 工程要把这五个部分从不同工具里拼起来,而上面那份搭建代价正是拼装本身的 +代价:构建文件假定机器上有某个编译器,包管理器假定一份不是它写的构建文件,而环境 +是 README 里的一段话。 + +mcpp 是一个程序,五个部分共用同一套模型。 + +对于已经在用相应工具的读者: + +| 组成部分 | 在 mcpp 中的形式 | 可对照的工具 | +|---|---|---| +| 通用构建系统 | `mcpp.toml`、模块图、ninja 后端 | CMake、Meson | +| 构建插件 | `build.mcpp`、规则包 | `build.zig`、xmake rules | +| 包管理 | `[dependencies]`、`mcpp.lock`、索引 | Conan、vcpkg | +| 工具链管理 | 编译器作为被安装并钉住的载荷 | Zig 自带的工具链、rustup、手工安装 GCC / LLVM / MSVC | +| 环境与运行时 | `[xlings]`、载荷、运行期搜索路径 | Nix、conda | + +整体上最接近的两个类比是 Cargo 与 Zig,二者各对应同一个想法的一半。Cargo 是一个 +程序同时充当构建、包管理、锁文件与测试运行器,于是一个 Rust 工程克隆下来直接构建、 +没有前置步骤。Zig 把工具链随工具一起发布,并且默认支持交叉编译,于是编译器不是 +机器必须先具备的东西。 + +mcpp 在 C++ 上是这个形状,外加二者都没有的一部分:环境层 —— 工程通过它声明构建 +所需的非编译器工具。 + +**这张表为各部分定位,不宣称等价。** 表中每个工具在它自己的领域里做的都比 mcpp +多,需要那种深度的工程使用它。 + +## 核心保证 + +> **拿到任何一个 mcpp 工程,`mcpp build` 就能构建** —— 不需要自行安装编译器、 +> 配置环境,也不需要寻找依赖。 + +这里写下两条边界,以便这句话可以被依赖:面向设备的工程第一次构建仍会下载该设备的 +工具包;而这台机器服务不了的目标会被点名拒绝,不会被错误地构建出来。 + +## 最小示例 + +还是那台机器,唯一的 C++ 编译器是上面那个 GCC 13。 + +```console +$ mcpp new hello +Created bin package 'hello' at /tmp/zero-demo/hello +Next: cd hello && mcpp build && mcpp run (or `mcpp test`) +``` + +四个文件,manifest 五行: + +```toml +[package] +name = "hello" +version = "0.1.0" +description = "A modular C++23 package" +license = "Apache-2.0" +``` + +**没有声明编译器、没有声明语言档位、没有声明任何依赖。** 这就是一个读者在改动 +这个工程之前需要理解的全部。而源码用的正是这台机器的编译器不具备的那个特性: + +```cpp +import std; + +int main() { + std::println("Hello from hello!"); +} +``` + +```console +$ mcpp run + Inferred target hello (bin from src/main.cpp) + Compiling hello v0.1.0 (.) + Finished dev [unoptimized + debuginfo] in 0.64s + Running `target/x86_64-linux-gnu/0946988e9e4b52ba/bin/hello` + +Hello from hello! +Built with import std + std::println on modular C++23. +``` + +墙钟 1.25 秒,含首次运行。完成这次构建的编译器: + +```console +$ mcpp self env +default toolchain = gcc@16.1.0 +``` + +mcpp 安装了 GCC 16 并使用它。宿主上没有任何东西被改动,而克隆这个工程的同事 —— +或者 agent —— 得到的是同一个编译器,不是那台机器恰好自带的那个。 + +**增加一个依赖是一行,不需要其他步骤:** + +```toml +[dependencies] +"mcpplibs.cmdline" = "^0.0.1" +``` + +`mcpp build` 会解析它、取回它、构建它、链接它。 + +## 适用范围 + +mcpp 围绕 **C++20/23 模块与较新的语言特性**建立,它维护的生态由此而来,而不是 +来自一个泛化的目标: + +| | | +|---|---| +| 模块化 C++ | `import std` 零配置、模块扫描、跨工程 BMI 缓存 | +| 嵌入式与裸机 | freestanding 目标、板级支持包、从源码到一个可运行镜像只需一条命令 | +| 异构计算与 GPU | CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C,每一条都是规则包而不是引擎特性 | +| 图形 | 着色器作为构建的一部分被编译,并以模块到达 | +| 内核与底层 | 零 libc 档、显式的链接模型、没有隐藏的宿主依赖 | + +不需要其中任何一项的工程,同样得到上述保证;需要其中之一的工程,不必离开这个 +工具去得到它。 + +## 阅读路径 + +| | | +|---|---| +| 运行第一个程序 | [01 —— 快速开始](01-getting-started.md) | +| 判断 mcpp 是否适用于当前工作 | [02 —— 场景](02-scenarios.md) | +| 阅读一个结构相近的工程 | [03 —— 示例项目](03-examples.md) | + +本章之后的一切都是参考:manifest 可以陈述什么、依赖怎样解析、目标怎样命名。本章 +按设计不写任何字段与旗标。 diff --git a/docs/zh/01-examples.md b/docs/zh/01-examples.md deleted file mode 100644 index ae817974..00000000 --- a/docs/zh/01-examples.md +++ /dev/null @@ -1,58 +0,0 @@ -# 01 — 示例项目 - -> 仓库的 [`examples/`](../../examples) 目录下提供了一组循序渐进的最小工程, -> 覆盖从单文件 `import std` 到全静态发布包的常见场景。每个示例都可以 -> 独立进入并通过 `mcpp build` 完成构建。 - -## 运行方式 - -```bash -git clone https://github.com/mcpp-community/mcpp -cd mcpp/examples/01-hello -mcpp build && mcpp run -``` - -每个示例附带独立的 README,仅说明该示例相对前一个引入的新概念。 -安装步骤、工具链初始化等通用内容统一放在 -[00 — 快速开始](00-getting-started.md) 中,不再在示例内重复。 - -## 示例列表 - -| # | 路径 | 说明 | 涉及的关键概念 | -|---|---|---|---| -| 01 | [`examples/01-hello`](../../examples/01-hello/) | 单文件 + `import std` 的最小工程 | 最小工程形态(`mcpp new` 还会生成 `tests/test_smoke.cpp`) | -| 02 | [`examples/02-with-deps`](../../examples/02-with-deps/) | 引入依赖 `mcpplibs.cmdline` 解析命令行参数 | `[dependencies]`、SemVer、`mcpp.lock` | -| 03 | [`examples/03-pack-static`](../../examples/03-pack-static/) | 通过 `mcpp pack --mode static` 生成全静态发布包 | `[target.]` 与 `[pack]` 配置 | -| 04 | [`examples/04-workspace`](../../examples/04-workspace/) | 多包工作空间:两个库 + 一个应用,共享一个命名空间 | `[workspace]`、path 依赖、`mcpp build --workspace` | -| 05 | [`examples/05-lib-distribution`](../../examples/05-lib-distribution/) | 一个预建库和它的消费者,只有放在一起才有意义 | 对库做 `mcpp pack`、同一份源码同时给出 C 头文件与 C++ 模块、distribution 包 | -| 06 | [`examples/06-openkal-cross`](../../examples/06-openkal-cross/) | 同一个程序问每台机器它是什么,从任意宿主构建到四个目标 | `--target`、openkal、不改源码的交叉编译 | -| 07 | [`examples/07-project-subos`](../../examples/07-project-subos/) | 构建程序在工程声明的环境里找工具,而不是问机器上恰好有什么 | `[xlings] subos`、`[xlings.workspace]`、构建程序的 `PATH` 来自工程声明的那个环境 | -| 08 | [`examples/08-build-rules`](../../examples/08-build-rules/) | 两个规则包,以及同时用到它们的工程 | `host-module = true`、`[build-dependencies]`、`role = "check"` 的 `mcpp::action` | -| 09 | [`examples/09-heterogeneous`](../../examples/09-heterogeneous/) | 同一个计算在设备上跑,写成多种编程模型,每种都带 CPU 回退;外加一个同时携带多个后端的产物 | `accel`、带约束的 source glob、接缝模块、来自 `mcpp:plugins` 的规则包、`cfg(accelerator = …)` | -| 09a | [`…/cuda`](../../examples/09-heterogeneous/cuda/) | 接缝模块背后的 CUDA kernel,`extern "C"` 边界是**生成**的 | `mcpp.rules.cuda`、`mcpp.tools.island`、`role = "object"` 的 `mcpp::action`、把驱动陈述为 fact 与 floor | -| 09b | [`…/vulkan`](../../examples/09-heterogeneous/vulkan/) | 同一个计算写成 Vulkan compute shader,SPIR-V 载荷以**模块**到达 | `mcpp.rules.spirv`、模块表面、`role = "source"` 的 `mcpp::action`、作为载荷的软件驱动 | -| 09c | [`…/sycl`](../../examples/09-heterogeneous/sycl/) | 同一个计算写成 SYCL kernel,由第二个编译器编译 | `mcpp.rules.sycl`、`mcpp.tools.island`、`.sycl` 设备扩展名、为 device link 串起来的 `mcpp::action`、`compat:sycl-runtime` | -| 09d | [`…/hip`](../../examples/09-heterogeneous/hip/) | 同一个计算写成 HIP,边界是**手写**的 —— 与 09a 的对照 | `mcpp.rules.hip`、HIP 作为 CUDA 运行时之上的一层头文件、两段式的 `accel` | -| 09e | [`…/multi-backend`](../../examples/09-heterogeneous/multi-backend/) | 多个后端进**同一个产物**,运行期选择 —— 这是库的形态,不是程序的形态 | `accel` 作为集合、`cfg(accelerator = "none")` 及其否定、分发链、C 岛边界之上的模块接缝 | -| 09f | [`…/cann`](../../examples/09-heterogeneous/cann/) | 同一道接缝背后的 Ascend C kernel。**目前还构建不了** —— README 里点明了缺的两块 | `.asc` 设备扩展名、CANN 本来就有的 `op_kernel`/`op_host` 岛、回退用 `accelerator = "none"` | -| 10 | [`examples/10-graphics`](../../examples/10-graphics/) | 图形而不是计算:一条渲染管线,它的结果是像素 | `mcpp.rules.spirv` 编译顶点与片段两个阶段、离屏渲染作为可断言的形态 | -| 10a | [`…/offscreen`](../../examples/10-graphics/offscreen/) | Vulkan 把一个三角形光栅化进缓冲区,同一道接缝背后是同一个三角形的软件光栅器 | 一个 glob 带出两个着色器阶段、无窗口无交换链的 render pass、判据是一个像素 | - -## 推荐阅读顺序 - -建议按编号依次阅读: - -1. **`01-hello`** 展示 mcpp 工程的最小骨架(`mcpp.toml` 与 `src/main.cpp`), - 并演示 `import std` 的基本用法。当前 `mcpp new` 脚手架还会生成 - `tests/test_smoke.cpp`。 -2. **`02-with-deps`** 在前一示例基础上引入外部依赖,涵盖锁文件机制 - 与模块化包索引的工作方式。 -3. **`03-pack-static`** 演示如何将构建产物打包为可独立分发的单文件 - 二进制;打包细节可参考 [02 — 发布打包](02-pack-and-release.md)。 - -## 新增示例 - -示例工程遵循统一的目录结构:`mcpp.toml` + `src/` + `README.md`。 -新增示例时,在 `examples/` 下创建编号目录(如 `04-xxx/`),并在 -README 中简要说明该示例演示的概念,然后提交 PR。提交规范见 -[04 — 从源码构建 & 参与贡献](04-build-from-source.md)。 diff --git a/docs/zh/00-getting-started.md b/docs/zh/01-getting-started.md similarity index 77% rename from docs/zh/00-getting-started.md rename to docs/zh/01-getting-started.md index 25d54178..88daf05c 100644 --- a/docs/zh/00-getting-started.md +++ b/docs/zh/01-getting-started.md @@ -1,30 +1,49 @@ -# 00 — 快速开始 +# 01 —— 快速开始 + +**读者:**还什么都没装的新用户。 + +**本章回答的那一个问题:**从一台空机器开始,怎样把一个程序编译并运行起来。 + +**不在这里:**mcpp 有哪些部件 —— 那是 [00 —— mcpp 是什么](00-what-mcpp-is.md), +本章假定它而不重复它;以及 manifest 可以写的每一个字段,那是 +[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md)。下一章:[03 —— 示例项目](03-examples.md)。 > 5 分钟完成 install → new → build → run → pack 全流程。 ## 安装 -支持的宿主为 Linux x86_64 / aarch64、macOS ARM64 与 Windows x86_64,无需预先安装 GCC、xlings 或其他构建依赖。 -mcpp 在首次运行时会将默认工具链安装至独立沙盒(`~/.mcpp/`)。选择会随宿主变化:Linux x86_64 使用 `gcc@16.1.0`; 其他 Linux 架构使用 `gcc@15.1.0-musl`; macOS 使用 `llvm@20.1.7`; Windows 在存在可用 MSVC 时使用 `llvm@20.1.7`,否则使用面向 `x86_64-windows-gnu` 的 `gcc@16.1.0`。 +支持的宿主为 Linux x86_64 / aarch64、macOS ARM64 与 Windows x86_64。GCC、xlings 以及 +其余构建依赖都由 mcpp 安装,一个都不需要预先具备。 -推荐通过 [xlings](https://xlings.d2learn.org) 进行安装,可与系统 -环境保持隔离: +**推荐方式是 [xlings](https://xlings.d2learn.org)**,它让 mcpp 与系统环境保持隔离: ```bash xlings install mcpp -y ``` -Linux x86_64/aarch64 或 macOS ARM64 也可使用一键安装脚本(内置 xlings,统一安装至 -`~/.mcpp/`): +
+其它方式:独立安装脚本,以及首次运行会装什么 + +在 Linux x86_64/aarch64 或 macOS ARM64 上,有一个内置 xlings 的一键脚本,把一切装到 +`~/.mcpp/` 下。它不支持 Windows —— 那里的入口是 README 里的 PowerShell xlings 命令。 ```bash curl -fsSL https://github.com/mcpp-community/mcpp/releases/latest/download/install.sh | bash ``` -该脚本不支持 Windows;请改用 README 中的 PowerShell xlings 安装命令。 +mcpp 首次运行时会把一条默认工具链装进 `~/.mcpp/`,按宿主选择: -完整安装说明(包括 xlings 安装命令、Windows 支持等)参见 -[README 的"安装"小节](../../README.zh-CN.md#安装)。 +| 宿主 | 默认 | +|---|---| +| Linux x86_64 | `gcc@16.1.0` | +| 其它 Linux 架构 | `gcc@15.1.0-musl` | +| macOS | `llvm@20.1.7` | +| 有可用 MSVC 的 Windows | `llvm@20.1.7` | +| 没有 MSVC 的 Windows | 面向 `x86_64-windows-gnu` 的 `gcc@16.1.0` | + +完整安装说明(含 Windows)见 [README 的「安装」小节](../../README.zh-CN.md#安装)。 + +
安装完成后,启动新的 shell 会话,然后验证: @@ -119,6 +138,8 @@ workspace 中运行。插件稳定依赖进程退出码和生成的 `compile_com ## 增量编译与测试 +[08 —— 测试](08-testing.md) 是拥有这个主题的章节;下面只是本教程需要的那一步。 + ```bash mcpp build # 增量构建 mcpp clean # 清理 target/ @@ -154,7 +175,7 @@ linux 1019s、windows 1289s)。给它一个默认上限会把「慢但正确」 `mcpp build` 将自动从 [mcpp-index](https://github.com/mcpplibs/mcpp-index) 解析 SemVer 约束、拉取源码并加入编译图。完整示例参见 -[01 — 示例项目](01-examples.md) 中的 `02-with-deps`。 +[03 — 示例项目](03-examples.md) 中的 `02-with-deps`。 ## 生成发布包 @@ -167,13 +188,13 @@ mcpp pack --mode static # musl 全静态构建 mcpp pack --mode self-contained # 打包 loader、libc 与依赖 ``` -四种模式的差异及产物布局参见 [02 — 发布打包](02-pack-and-release.md)。`bundle-project` 与 `bundle-all` 仍分别是 `vendored` 与 `self-contained` 的兼容别名。 +四种模式的差异及产物布局参见 [10 — 发布打包](10-pack-and-release.md)。`bundle-project` 与 `bundle-all` 仍分别是 `vendored` 与 `self-contained` 的兼容别名。 ## 后续阅读 -- [01 — 示例项目](01-examples.md) — 可直接运行的最小工程集合 -- [02 — 发布打包](02-pack-and-release.md) — 构建可分发产物 -- [03 — 工具链管理](03-toolchains.md) — 切换编译器与多版本管理 +- [03 — 示例项目](03-examples.md) — 可直接运行的最小工程集合 +- [10 — 发布打包](10-pack-and-release.md) — 构建可分发产物 +- [20 — 工具链管理](20-toolchains.md) — 切换编译器与多版本管理 - 任意命令的完整选项可通过 `mcpp --help` 查阅 @@ -185,3 +206,4 @@ mcpp pack --mode self-contained # 打包 loader、libc 与依赖 - 解释默认决策:`mcpp why [toolchain|runtime|deps]`;主机能力体检:`mcpp self doctor`; 机器可读解析清单:构建产物 `target///resolution.json`。 - 离线运行:`mcpp --offline` 或 `MCPP_OFFLINE=1` 可阻止索引刷新、下载和工具链安装。在从未使用过的 home 中,它同时跳过首次使用时的沙箱引导(索引克隆、ninja、patchelf),只提示一次,并让该 home 保持未引导状态;需要这些工具的命令会自行报告。 + diff --git a/docs/zh/02-scenarios.md b/docs/zh/02-scenarios.md new file mode 100644 index 00000000..4f4f375c --- /dev/null +++ b/docs/zh/02-scenarios.md @@ -0,0 +1,249 @@ +# 02 —— 场景 + +**读者:**正在判断 mcpp 适不适合手头这份工作、以及这份工作会用到它哪些功能的 +开发者。 + +**本章回答的那一个问题:**mcpp 服务哪几类工程,而对其中一类,要用到哪些功能、 +按什么顺序。 + +**不在这里:**字段参考,那是 [04](04-mcpp-toml.md);示例目录,那是 +[03](03-examples.md) —— 它按**示例**索引而不是按场景;以及命令查阅,那是 +[09](09-commands-by-scenario.md)。本章按**工作本身**索引。 + +每个场景陈述:处境、mcpp 对它贡献了什么、穿过章节的路径、可以跑的一个工程,以及 +**最常让人意外的那一件事**。只读与手头工作相符的那个场景即可;它们之间不相互叠加。 + +## 场景一览 + +| | 场景 | 运行 | +|---|---|---| +| [1](#1-命令行工具或服务) | 命令行工具或服务 | `examples/01-hello` → `03-pack-static` | +| [2](#2-供他人-import-的库) | 供他人 import 的库 | `examples/11-features`、`05-lib-distribution` | +| [3](#3-一个仓库里的多个包) | 一个仓库里的多个包 | `examples/04-workspace` | +| [4](#4-图形应用) | 图形应用 | `mcpp new … --template ocornut.imgui` | +| [5](#5-为另一个操作系统构建) | 为另一个操作系统构建 | `examples/06-openkal-cross` | +| [6](#6-没有操作系统的目标) | 没有操作系统的目标 | `mcpp new … --template riscv-virt-rt` | +| [7](#7-gpu-或加速器上的计算) | GPU 或加速器上的计算 | `examples/09-heterogeneous` | +| [8](#8-图形渲染) | 图形渲染 | `examples/10-graphics/offscreen` | +| [9](#9-工程需要的一步构建工作) | 工程需要的一步构建工作,以及把它共享出去 | `examples/08-build-rules`、`12-a-new-device-language` | +| [10](#10-为别人打包一个工具一个驱动或一块板子) | 为别人打包一个工具、一个驱动或一块板子 | `xim-pkgindex` 与 `mcpp-index` 里的描述符 | + +## 1. 命令行工具或服务 + +**处境。** 一个由 C++23 模块构建的程序,带几个依赖,而且要跑在一台没有 mcpp 的 +机器上。 + +**mcpp 贡献了什么。** `import std` 零配置可用;编译器是被钉住的载荷,而不是机器 +上恰好有的那个;并且一条命令产出一个自带所需之物的二进制。 + +**路径。** + +1. [00 —— mcpp 是什么](00-what-mcpp-is.md) —— 五个名词。 +2. [01 —— 快速开始](01-getting-started.md) —— 把程序跑起来。 +3. [05 —— 依赖与解析](05-dependencies.md) —— `[dependencies]`、锁文件。 +4. [08 —— 测试](08-testing.md) —— `tests/**/*.cpp`。 +5. [20 —— 工具链管理](20-toolchains.md) —— 当编译器版本要紧,或者工程必须钉住 + 一个的时候。 +6. [10 —— 发布打包](10-pack-and-release.md) —— `mcpp pack`。 + +**跑。** [`examples/01-hello`](../../examples/01-hello/),然后 +[`02-with-deps`](../../examples/02-with-deps/),再 +[`03-pack-static`](../../examples/03-pack-static/)。 + +**最让人意外的一件事。** debug 构建与 release 构建互不失效。每种配置在 `target/` +下有自己的指纹目录,所以在两者之间来回切换不是一次重建。 + +## 2. 供他人 import 的库 + +**处境。** 别的包会把它写进 `[dependencies]` 的代码,其中可能有可选的部分。 + +**mcpp 贡献了什么。** 模块接口就是被发布的表面;feature 让其中一部分成为可选而 +不必拆出第二个包;预建二进制可以声明它与哪些工具链兼容。 + +**路径。** + +1. [04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md) —— `[lib]`、库根。 +2. [06 —— Feature 与能力](06-features-and-capabilities.md) —— 可选部分,以及它们 + 拉取的依赖。 +3. [08 —— 测试](08-testing.md) —— `[dev-dependencies]`。 +4. [11 —— 发布一个库](11-publishing-a-library.md) —— 描述符。 +5. [12 —— 分发预编译库](12-binary-distribution.md) —— 如果同时交付二进制。 + +**跑。** [`examples/11-features`](../../examples/11-features/) 把三种 feature 形态 +都声明了一遍;[`05-lib-distribution`](../../examples/05-lib-distribution/) 是 +生产方与消费方成对的那个。 + +**最让人意外的一件事。** 判断一个可选后端是否真的可选,判据**不是**「默认构建仍然 +能过」,而是「默认构建的**解析里不出现**那个包」。一个被解析出来却没被用到的依赖, +仍然要付下载的代价。 + +## 3. 一个仓库里的多个包 + +**处境。** 多个包一起开发,彼此按路径依赖。 + +**mcpp 贡献了什么。** 一条命令构建或测试整组;成员之间不经注册表就能相互解析; +每个成员保留自己的 manifest 与身份。 + +**路径。** + +1. [07 —— 工作空间](07-workspace.md) —— `[workspace]`、path 依赖。 +2. [05 —— 依赖与解析](05-dependencies.md) —— path 依赖做什么、不做什么。 +3. [08 —— 测试](08-testing.md) —— 扇出。 + +**跑。** [`examples/04-workspace`](../../examples/04-workspace/)。 + +**最让人意外的一件事。** 工作空间的成员**不是**一个根。任何枚举「每一个包」的工具 +都必须说清它指的是两者中的哪一个,而答案会改变构建什么。 + +## 4. 图形应用 + +**处境。** 一个带窗口、渲染器与字体的桌面程序。 + +**mcpp 贡献了什么。** 窗口与渲染栈就是普通依赖;模板脚手架出一个能跑的工程;产物 +在运行期需要什么是**被声明的**,不是被发现的。 + +**路径。** + +1. [01 —— 快速开始](01-getting-started.md) —— `mcpp new --template`。 +2. [05 —— 依赖与解析](05-dependencies.md) —— 那套栈。 +3. [10 —— 发布打包](10-pack-and-release.md) —— 可执行文件旁边还要带什么。 + +**跑。** `mcpp new myapp --template ocornut.imgui`。 + +**最让人意外的一件事。** 在 Linux 上,mcpp 的产物跑在一个**不查 `/usr/lib`** 的 +私有加载器后面。任何必须由宿主供给的东西 —— 图形驱动、Vulkan ICD —— 都经由工程 +声明的适配包到达,而不是靠它装在机器上。 + +## 5. 为另一个操作系统构建 + +**处境。** 一份源码树要产出 Linux、Windows 与 macOS 的二进制。 + +**mcpp 贡献了什么。** 目标是一个参数,不是第二份检出;交叉工具链是载荷;而这台 +宿主服务不了的目标会被**拒绝**,不会被悄悄按宿主构建。 + +**路径。** + +1. [21 —— 目标三元组](21-the-target-triple.md) —— 目标怎么命名,哪台宿主服务哪个。 +2. [24 —— 基于 openkal 的交叉构建](24-openkal-cross.md) —— 机制。 +3. [22 —— 目标侧](22-target-side.md) —— manifest 必须按目标不同时。 + +**跑。** [`examples/06-openkal-cross`](../../examples/06-openkal-cross/) —— 一个 +程序在任意宿主上为四个目标构建。 + +**最让人意外的一件事。** `--target` 不要求机器上**已经有**那条工具链。它要求的是 +这个目标能被这台宿主**服务**;[21](21-the-target-triple.md) 里的支持矩阵说明哪些 +可以。 + +## 6. 没有操作系统的目标 + +**处境。** 板子或单片机上的固件:没有 OS、默认没有 libc、有链接脚本与向量表。 + +**mcpp 贡献了什么。** 板级支持包供给整个目标世界 —— 链接脚本、启动代码,以及 +**runner** —— 于是 `mcpp run` 在模拟器上与在真实硬件上是同一条命令。 + +**路径。** + +1. [40 —— 裸机与 freestanding 目标](40-baremetal.md) —— 目标、档位、`std` 还剩什么。 +2. [41 —— 抵达一台设备](41-devices.md) —— runner、具名 runner。 +3. [08 —— 测试](08-testing.md) —— 跑在板子上的测试。 + +**跑。** `mcpp new blinky --template riscv-virt-rt`。 + +**最让人意外的一件事。** 模拟器与真实板子相差的是**一个 feature**,不是两个包。 +`mcpp run --features hardware` 移动的是默认 runner;开发者敲的那条命令不变。 + +## 7. GPU 或加速器上的计算 + +**处境。** 程序的一部分是由厂商编译器编译、再链接进普通二进制的 kernel。 + +**mcpp 贡献了什么。** 设备工具包由规则包声明、由构建安装;加速器是**一条只写一次 +的轴**;产物记录下它携带了哪些设备的代码。 + +**路径。** + +1. [42 —— 异构硬件构建](42-heterogeneous-builds.md) —— `accel`、岛、接缝。 +2. [30 —— 构建程序](30-build-mcpp.md) —— 规则怎样到达图。 +3. [06 —— Feature 与能力](06-features-and-capabilities.md) —— 选中一条 lane 的 + 那个 feature。 + +**跑。** 先 +[`examples/09-heterogeneous/boundary`](../../examples/09-heterogeneous/boundary/) +—— 它不需要设备 —— 再 +[`…/cuda`](../../examples/09-heterogeneous/cuda/)。 + +**最让人意外的一件事。** 不点名加速器的构建**一个字节都不下载**。多 GB 的工具包 +被取之前要开两道闸:选中规则的那个 feature,以及说明「这次构建确实为设备编译」的 +`cfg(accelerator = …)` 选择器。 + +## 8. 图形渲染 + +**处境。** 编译到 SPIR-V 的着色器、一条管线,以及必须正确的像素。 + +**mcpp 贡献了什么。** 着色器编译器是被声明的载荷;编译好的着色器以**模块**到达, +而不是一个没人写过其名字的生成头;并且渲染结果可以在没有 GPU 的情况下被断言。 + +**路径。** + +1. [42 —— 异构硬件构建](42-heterogeneous-builds.md) —— 着色器那条 lane。 +2. [30 —— 构建程序](30-build-mcpp.md) —— 要模块表面的那一行。 +3. [10 —— 发布打包](10-pack-and-release.md) —— 交付它。 + +**跑。** [`examples/10-graphics/offscreen`](../../examples/10-graphics/offscreen/) +—— 离屏渲染一个三角形,并与软件光栅器逐像素比对。 + +**最让人意外的一件事。** 软件设备不自动等价于硬件的替身。框架可能因为设备的**类型** +就拒绝它,哪怕它满足框架要求的每一项;那是框架自己的策略,不是打包缺陷。 + +## 9. 工程需要的一步构建工作 + +**处境。** 代码生成、嵌入资源、一项检查,或者第二个编译器 —— 引擎没有规则的东西。 + +**mcpp 贡献了什么。** 这一步变成与其余一切同一张图上的边:被定序、进指纹、可增量, +失败时按自己的名字被报出。它不是一个构建前脚本。 + +**路径。** + +1. [30 —— 构建程序](30-build-mcpp.md) —— `mcpp::action`、四种 role。 +2. [31 —— 编写规则包](31-authoring-a-rule-package.md) —— 如果别的工程也该用上它。 +3. [23 —— 项目环境](23-the-project-environment.md) —— 声明这一步要跑的工具。 + +**跑。** [`examples/08-build-rules`](../../examples/08-build-rules/) 是做检查与 +嵌入的规则;[`12-a-new-device-language`](../../examples/12-a-new-device-language/) +教会 mcpp 一门引擎从未听说过的语言。 + +**最让人意外的一件事。** 这一步调用的**那个工具本身是它的输入**。少了这一条,改动 +生成器会让每条边都是干净的,产物保留上一个生成器产生的字节 —— 一次覆盖在陈旧结果 +之上的绿色构建。 + +## 10. 为别人打包一个工具、一个驱动或一块板子 + +**处境。** 别的工程应当能够声明并得到的东西:一个编译器、一个着色器编译器、一个 +模拟器、一个宿主图形驱动、一块板子。 + +**mcpp 贡献了什么。** 消费者按名字声明它就能得到一个能用的程序 —— 载荷被安装、它的 +库进了产物的搜索路径,而档位决定「一次从不运行的构建要不要为它付代价」。 + +**路径。** + +1. [32 —— 编写一个载荷](32-authoring-a-payload.md) —— 由 mcpp 安装的工具或预编译库。 +2. [33 —— 编写运行时适配包](33-authoring-an-adapter.md) —— 当那个库属于宿主、 + 不可再分发时。 +3. [34 —— 编写板级支持包](34-authoring-a-bsp.md) —— 一块板子、它的内存布局,以及 + 抵达它的方式。 +4. [31 —— 编写规则包](31-authoring-a-rule-package.md) —— 如果有一步构建工作要驱动 + 这个工具。 + +**跑。** 描述符本身:载荷看 `xim-pkgindex/pkgs/g/glslang.lua`,适配包看 +`mcpp-index/pkgs/c/compat.vulkan-runtime.lua`,板子看 `mcpplibs/cortex-m-rt`。 + +**最让人意外的一件事。** 该用哪个答案,由**许可证与 ABI** 决定,不由偏好决定。 +用户态与内核模块处于锁步、且许可证禁止再分发的驱动**不能**是载荷;它是宿主能力, +而适配包是产物够到它的方式。开源驱动就是载荷,用着它的机器完全不需要适配包。 + +## 当前边界 + +- 这里列出的场景,都是背后有**可运行的工程或已发布模板**的那些。mcpp 服务得了、 + 但本仓库没有任何东西演示的场景不在此列 —— 一条没有工程可跑的路径是一个主张, + 不是一个场景。 +- 在既有构建系统内部采用 mcpp 不是这里的场景。mcpp 构建它自己拥有的工程;与另一个 + 构建系统的产物互操作既无文档也无示例覆盖。 diff --git a/docs/zh/03-examples.md b/docs/zh/03-examples.md new file mode 100644 index 00000000..8a76cfe5 --- /dev/null +++ b/docs/zh/03-examples.md @@ -0,0 +1,108 @@ +# 03 —— 示例项目 + +**读者:**在挑一个起点,或者在找一个与自己形状相近的工程的人。 + +**本章回答的那一个问题:**哪个示例教什么,以及它们以什么顺序相互叠加。 + +**不在这里:**任何一个示例的内容 —— 每个示例自带 README,只解释它新增的部分。 +在此之前:[01 —— 快速开始](01-getting-started.md)。在此之后: +[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md)。 + +[`examples/`](../../examples) 目录是一套课程。每个工程都可以单独跑起来,而且每个 +工程都教**一件更早的示例没有教过的事**。本章说明那件事是什么,于是你可以按自己 +需要的深度进入,而不必从头读起。 + +## 运行方式 + +```bash +git clone https://github.com/mcpp-community/mcpp +cd mcpp/examples/01-hello +mcpp build && mcpp run +``` + +每个示例自带 README,只解释它新增的部分。安装与工具链初始化在 +[01 —— 快速开始](01-getting-started.md),不在示例里重复。 + +## 课程 + +### A —— 工程的形状 + +| 示例 | 首次引入的内容 | +|---|---| +| [`01-hello`](../../examples/01-hello/) | 一个包、`import std`、`mcpp build` 与 `mcpp run` | +| [`02-with-deps`](../../examples/02-with-deps/) | `[dependencies]`、锁文件、`mcpp add` | +| [`04-workspace`](../../examples/04-workspace/) | `[workspace]`、path 依赖、`mcpp build --workspace` | +| [`11-features`](../../examples/11-features/) | **声明** feature 而不是消费它,`[feature-deps]`、`[dev-dependencies]`、`[profile.]`、`mcpp::has_feature` | + +### B —— 发布 + +| 示例 | 首次引入的内容 | +|---|---| +| [`03-pack-static`](../../examples/03-pack-static/) | `mcpp pack --mode static`、`[target.]`、`[pack]` | +| [`05-lib-distribution`](../../examples/05-lib-distribution/) | 一个库的接口与它的预编译二进制;从同一份源产出 C 头文件与 C++ 模块 | + +### C —— 环境 + +| 示例 | 首次引入的内容 | +|---|---| +| [`07-project-subos`](../../examples/07-project-subos/) | `[xlings]`、`[xlings.workspace]`,以及 `PATH` 来自工程声明环境的构建程序 | + +### D —— 目标 + +| 示例 | 首次引入的内容 | +|---|---| +| [`06-openkal-cross`](../../examples/06-openkal-cross/) | `--target`,同一份源在任意宿主上为四台机器构建 | + +裸机由**模板**而不是本目录里的一个工程来教 —— 见下面的*以模板形式到达的课程*。 + +### E —— 设备与图形 + +[`09-heterogeneous`](../../examples/09-heterogeneous/) 按顺序读。它的 README 是 +地图;下表是每个子示例新增的部分。 + +| 示例 | 首次引入的内容 | +|---|---| +| [`…/boundary`](../../examples/09-heterogeneous/boundary/) | 单独的岛边界:消费者 import 一个生成的模块,工程里没有接缝也没有头文件。不需要设备 | +| [`…/cuda`](../../examples/09-heterogeneous/cuda/) | 设备编译器、生成边界之上的接缝、把驱动陈述为 fact 与 floor | +| [`…/vulkan`](../../examples/09-heterogeneous/vulkan/) | 一个 compute shader,其 SPIR-V 载荷以模块到达 | +| [`…/sycl`](../../examples/09-heterogeneous/sycl/) | 第二个编译器,自带它自己的标准库 | +| [`…/hip`](../../examples/09-heterogeneous/hip/) | 手写的边界 —— 与 `boundary/` 和 `cuda/` 的对照 | +| [`…/cann`](../../examples/09-heterogeneous/cann/) | NVIDIA 与 Khronos 谱系之外的厂商 | +| [`…/multi-backend`](../../examples/09-heterogeneous/multi-backend/) | 多个后端进同一个产物,运行期选择 | +| [`10-graphics/offscreen`](../../examples/10-graphics/offscreen/) | 结果是像素的渲染管线,并与软件光栅器逐像素比对 | + +### F —— 为生态编写扩展 + +| 示例 | 首次引入的内容 | +|---|---| +| [`08-build-rules`](../../examples/08-build-rules/) | 两个规则包与同时使用它们的工程;`host-module = true`、`role = "check"` 的 `mcpp::action` | +| [`12-a-new-device-language`](../../examples/12-a-new-device-language/) | `device_extensions` 与 `rule_module`:规则包教会 mcpp 一门引擎从未听说过的语言,而它的编译器是一个经 `tools = [...]` 为构建机构建出来的包 | + +[31 —— 编写规则包](31-authoring-a-rule-package.md) 是这两个示例所演示内容的参考。 + +## 以模板形式到达的课程 + +一个包可以提供 `templates//`,由 `mcpp new --template` 实例化。那是与本目录 +和章节并列的第三个教学面;当被教的东西属于某个包而不属于 mcpp 时,课程就落在那里。 + +| 模板 | 课程 | 章节 | +|---|---|---| +| `riscv-virt-rt` | 一个裸机工程、它的板级支持与它的 runner | [40](40-baremetal.md) | +| `riscv-virt-rt:nolibc` | 同上,但没有 C 库 | [40](40-baremetal.md) | +| `ocornut.imgui` | 一个带窗口与渲染栈的图形应用 | [20](20-toolchains.md) | + +```bash +mcpp new blinky --template riscv-virt-rt +``` + +## 新增一个示例 + +一个示例目录是 `mcpp.toml` + `src/` + `README.md`,编号接在最后一个之后。什么时候 +值得新增一个示例:当一个能力**改变工程的形状** —— 它包含的文件、它声明的 manifest、 +或者作者敲的命令。如果一个能力只是既有示例工程里的一行,它属于对应章节里的一个 +代码块;如果它只经由命令到达,它属于 +[09 —— 按场景选命令](09-commands-by-scenario.md)。 + +README 要写明这个示例第一个教什么,以及判断它是否成立的判据。贡献流程见 +[90 —— 从源码构建 & 参与贡献](90-build-from-source.md)。 + diff --git a/docs/zh/04-build-from-source.md b/docs/zh/04-build-from-source.md deleted file mode 100644 index 8ab0125f..00000000 --- a/docs/zh/04-build-from-source.md +++ /dev/null @@ -1,117 +0,0 @@ -# 04 — 从源码构建 & 参与贡献 - -> mcpp 采用自托管模式 —— 通过 mcpp 自身从源码构建 mcpp。 -> 任何已具备可运行 mcpp 二进制的环境均可完成源码构建。 - -## 准备 - -参照 [00 — 快速开始](00-getting-started.md) 安装一份现成的 mcpp, -然后克隆仓库: - -```bash -git clone https://github.com/mcpp-community/mcpp -cd mcpp -``` - -## 构建与测试 - -```bash -mcpp build # 使用现成 mcpp 编译当前源码 → ./target/.../bin/mcpp -mcpp run -- --version # 运行刚构建出的产物 -mcpp test # 构建并运行 tests/**/*.cpp 中发现的 C++ 测试(包含 tests/unit) -``` - -`mcpp test` 不执行 `tests/e2e/` 下的 shell 端到端套件;应单独让它使用刚构建的二进制。 - -首次构建会自动拉取默认工具链,详见 -[03 — 工具链管理](03-toolchains.md)。 - -如需生成与 release 一致的全静态二进制(对应 `release.yml` 走的路径): - -```bash -mcpp build --target x86_64-linux-musl -# → target/x86_64-linux-musl/.../bin/mcpp 为全静态 ELF -``` - -## 源码结构 - -``` -src/ -├── main.cpp 入口 -├── cli.cppm 命令分发与参数解析 -├── cli/ 命令实现 -├── manifest/ manifest 模型、TOML 解析与 xpkg 描述符 -├── lockfile.cppm mcpp.lock -├── version_req.cppm SemVer 约束 -├── fetcher.cppm fetcher 门面 -├── fetcher/ 包/索引下载与安装 -├── config.cppm ~/.mcpp/config.toml -├── bmi_cache.cppm 跨项目 BMI 缓存 -├── bmi_cache/ 缓存存储与失效 -├── dyndep.cppm ninja dyndep 生成 -├── ui.cppm 进度条与输出格式 -├── build/ 构建编排与 ninja 后端 -├── fallback/ 回退解析路径 -├── modgraph/ P1689 模块扫描与依赖图 -├── pm/ 依赖解析器与包管理命令 -├── platform/ 平台与进程抽象 -├── scaffold/ `mcpp new` 模板与工程创建 -├── toolchain/ 工具链探测、指纹与 std 模块 -├── pack/ mcpp pack 实现 -├── publish/ mcpp publish 与 xpkg 生成 -└── libs/ 第三方依赖(toml 解析等) - -tests/ -├── unit/ C++ 单元/集成测试,通常按子系统分组 -└── e2e/ 端到端 shell 脚本(run_all.sh 为 CI 入口) -``` - -## 测试组织 - -测试分为两层: - -- **单元/集成测试** 是 `tests/**/*.cpp` 下由 `mcpp test` 发现的 C++ 文件。它们通常 - 按所测子系统或模块命名(例如 `test_pm_lock_io.cpp`、`test_toolchain_triple.cpp`)。 -- **E2E 测试** 位于 `tests/e2e/NN_.sh`,通过执行真实的 `mcpp` - 二进制覆盖端到端行为;`run_all.sh` 为 CI 调用入口。 - -根据变更的契约选择有针对性的单元和/或 E2E 覆盖。E2E 脚本可能需要 CI 使用的 -同一套沙盒、镜像与 capability 配置。 - -执行单个 e2e 脚本: - -```bash -MCPP= bash tests/e2e/02_new_build_run.sh -``` - -`` 必须替换为前一步刚构建二进制的绝对路径;Windows 上该文件为 -`mcpp.exe`。 - -## Issue 与 PR 提交规范 - -### Issue - -提交至 [github.com/mcpp-community/mcpp/issues](https://github.com/mcpp-community/mcpp/issues), -建议附带以下信息: - -- `mcpp self env` 的完整输出 -- 失败命令的完整输出(配合 `MCPP_LOG_LEVEL=debug` 可获得更详细信息) -- 操作系统、发行版、glibc 版本(可通过 `ldd --version` 查看) - -### Pull Request - -mcpp 处于早期迭代阶段,接口可能调整,提交 PR 前请注意: - -1. 涉及 CLI 或 `mcpp.toml` schema 的改动,建议先开 issue 对齐方向。 -2. 单个 PR 聚焦单一改动;commit 标题使用英文 imperative 形式 - (`fix: ...` / `feat: ...`)。 -3. 行为改动或测试文档改动在提交前运行 `mcpp test`,并让相关 E2E 脚本使用刚构建的 - 二进制通过;纯文档改动复核示例和链接,并用 `gh pr checks ` 确认 PR - 实际 required checks。 - -## 社区资源 - -- [社区论坛](https://forum.d2learn.org/category/20) -- 交流群 QQ: 1067245099 -- [mcpp-index](https://github.com/mcpplibs/mcpp-index) — 默认包索引 -- [mcpplibs](https://github.com/mcpplibs) — 配套的模块化 C++ 库集合 diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md new file mode 100644 index 00000000..78ee1835 --- /dev/null +++ b/docs/zh/04-mcpp-toml.md @@ -0,0 +1,1111 @@ +# 04 —— mcpp.toml 工程文件指南 + +**读者:**正在写或正在读一份 manifest 的作者。 + +**本章回答的那一个问题:**一份 `mcpp.toml` 可以说什么,逐字段地。 + +**不在这里:**四个主题的表虽然写在这个文件里,但本章不拥有它们 —— 依赖是 +[05](05-dependencies.md),feature 是 [06](06-features-and-capabilities.md), +以目标为条件是 [22](22-target-side.md),工程的环境是 +[23](23-the-project-environment.md)。每一处都在它的表本该出现的位置点名。 + +`mcpp.toml` 是 mcpp 构建工具的项目配置文件,类似 Cargo 的 `Cargo.toml` 或 Node 的 `package.json`。放在项目根目录下,`mcpp build` 会自动发现并读取它。 + +## 1. 最小化示例 + +mcpp 的设计原则是 **约定优于配置** —— 大多数字段都有合理默认值,最简单的 `mcpp.toml` 只需几行: + +### 1.1 可执行程序(最简) + +```toml +[package] +name = "hello" +version = "0.1.0" +``` + +mcpp 自动推断: +- 源文件: `src/**/*.{cppm,cpp,cc,c,S,s,asm}` +- 入口: `src/main.cpp` → 生成 `hello` 二进制 +- 标准: C++23 +- 模块: 扫描 `export module ...` 声明自动建立依赖图 + +### 1.2 库项目(最简) + +```toml +[package] +name = "mylib" +version = "0.1.0" + +[targets.mylib] +kind = "lib" +``` + +lib-root 约定:主模块接口默认在 `src/mylib.cppm`(包名的最后一段)。 + +## 2. 完整字段参考 + +### 2.1 `[package]` — 包元数据 + +```toml +[package] +name = "myapp" # 包名(必填) +version = "0.1.0" # 语义化版本(必填) +standard = "c++23" # C++ 标准(默认 c++23; 可设 c++20 / c++26) +description = "My awesome app" # 简介(可选) +license = "MIT" # 许可证(可选) +authors = ["Alice", "Bob"] # 作者列表(可选) +repo = "https://github.com/user/myapp" # 仓库地址(可选) +``` + +`standard` 是 C++ 语言标准的一等配置。推荐值: + +- `c++23`:默认值,适合当前模块化默认模板。 +- `c++20`:mcpp 接受的最低档位——命名模块本身是 C++20 特性,再往下这套构建模型就不存在了。当外部约束(公司内规、只到 C++20 的第三方 API)必须压低档位时使用。**`import std;` 在这一档依然可用**:它虽然是 C++23 的*库*特性,但 GCC(≥ 15)、Clang + libc++(≥ 17)与 MSVC STL(VS 2022 17.8 起)都在 C++20 模式下提供 `std` 模块。代价是 C++23 库设施(`std::print`、`std::expected` 等)不可用——包括 `mcpp new` 生成的模板代码。 +- `c++26`:需要 C++26 语言特性时使用。 +- `c++2a` / `c++2c`:兼容别名,解析后分别归一为 `c++20` / `c++26`。 +- `gnu++20` / `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。 +- `c++latest`:跟随当前 mcpp 支持的最新标准,适合本地试验,不推荐要求可复现的发布包使用。 +- `c++fly`:`c++latest` **再加上该工具链能开启的全部实验性标准特性**(语言 + 标准库)。GCC ≥ 16 上会打开 C++26 反射(`-freflection`)与契约;Clang/libc++ 上追加 `-fexperimental-library`;不支持的门会跳过并打印 summary。刻意是工具链相关的——最前沿的试验场模式,永远不要用于发布包。 + +两条需要知道的性质: + +- **标准是模块图全局的。** 根包的 `standard` 作用于本次构建的每一个 TU,依赖也不例外—— + 依赖自己 manifest 里的 `standard` 在它作为依赖被构建时不生效。这不是简化:BMI 跨档位 + 不兼容(GCC 直接报 `language dialect differs`),同一张图物理上不可能存在两个档位。 +- **档位之间从不共用缓存。** 标准同时进入 fingerprint、`import std` 的 BMI 身份和依赖构建 + 缓存键,所以在 `c++20` 与 `c++23` 之间切换只会各自拿到独立的产物目录和独立的 std BMI, + 不会出现错误命中。 + +如果源码在某个档位上 `import std;` 而解析出的工具链在该档位不提供 `std` 模块, +mcpp 会在编译前失败,并同时报出工具链与工程档位。 + +值的两种拼法都接受:`standard = "c++26"` 与 `standard = 26`。 + +当**依赖声明的档位高于当前图**时,mcpp 会在编译前说出来,而不是让它在那个依赖的源码里 +某处失败。见 [workspace §4.2](07-workspace.md)。 + +#### 方言标志与 `import std` BMI + +有些标志会改变标准库头文件**声明出什么**,因此预编译的 `import std` BMI 也必须带着它们一起 +构建。这就是 `[build] dialect_cxxflags` 的用途:它会被施加到 std BMI 预编译、模块扫描 +**以及**图中每一个 TU(依赖也包括在内)。 + +```toml +[build] +dialect_cxxflags = ["-fno-exceptions"] +``` + +其中少数几个标志,mcpp 在 `cxxflags` 里发现时会自动提升进这条通道 +(`-freflection`、`-fchar8_t`、`-D_GLIBCXX_USE_CXX11_ABI=…`)—— 混用这些标志的图本来就是 +病态的,任何依赖都不可能对它们持有另一种自洽的意见。 + +`-fno-exceptions` 与 `-fno-rtti` **不会**被自动提升,因为依赖可以合法地不同意:它们移除的是 +依赖可能正在使用的语言设施,而消费者无权替它做这个决定。留在 `cxxflags` 里,它们会到达每一个 +TU 却到不了预编译,于是构建不可能成功 —— mcpp 在编译前就拒绝,并指出该用哪个键: + +``` +error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is + precompiled without it, so every importing translation unit will fail with + "language dialect differs". + Declare it as a dialect flag instead: + + [build] + dialect_cxxflags = ["-fno-exceptions"] +``` + +这项检查读的是**生效后的**标志集合,所以同一个标志写在 `[profile.] cxxflags` 或 +`[target.…]` 块里同样会被抓到。而当图中根本没有 `import std` 时它不触发 —— 那里它就是一个 +正常工作的按 TU 选项。 + +### 2.2 `[targets.]` — 构建目标 + +```toml +# 可执行程序(默认,有 src/main.cpp 时自动推断) +[targets.myapp] +kind = "bin" +main = "src/main.cpp" # 可选,默认 src/main.cpp + +# 静态库 +[targets.mylib] +kind = "lib" + +# 共享库 +[targets.mylib] +kind = "shared" +soname = "libmylib.so.1" # 可选: Linux/ELF ABI 名称,运行时会生成同名 alias +``` + +`soname` 用于共享库的 ABI 名称,类似 Autotools/CMake 中的 +`SOVERSION`/`SONAME`。在 Linux 上,mcpp 会向链接器传递 +`-Wl,-soname,`,并在输出目录生成 ` -> lib.so` alias, +让下游程序可通过标准 ABI 名称 `DT_NEEDED` 或 `dlopen()` 加载该库。 +该字段只对 `kind = "shared"` 有效,值必须是文件名 basename。 + +共享库目标在三种二进制格式上都可用。ELF 产出带 `soname` 的 `.so` 与 `$ORIGIN` +搜索路径;Mach-O 产出 install name 为 `@rpath/` 的 `.dylib`,因此移动后 +仍能被找到;PE 同时产出加载器打开的 `.dll` 和链接器消费的 import library,并在 +MSVC ABI 上从对象生成导出表(该 ABI 没有 `__declspec(dllexport)` 或 `.def` 时 +不导出任何符号)。参见 `tests/e2e/08`、`257`、`259`。 + +#### `exports` —— 产物发布的符号集合(mcpp 2026.9.6.5+) + +```toml +[targets.mydriver] +kind = "shared" +soname = "libmydriver.so.1" +exports = "abi/mydriver.exports" # 或内联:exports = ["vk_icd*"] +``` + +**不写这个键就发布全部,而那正是两个平台今天的默认**——ELF 给符号默认可见性,PE 会 +自动生成列出全部符号的 `.def`。`exports` 把它收窄。 + +两类工程需要收窄。**有稳定 ABI 的运行时**只发布一份经过评审的集合,不在集合里的东西 +才保持可改。**与同类并存的插件**不能撞名:Vulkan loader 按名字找 +`vk_icdGetInstanceProcAddr`,一个把内部符号也导出的 ICD 会与 loader 以及同进程内另一个 +ICD 相撞。 + +文件一行一条符号模式,`#` 起注释,`*` 是唯一的通配符。内联数组说的是同一件事,用于 +只有两三个入口、单开一个文件反而是仪式的场合。 + +一句话,三种渲染: + +| 平台 | 渲染为 | +|---|---| +| ELF | version script,`-Wl,--version-script=` | +| Mach-O | `-Wl,-exported_symbols_list`(前导下划线由引擎补) | +| PE | `.def`,取代自动生成的全导出版本 | + +**它不改变编译期可见性,这是有意的。** 三种格式上收窄都是链接期属性,所以一个键只有 +一个效果。`-fvisibility=hidden` 仍可经 `[build] cxxflags` 使用以取得代码生成上的收益, +而它是一个**单独**的决定,因为它同时改变本库各翻译单元之间如何看见彼此。 + +**符号版本化不是这个键。** `foo@@LIB_1.0` 与 `foo@LIB_0.9` 并存是 ELF 独有的能力, +无法中立表达;需要它的包自己写 version script 经 `[build] ldflags` 传入,或者算出来后 +用 `mcpp:link-flag=` 发出(docs/07)。 + +`soname` 对 `kind = "lib"` 同样有意义 —— 见下文的 `dependency_linkage`, +库以何种形态出现是**消费者**的决定。 + +#### 按目标的键(per-target keys) + +```toml +[targets.server] +kind = "bin" +main = "src/server.cpp" +defines = ["BUILD_SERVER=1", "PORT=8080"] # -D 宏,只作用于该目标的入口 +cxxflags = ["-Wno-deprecated-declarations"] # 该目标入口的额外 C++ 标志(不要放 -std=...) +cflags = ["-DPURE_C"] # 该目标入口的额外 C 标志 + +[targets.gui] +kind = "bin" +main = "src/gui.cpp" +required_features = ["gui"] # 仅当 feature `gui` 激活时才构建 +``` + +| 键 | 含义 | +|---|---| +| `defines` | 预处理宏(`name` 或 `name=value`),脱糖为 `-D`,作用于该目标入口的 C 与 C++ 编译。 | +| `cxxflags` / `cflags` | 该目标的额外编译标志。**不要**放 `-std=...`——用 `[package].standard`。 | +| `required_features` | 仅当列出的 feature **全部**激活时才生成该目标,否则静默跳过。只是门禁——不激活 feature(用 `--features` / `[features].default`)。 | + +> **作用域(重要):** 目标上的 `defines` / `cxxflags` / `cflags` **只作用于该目标独占的入口源** +> (它的 `main`)——**绝不**作用于共享的模块/实现对象(那些只编译一次、被每个目标链接,即 mcpp 的 +> compile-once 模型)。当标志只需影响某个二进制(或测试)**自己的入口**时,这正是合适的工具 —— +> 例如某个测试的 `main` 里触发契约违规、需要按测试设置契约求值语义 +> (`-fcontract-evaluation-semantic=observe`),或入口独享的 feature 宏、局部告警抑制。 +> 若标志必须穿透**共享**代码,就不该放在这里 —— 改用 [workspace](07-workspace.md) member 或 +> `[features]`;若是整次构建的模式,用 `[profile.*]`(`mcpp test --profile ` 会让包括被测 +> 代码在内的整个测试镜像都在该 profile 下编译)。 +> +> `[targets.]` 下的不支持键会产生 warning(`--strict` 下为 error)。 + +**构建配置该放哪** —— 当多个二进制需要不同配置时: + +| 目标 | 使用 | +|---|---| +| 某二进制**自己入口**上的不同宏/标志 | per-target `defines` / `cxxflags`(见上) | +| 两个产品差异在它们**共享**的代码里 | 拆成 [workspace](07-workspace.md) member,各自 `[build]` 标志,共享一个 `lib` | +| **选择**某共享库的变体(如某后端) | 在该库上用 `[features]`(§2.8)——additive,作用到库自己的编译 | +| **整次构建的模式**(sanitizer、契约语义、优化档) | `[profile.]`(§2.9)+ `--profile`;`mcpp test --profile ` 同样支持 | + +mcpp 刻意不在一次构建里把同一个共享源编译成两份:一个源对应一个对象(模块还对应一个 BMI), +所以"必须穿透共享代码"的差异应放在包/feature 边界,而非单个目标上。 + +### 2.3 `[build]` — 构建配置 + +> **`sources` 匹配到的每一项都必须产出一个会被链接的对象。** mcpp 放不下的文件 —— +> 扩展名既不在内建表也不在 `module_extensions` 里 —— 会被拒绝,并点名文件、 +> 扩展名与该写的键。**不是忽略**:催生这条规则的失败不是「多编了一个文件」, +> 而是**编了却没人链** —— 扫描器读到 `export module` 就给那条边挂了 BMI, +> 而分类器说这个文件没有角色,作者看到的是一条模块修饰过的 `undefined reference`。 +> 头文件应放进 `include_dirs`,Windows 资源脚本放进 `[resources]`。 + +> **`sources = []` 与不写 `sources` 不是一回事。** 不写这条键选择默认 glob; +> 显式的空列表意味着**什么都不编** —— 那正是一个纯头文件的分发包需要表达的。 +> 在 mcpp 2026.8.18.1 之前两者逐字节等价,于是「什么都不编」无从表达, +> `src/` 下剩下的任何文件都会被扫进来。 + + +> **`sources` 的条目可以带上它所面向的加速器**(2026.9.5.2+): +> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`。glob 与其它条目一样 +> 进入列表;约束决定它是否适用于某一次构建。它必须至少匹配一个文件(空匹配会被拒绝: +> 那会让这个设备无东西可编,而只在链接时才说话)。`--no-accel` 下该 glob 被排除, +> 一个工程由此产出它的 CPU-only 变体。`--accel` 未覆盖该约束时构建被拒并给出两侧 +> (`accel-mismatch`)。有效集合匹配到的设备类源文件 —— CUDA 与 HIP、GLSL 各 stage、 +> HLSL、OpenCL C 与 Metal,完整清单见 [42 — 异构硬件构建](42-heterogeneous-builds.md) —— 引擎 +> 从不编译;它们以 `MCPP_DEVICE_SOURCES` 到达构建程序,由工程引入的规则包把每一个 +> 变成一条 `mcpp::action`。 + +```toml +[build] +sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/**/*.{cppm,cpp,cc,c,S,s,asm}) +module_extensions = [".ixx"] # 模块**接口**额外使用的扩展名(见下节) +build_program_timeout = 1800 # build.mcpp 的运行上限(秒);0 = 不限(见下节) +include_dirs = ["include", "third_party/include"] # 头文件搜索路径 +include_dirs_after = ["*"] # 排在系统目录之后搜索的头文件目录(-idirafter) +private_include_dirs = ["vendor/src/include"] # `include_dirs` 中不发布给消费者的那些 +c_standard = "c11" # C 源文件的标准(默认 c11) +cflags = ["-DFOO=1"] # 额外 C 编译参数 +cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...) +ldflags = ["-lfoo"] # 额外链接参数 +defines = ["BIZ=1", "QUX"] # 作用于每个 TU 的预处理宏(脱糖为 -D;会进入模块扫描) +cxx_runtime = "self-contained" # C++ 运行时契约(见下节);static_stdlib 是旧拼写 +macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效) +dependency_linkage = "static" # 依赖以何种形态进入:static(默认)| shared(见下文) +cache = "global" # 依赖的全局构建缓存:global(默认)| local | off(见 §2.10) +jobs = "auto" # 并发编译数:正整数,或 "auto"(见下节) +bmi_schedule = "auto" # 模块边调度:auto(= 关)| on | off(见下节) +``` + +#### `dependency_linkage` —— 静态还是动态由消费者决定 + +```toml +[build] +dependency_linkage = "shared" # 全图默认;缺省即 "static" + +[profile.dev] +dependency_linkage = "shared" # 按 profile 覆盖 + +[dependencies] +"compat.zlib" = { version = "1.3.2", linkage = "shared" } # 单个包 +``` + +在 mcpp 2026.8.28.2 之前,一个依赖只有一种形态,而且由**包作者**定死: +`kind = "lib"` 把它的对象并进每个消费者的链接,`kind = "shared"` 产出真正的 +共享库。这个决定放错了位置。一个库在运行期该不该是独立文件,是**被构建的那个 +程序**的性质 —— 它怎么分发、多久重链一次、进程里是不是已经有人提供了这个库。 + +- **`static`**(默认)—— 依赖的对象并进使用它的映像。与 mcpp 一直以来的行为 + 逐字节相同;不写这个键的工程构建结果不变。 +- **`shared`** —— mcpp 把依赖构建成产物旁边的共享库并链接它,由 `$ORIGIN` + (ELF)/ `@loader_path`(Mach-O)/ 可执行文件自身目录(PE)保证构建目录 + 移动后仍能找到它。 + +**这不是 `[target.].linkage`**(§2.7.1)。那个键回答的是听起来相同、 +实则关于 **C 库**的问题(musl 的 `-static`、MSVC 的 `/MT`)。两者并不独立,而且 +方向很重要:整链静态的映像没有解释器,根本装不下任何共享对象。因此在 C 库静态 +链接的目标上 —— 这是 **musl 的默认** —— `dependency_linkage = "shared"` 会被 +拒绝,并说明原因。 + +**包可以声明它必须是某一种形态**,而且只在确有理由时: + +| 包写了 | mcpp 读作 | +|---|---| +| `[targets.] kind = "shared"` | *必须* shared —— 进程里会有别人 `dlopen` 它,因此只能有一份(X11、Vulkan loader) | +| `ldflags` 里含 `-L` | *必须* static —— 包携带了 mcpp 没有编译的预构建归档,放不进 mcpp 自己构建的共享对象 | +| 分发包(`mcpp pack`) | 它实际随包的那些腿,取自 `[[runtime.artifacts]] role` | +| 其他 | 两种形态都可以 | + +`kind = "lib"` **不是**约束:它是默认值,大多数包写下它并没有做任何选择。 +**没有陈述不等于一条陈述。** + +依赖边上的 `linkage` 只在**根工程**的 `[dependencies]` 里生效。依赖图深处的包 +无权决定最终程序的布局;真正必须只有一份共享副本的包,应当在自己的 target 上 +声明。 + +#### library 目标上的 `soname` + +`soname`(§2.2)在 `kind = "lib"` 上同样可以声明。它是一个库被**找到**时用的 +名字,也是 mcpp 构建的那份与第三方携带的同一个库能解析到**同一个文件**的唯一 +途径 —— 而如果声明它就意味着这个包不能再作为静态库被消费,包就无法陈述这件事。 + +在非 shared 目标上写 `soname` 的描述符,**无法被 2026.8.28.2 之前的 mcpp 读取** +—— 失败的是整份 manifest,不只是这个键。因此把它发布进索引要等下限抬上去。 + +#### 符号提供者检查 + +链接之后,mcpp 会问:映像里的每个符号是不是**恰好有一个**提供者。在 ELF 上 +可执行文件排在最前,因此被静态并进程序的库,会在它与旁边加载的共享库共有的 +每个符号上获胜 —— 共享的那份永远不会被调用,而那个库里的代码跑在一份它并非 +针对其链接的构建上。链接器和加载器都不会为此报任何一句话。 + +这项检查是**测量**而不是声明:读产物的动态符号表,去掉 copy relocation,只报告 +产物自身闭包里**也**有定义的那些。进程里只有一份副本的安排保持静默。判定记录在 +`target///resolution.json` 的 `runtime.symbol_provision` 下,带计数 +与分母,CI 不需要 `readelf` 就能读。 + +默认是警告,`--strict` 下升级为错误。三条出路**有次序**,而次序是要紧的: + +1. **让其中一方不再提供这个库** —— 通常是那个携带了依赖图已经在构建的库的副本 + 的包。永远正确。 +2. **让两者解析到同一个文件**:在库的 target 上声明它真正的 `soname`。 +3. **`dependency_linkage`** 改变 mcpp 构建的形态。它会消掉**这一条**报告,但单 + 独用可能把一份变成**两份**:实测在一个暂存了 glib(其 `libgio` 需要 + `libz.so.1`)、同时静态构建 `compat.zlib` 的图上,切换形态让可执行文件的 88 + 个导出符号归零,然后 `libzlib.so` 与 `libz.so.1` **两个都被加载**。只有在 + (2) 同时成立时它才真的把两个提供者合成一个。 + +`private_include_dirs` 指出 **`include_dirs` 中**在本包边界处停住的那些条目: +本包用它们编译,消费者永远收不到。 + +绝大多数包发布的就是它编译时用的那一套,所以长期以来只有 `include_dirs` 就够了。 +两者不同的形状只有一种 —— 一个包**内嵌了带内部头覆盖层的库**。musl 通过 +`src/include` 到达它自己的声明,而那些头定义了 `hidden`、`weak`、`weak_alias`, +这些名字只对 musl 自己的源码有意义。把那个目录发布出去,等于把这些宏交给每一个 +消费者;而一个把 `hidden` 当普通标识符用的消费者会编不过,且看不出原因。 + +```toml +[build] +# 两类目录的**相对顺序**是承重的:本包自己构建时,内部覆盖层必须排在公共头之前。 +# 这正是它被设计成 `include_dirs` 的**子集**而不是第二个列表的原因 —— +# 两个数组表达不了一个顺序。 +include_dirs = ["port/include", "musl/src/include", "musl/include"] +private_include_dirs = ["musl/src/include"] +``` + +条目支持与 `include_dirs` 相同的 `*` glob 约定,并在**展开之后**比对 —— +所以一个 glob 可以恰好指名它展开出的那些目录。若某条目不在本包的 `include_dirs` +里,它什么也没扣下,mcpp 会把这件事说出来而不是让它悄悄通过。 + +**旧引擎会忽略这个键,而不会因此失败。** 在 2026.8.26.2 上实测:出现在依赖的清单里 +时被静默接受;出现在根清单里时给一条警告 —— `[build] has unsupported key +'private_include_dirs' (ignored)` —— 构建照常继续。所以一个包可以先用上这个键, +不必等消费者升级;还在旧引擎上的消费者只是像以前一样继续收到那个目录。**唯一不成立 +的地方**是已发布的 `xim` 描述符的 `target_cfg` 块:那里不认识的子键是硬错误,会让 +整份清单加载失败 —— 在索引下限指向认识它的引擎之前,不要把这个键写进那里。 + +`include_dirs_after`(#249)列出**排在工具链系统目录之后**搜索的头文件目录 +(GCC/Clang 发射为 `-idirafter`;MSVC 方言退化为排在末尾的 `/I`,NASM 汇编 +单元退化为普通 `-I`——两者都没有对应 flag,也都没有需要保护的系统头搜索链)。当目录是解压后的源码 tarball 根目录、且其中的文件名会与标准头冲突时, +用它代替 `include_dirs` —— 例如 ffmpeg 根目录的 `VERSION` 文件在大小写不敏感 +的 macOS 文件系统上会把 libc++ 的 `` 遮蔽(若该根目录挂在 `-I` 上)。 +使用 `include_dirs_after` 时系统头永远优先,而包自己的真实头文件 +(``)仍能找到。条目支持与 `include_dirs` 相同的 `*` glob +约定,并沿相同的依赖边传播给消费者 —— 消费者收到的仍是 after 目录, +永远不会被升级为 `-I`。 + +`macos_deployment_target` 设定产物 Mach-O 头里的最低系统版本 +(`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态 +惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖, +cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的 +`platforms:`)> **内建默认 `14.0`**(rustc 风格——每个 target 都有基线, +14.0 即 LLVM 官方静态库自身的下限)。该值会进入 BMI 指纹——切换 target +会自动重建模块缓存。 + +### 构建并发(`jobs`)与模块调度(`bmi_schedule`) + +```toml +[build] +jobs = "auto" # 或正整数;--jobs / MCPP_JOBS 覆盖它 +bmi_schedule = "off" # auto(默认,= 关)| on | off +``` + +`jobs` 是同时跑几个编译。`"auto"` **在构建这台机器上现算**,绝不冻进 manifest: +异构 CPU 上取物理核数(13900K 是 8 P-core + 16 E-core,它的 32 个线程不是 32 个 +等价的工人),再按可用内存夹一次 —— 单个模块接口编译峰值 0.5–1.0 GB。 +优先级:`--jobs` / `MCPP_JOBS` > 这个键 > 后端自己的默认值。写错的值会被 +**明确报出来,绝不静默当成默认值** —— 一个悄悄退回默认的拼写错误,表现是 +「构建莫名其妙比我要求的慢」。 + +`bmi_schedule` 决定**导入方什么时候被解锁**。 + +| 值 | | +|---|---| +| `"auto"` | **默认值,而它目前等于「关」** | +| `"on"` | 拆开模块边:BMI 一发布导入方就能开始,而不是等编译器退出 | +| `"off"` | 每个模块一条边 | + +只认这三种拼写。`"ON"`、`"true"`、`"yes"` 会被**拒绝并给出诊断**,而不是悄悄 +当成关 —— 而且它们不是无害的笔误:这个值会进构建指纹,所以一个被拒的拼写 +以前会选到**另一个构建目录**(即一次全量重建),同时对调度没有任何影响。 + +**`auto` 为何等于关闭。** 模块接口编译中约 86% 是任何导入方都不会读取的代码生成, +因此提前发布 BMI 收益显著 —— 在 mcpp 自身上实测:`cold` 86.7s → 35.7s、 +`edit-body` 80.9s → 29.8s。但调度错误的表现是静默失效:缺少一条依赖不会使构建 +报错,只会使某个目标不再重建。因此在所有平台完成 CI 验证前,该键保持 opt-in。 + +**该键无效的场景。** mcpp 本来就跳过级联的地方(`touch-hub`、`edit-comment`) +没有可以移出关键路径的必需工作,该键不产生收益。见 +[性能对比](../../README.zh-CN.md#性能对比)。 + +**实现方式**按编译器确定,无需用户选择:gcc 用 `rename()` 发布 BMI,所以代码 +生成被分离出去、边在发布时就返回;clang 换成两条普通边 —— 它把 BMI 直接 +`O_TRUNC` 写到最终路径,读的人可能看到写了一半的文件。MSVC 不动:`/ifcOnly` +的代价和 `.ifc` 是否原子发布都没测过,而这两件事猜错都是无声的。 + +### 模块接口扩展名(`module_extensions`) + +mcpp 把 `.cppm` 视为模块接口单元。C++ 生态并没有收敛到一种拼法 —— Clang 还认 +`.ccm` 和 `.cxxm`,MSVC 用 `.ixx` —— 所以接口用别的扩展名的工程自己声明: + +```toml +[build] +module_extensions = [".ixx", ".ccm"] +``` + +这个列表是**追加**的:`.cppm` 永远是模块接口,不能删。要让某个文件不参与构建, +用 `sources` 的 `!` 前缀 —— 那才是 `sources` 的职责。 + +声明一个扩展名会同时做三件事,这正是「一个键而不是几个键」的理由: + +1. `sources` 的约定默认值跟着变宽,文件才**能被找到**(`src/**/*.ixx` 自动进入默认 glob); +2. 这些单元用**模块**规则编译 —— 产出 BMI,其 `.o` 无条件进入链接; +3. 新鲜度快路径会扫描它们,所以给其中一个加 `import` 会让构建图作废, + 而不是静默复用一张过期的图。 + +**任何扩展名都接受**,唯独拒绝那些已经代表其他角色的 +(`.cpp` `.cc` `.cxx` `.c` `.m` `.mm` `.h` `.hpp` `.hh` `.hxx` `.S` `.s` `.asm`)—— +这是 manifest **错误**而不是警告,因为它会把(比如)C 文件送进 C++ 模块规则, +最终失败在一个既不提文件也不提这个键的地方。 + +扩展名**按字面匹配,不做大小写折叠** —— 在这个领域里 `.S` 和 `.s` 是两种不同的语言, +所以大小写从不被忽略。 + +mcpp 每次都会**显式告诉编译器**这个单元是模块接口(Clang 用 `-x c++-module`, +GCC 用 `-x c++`,MSVC 用 `/interface /TP`),所以即使编译器驱动从没听说过这个扩展名 +也能工作。这也是为什么任何扩展名都被允许:mcpp 不需要编译器认识它。 + +> **发布须知**:旧版 mcpp 不认识这个键 —— 它会警告、忽略,然后把那些文件当作普通 +> 翻译单元编译,得到一个**错误的构建**而不是一次干净的失败。发布一个用了 +> `module_extensions` 的包,请在它的索引描述符里声明 mcpp 版本下限。 + +### 构建程序超时(`build_program_timeout`) + +`build.mcpp` 默认有 **600 秒**,超时后 mcpp 杀掉它并让构建失败、点名是哪个包。 +构建程序确实需要跑更久的工程(大规模代码生成)自己抬高上限: + +```toml +[build] +build_program_timeout = 1800 # 秒;0 = 不限 +``` + +这个值读的是**拥有该 `build.mcpp` 的那个包**的 manifest —— 依赖的生成器由依赖自己的 +声明来限制,因为只有它的作者知道要跑多久。优先级与 `macos_deployment_target` 同构: + +``` +MCPP_BUILD_PROGRAM_TIMEOUT=<秒> 本次调用(最高) + > [build] build_program_timeout 该包自己的 manifest + > 600 内置默认 +``` + +**不写这个键**与**写 `0`** 不是一回事:不写表示「用默认上限」,`0` 表示「完全不设上限」。 + +这个值刻意**不进构建指纹** —— 它不改变图里的任何一条边,而把它折进指纹会让 +「抬高超时」触发全量重建,这恰好与抬高超时的人想要的相反。 + +只有构建**程序**受限,**编译**不受限。原因见 +[30-build-mcpp.md](30-build-mcpp.md)。 +### C++ 运行时契约(`cxx_runtime`) + +已移入 [20 —— 工具链管理](20-toolchains.md)。 + + +### 宿主代码页之外的文件名 + +glob 是窄字符串,编译命令和 `build.ninja` 也是。在 Windows 上这些字符串由进程的 +**ANSI 代码页**产生,因此一个名字在该代码页里无法拼写的文件,既匹配不了 glob,也 +写不进编译命令或构建文件。 + +这类条目会被跳过,并按目录报告一次: + +```text +warning: 'C:/.../pkg/test/www' contains names this system's active code page cannot represent + impact: those files take no part in the build + hint: Windows only: this is the process ANSI code page, which `chcp` does not change. ... +``` + +报告里给的是**最近一个代码页拼得出的祖先目录**,用通用(`/`)写法。拼不出的那个名字本身 +永远不会被打印:渲染它会抛出这条消息正在报告的同一个异常。 + +`chcp` 改的是**控制台**代码页,对此无效。若这些名字只是测试数据或文档,跳过是无害 +的——上游 tarball 里带一个日文夹具目录,在 en-US 宿主上照样构建。源文件则不然:需要 +改名,或换一台代码页覆盖得了的机器。 + +Linux 与 macOS 不做这种转换,因此那里不会跳过任何名字。一个包在一边能构建、在另一 +边报 `internal: unhandled exception` 并指向代码页,就是 mcpp#516。 + +### 2.3.1 `[build] accel` — 本次构建面向的加速器 + +```toml +[build] +accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" +``` + +本次构建为哪些设备后端与架构编译。单次构建可用 `--accel` 覆盖 —— +这与 `--target` 对 `[toolchain]` 的关系相同;`--no-accel` 是显式请求「不要加速器」, +也就是在一个同时发布了设备构建的包中选中 CPU-only 变体的方式。 + +该取值会与构建所消费的任何预建产物的 `accel` 字段比较,而请求为空的构建被任何产物满足。 +见 [42 — 异构硬件构建](42-heterogeneous-builds.md)。 + +### 2.4 `[lib]` — 库根模块约定 + +```toml +[lib] +path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置 +``` + +默认约定:`src/<包名最后一段>.cppm`(如包名 `mcpplibs.cmdline` → `src/cmdline.cppm`)。 +### 2.5 `[dependencies]`、`[dev-dependencies]`、`[build-dependencies]` + +已移入 [05 —— 依赖与解析](05-dependencies.md)。 + +### 2.7 `[toolchain]` —— 工具链配置 + +```toml +[toolchain] +default = "gcc@16.1.0" + +# 交叉编译目标覆盖 +[target.x86_64-linux-musl] +toolchain = "gcc@16.1.0" +linkage = "static" +``` +### 2.7.1 `[target.*]` —— 平台条件依赖与 flag + +已移入 [22 —— 目标侧](22-target-side.md)。 + + +### 2.7.2 裸机(`os = none`)—— freestanding target + +`riscv64-none-elf` 与 `riscv32-none-elf` 是底下没有操作系统的 target。它们不需要 +逐宿主的交叉工具链:clang 与 lld 天生是交叉编译器,任何能装 llvm 载荷的宿主都能 +产出它们。 + +本节是清单参考。示例部分 —— 生成工程、运行、在目标上测试、freestanding 标准库 +子集,以及编写板级支持包 —— 在 +[40 — 裸机与 freestanding 目标](40-baremetal.md)。 + +```bash +mcpp build --target riscv64-none-elf +mcpp run --target riscv64-none-elf # 经 [target.].runner +``` + +**从板级支持包起步** + +下面这些几乎都不需要手写。板级支持包(BSP)自带 C 库、启动代码、内存布局和模拟器, +所以跑起一个镜像的最短路径是: + +```bash +mcpp new blinky --template riscv-virt-rt +cd blinky && mcpp run +``` + +生成的 manifest 里没有链接脚本、没有加载地址、没有 libc、没有模拟器 —— 连 +`[target.*]` 段都没有。本节余下的内容讲的是**这样一个包提供了什么**,也就是要给 +一块还没有 BSP 的板子写一个时该照着做什么。 + +**freestanding target 上有什么不同** + +| | | +|---|---| +| 链接线 | `-nostdlib -nostartfiles -static`,且不带任何 hosted 的东西 —— 没有 crt 文件、没有动态链接器、没有 C++ 运行时。链接器用**绝对路径**寻址(`-fuse-ld=<载荷>/bin/ld.lld`),因为 `-fuse-ld=lld` 走 `PATH` 解析,在任何 binutils 排前面的机器上都会找到 GNU ld。 | +| ISA flag | `-march` / `-mabi` / `-mcmodel` 来自 target 表,所以只写 `--target ` 就足以产出正确的目标文件。 | +| C 库 | **属于 target**,由 mcpp 从目标自己那一行解析,和解析编译器同理 —— 裸机工程不声明 libc,正如宿主工程不声明 glibc。它的头进入每一个翻译单元,它的目录进入链接搜索路径,所以板级包用**裸名**选库(`-lc`、`-lcrt0-semihost`)。**选哪个**启动对象、**用哪份**链接脚本仍然是板级决定。 | +| 异常与 RTTI | **关闭**,作用于每一个翻译单元,依赖的也不例外。没有 unwinder、没有 `libc++abi`,谁都抛不了;否则光是 `std::optional::value()` 就会拉进 `__cxa_throw` 等四个未定义符号。它属于 **target** 而不是工程的 `cxxflags`,因为 **BMI 会记录这个配置** —— 带异常编出来的依赖,不带异常的单元 import 不进来。 | +| `import std` | **不可用。** `std` 是覆盖整个库的一个模块 —— 线程、文件系统、iostreams 全在内 —— 没有 OS 就没有它的子集可编。取代它的是两个普通依赖:**板级包**包住目标的 C 库,**`std-freestanding`** 提供标准库里不需要 OS 的那部分(实测 libc++ 110 个头里的 103 个)。 | +| 入口点 | **只要有人提供 `crt0`,`int main()` 就能用** —— 板级支持包通常就提供它,于是固件的入口就是普通的 `main`,它的返回值经 semihosting 传回宿主。**只有零 libc 的板子**才需要显式声明 target 并把 `main` 指向携带 `_start` 的那个文件。 | + +**一个最小固件** + +```toml +[package] +name = "fw" +version = "0.1.0" + +[build] +ldflags = ["-T", "/abs/path/to/link.ld"] + +[targets.firmware] +kind = "bin" +main = "src/start.S" # 入口在汇编里,不在 main() + +[target.riscv64-none-elf] +runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", + "-no-reboot", "-bios", "default", "-kernel"] +``` + +**`runner` —— `mcpp run` 如何执行本机跑不了的东西** + +裸机镜像的 ISA 不对、没有 loader、且期望独占整个地址空间;直接 exec 它得到的是 +"Exec format error"。`runner` 就是挡在它前面的 argv 模板。产物路径会被**追加**, +或者在模板含 `{}` 时替换进去。 + +mcpp **刻意不提供默认 runner**。用哪个模拟器、哪个机器型号、哪种固件模式都是板级 +事实 —— 同一 ISA 的两块板需要不同 argv(OpenSBI 启动用 `-bios default`,picolibc +镜像用 `-bios none -semihosting`)—— 引擎一旦猜一个,另一块板就得跟它打架。板级 +支持包通常会提供它。 + +### 2.7.3 hosted 目标上的 `runner`(2026.9.2.1+) + +`[target.].runner` 对每一个精确三元组生效,不限于裸机。一个 hosted 交叉产物 +—— 在 x86_64 机器上构建的 `aarch64-linux-musl` —— 有的宿主能直接执行(binfmt_misc +注册了 qemu-user),有的宿主以 `Exec format error` 拒绝;属于哪一种是机器的性质,不是 +三元组的性质。mcpp 不预测它:要么通过工程声明的 runner 执行产物,要么尝试直接执行并 +报告内核的回答。 + +```toml +[target.aarch64-linux-musl] +runner = ["qemu-aarch64-static"] +``` + +规则对 `mcpp run` 与 `mcpp test` 相同: + +- **声明了 runner 就使用它。** 其第一个元素由 mcpp 定位:先在 `[xlings.workspace]`(§2.13) + 声明的每个载荷的 `bin/` 目录里找,再找 `PATH`。`PATH` 上的裸名会命中 xvm shim,而 + shim 按当前 SubOS 而非按包作答;先查载荷,runner 才能直接写工程声明过的程序名。 +- **声明的 runner 找不到或启动不了是错误**,错误里带程序名、搜索过的目录和 errno。 + 不回落到直接执行:让产物在另一个解释器下带着另一组参数运行,正是这个键要防止的 + 失败。 +- **没有 runner 且内核拒绝产物:** `mcpp run` 报告拒绝原因与应当写的键,退出码 2。 + `mcpp test` 把每个测试报告为未运行,原因只打印一次,退出码 2(§2.7.3.1)。 +- **`--no-runner`** 直接执行产物并忽略声明的 runner。它陈述的是关于本机的事实 —— + 这个三元组在本机是原生的 —— 清单没有承载它的轴;为 x86_64 开发者写的 runner 在 + aarch64 机器上仍可用。 + +通过 `[xlings.workspace]` 装模拟器是 CI 任务或单一宿主类别工程的形态。索引里的 +`qemu-user-aarch64` 只为 x86_64 Linux 构建,而这张表在每台构建本工程的宿主上都会 +provisioning,所以条目按平台写(§2.13): + +```toml +[xlings.workspace] +"xim:qemu-user-aarch64" = { linux = "" } # Linux 上存在即可,版本不限 + +[target.aarch64-linux-musl] +runner = ["qemu-aarch64-static"] +``` + +宿主装不了的包是硬构建错误,所以不带平台形式的条目会让工程在 macOS 与 Windows 上 +无法构建。同样没有这个包的 Linux/aarch64 宿主传 `--no-runner`。 + +#### 2.7.3.1 `mcpp test` 与未运行的测试 + +产物在本机无法执行的测试既没有通过也没有失败。`mcpp test` 把它报告为**未运行**, +在确立原因时打印一次,在汇总里重复原因的第一行,退出码 2: + +``` +warning: this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); declare [target.aarch64-linux-musl].runner, or pass --no-runner on a host that can +smoke ... not run +error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); ...); finished in 0.41s (build 0.39s + run 0.00s) +``` + +退出码 1 含义不变 —— 有测试运行并失败;0 表示每个测试都运行并通过。 +`--message-format json` 在每条记录上带 `"status":"not_run"` 与 `reason`,在汇总记录上 +带 `not_run` / `not_run_reason`(见 [50 —— 机器可读输出](50-machine-output.md))。 +### 2.8 `[features]` —— Feature + +已移入 [06 —— Feature 与能力](06-features-and-capabilities.md), +连同 `provides` / `requires` 与 `[feature-deps.]`。 + + +### 2.8.3 `[scan_overrides.""]` —— 作者断言的扫描结果 + +默认的模块扫描器是文本级的一遍扫描,它(刻意地)拒绝条件预处理块内部的 `import` +语句。有些合法的模块单元带着这种写法 —— 例如 fmt 官方的 `src/fmt.cc` 把 +`import std;` 收在 `#ifdef FMT_IMPORT_STD` 之后。当该文件的 import 集合已知且稳定时, +用声明取代扫描: + +```toml +[modules] +sources = ["src/**/*.cppm", "vendor/fmt.cc"] + +[scan_overrides."vendor/fmt.cc"] +provides = ["fmt"] # 每个单元至多提供一个模块 +imports = ["std"] +``` + +被 glob 命中的文件跳过文本扫描,声明的单元直接进入模块图。该声明**每次构建都被审计**: +编译器自己对该文件的 P1689 扫描结果(`.ddi` dyndep 输入)会与之比对,任何分歧都会让 +那条编译边失败并打印双方 —— 陈旧的声明无法静默污染模块图。未命中任何源文件的 +override glob 是错误。 + +同一个键在 xpkg 描述符(索引包)中同样存在: + +```lua +mcpp = { + sources = { "*/src/fmt.cc" }, + cxxflags = { "-DFMT_IMPORT_STD" }, + scan_overrides = { + ["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } }, + }, +} +``` + +要把 plan 与 ddi 的比对审计扩展到**每一个**模块单元(而不只是 override), +在生成构建时设置 `MCPP_VERIFY_MODGRAPH=1`。 + +### 2.9 `[profile.]` — 构建档案 + +```toml +[profile.dist] +opt = 3 # -O 级别(数字或 "s"/"z" 字符串) +debug = false # -g +lto = true # -flto(注意:部分打包 gcc 未启用 LTO 插件) +strip = true # 链接期 -s +# passthrough 逃生口(固定键、开放值): +cflags = ["-fno-plt"] +cxxflags = ["-fno-plt"] +ldflags = [] +``` + +- 选择与默认:裸 `mcpp build` 走 **`dev`** 档(`-O0 -g`)——主流惯例(参照 + Cargo/Meson/CMake/Zig/Bazel)。**release 为 opt-in:** `mcpp build --release`(短写)或 + `--profile release`;`--dev` 是 dev 的显式短写。`mcpp test --profile ` 同理 + (被测代码与测试二进制都在该 profile 下编译)。 +- **项目级默认** —— `[build].default-profile = ""`(别名 `profile`)设置该项目在不带 + flag 时的默认。典型用途是"以发布优化为常态"的工具/库:`[build] default-profile = "release"`。 + 优先级:`--profile`/`--release`/`--dev` flag **>** `[build].default-profile` **>** 全局 `dev`。 + (默认 dev 的项目在产出可分发物时应显式 `--release`。) +- 内置档案:`release`(-O2)/ `dev`、`debug`(-O0 -g)/ `dist`(-O3 + strip; + **不默认开 lto**)。`[profile.<内置名>]` 可整体覆盖内置定义。 +- **每个 profile 各占一个构建目录。** 解析后的 profile 开关参与指纹,所以 + `target//` 下每个 profile 一个哈希目录,来回切换是增量而不是全量重编; + 代价是磁盘占用随实际使用的 profile 数量增长。 + +### 2.10 `[build] cache` — 依赖的全局构建缓存 + +从索引获取的依赖,其编译产物按包缓存在 `$MCPP_HOME/build-cache/v1/` 下,跨工程共享。 +依赖的产物与"谁在消费它"无关,所以工具链、profile、依赖版本相同的两个工程复用同一条目。 + +```toml +[build] +cache = "global" # "global"(默认)| "local" | "off" +``` + +| 模式 | 读缓存 | 写缓存 | 先清构建目录 | +|---|---|---|---| +| `global`(默认) | 是 | 是 | 否 | +| `local` | 否 | 否 | 否 | +| `off` | 否 | 否 | 是 | + +`local` 把所有依赖都编在本工程 `target/` 内 —— 排障时一次性排除"是不是缓存的问题", +也给 CI 一个无共享的可复现基线。`off` 额外清掉本次的 `target///` 做冷构建; +`--no-cache` 是它的兼容别名。 + +优先级:`--cache ` **>** `MCPP_BUILD_CACHE` **>** `[build] cache` **>** `global`。 +无法识别的值会被报出来(`--strict` 下为错误),而不是静默回落到 `global`。 + +**不进缓存的**:`path` 与 `git` 依赖(任意深度)以及 workspace 成员。它们的源码可以在 +`name@version` 不变的情况下改变,任何基于该身份的键都看不见这种变化。 + +查看与回收: + +``` +mcpp cache dir # 缓存在哪 +mcpp cache list [--json] # 条目、体积、最后使用时间 +mcpp cache info @ # 单条目详情,含它是用什么键输入编出来的 +mcpp cache verify # 逐条目校验清单与磁盘 +mcpp cache gc --max-size 5GiB # 按 LRU 收到容量预算内 +mcpp cache gc --older-than 30d # 或按"多久没用过"回收 +mcpp cache clean [--deps|--std|--all|--legacy] +``` + +条目的磁盘布局是带版本的。改动布局的 mcpp 版本会**一次性作废全部旧条目**, +所以升级后的第一次构建会重编依赖并重新填充 —— 不需要手工清理。 +2026.8.3.4 就是这样一次:条目里对象的地址现在相对**包**自身, +而不再相对"最先填充这个条目的那个工程"的构建目录。 +`mcpp cache verify` 另外会报告任何逃出条目的记录地址, +使这条不变量可以离线审计。 + +### 2.11 `[runtime]` — provider-neutral 运行时契约 + +```toml +[runtime] +requirements = [ + { kind = "capability", value = "display.present", phase = "run", required = true }, + { kind = "soname", value = "libwidget.so.1", phase = "link", required = false }, +] +provides = ["display.present"] +artifacts = [ + { role = "library", path = "runtime/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:...", host_fingerprint = "host-1" }, +] + +# 平台无关 LinkIntent;路径相对本包根目录。 +libraries = ["widget"] +link_library_dirs = ["lib"] +transitive_needed_dirs = ["runtime/closure"] +runtime_search_dirs = ["runtime"] +frameworks = ["WindowKit"] +deploy_files = ["bin/widget.dll"] + +# 多 provider 时使用精确 canonical identity。 +[runtime."display.present"] +provider = "acme.widget-runtime@2.0.0" +``` + +`requirements` 记录非空 `kind`/`value`、`link` 或 `run` 阶段,以及是否强制 +(`required` 默认 `true`)。`artifacts` 必须含 `role`、`path`、`provenance`; +可选 requirement 仍保留为 provenance,但不会进入硬 ABI/doctor 输入。 +`libraries` 中显式的相对文件路径按声明包根目录解析;裸逻辑名仍按目标平台拼成库名。 +`abi`、`digest`、`host_fingerprint` 是可选证据。requester/provider 身份不由描述符 +填写:resolver 会用含 namespace、version、source/index provenance 的精确 PackageId +给 requirement 和 artifact 盖章。因此描述符不能冒充别的包, +`alpha.backend` 也不会与 `beta.backend` 混同。 + +只有 `provides` 会创建描述符侧 provider fact;需要某能力绝不会让 requester 自动 +成为 provider。显式 `[runtime.] provider=` 接受 canonical +`namespace.name@version`(或唯一无歧义的兼容拼写);不存在或同短名歧义都会 hard error。 +xlings SubOS 已选择的 provider/artifact fact 排在描述符 fallback 前。图形栈、driver、 +ICD、WSL 与 host provenance 选择由 xlings/xim 负责;mcpp 只记录、消费通用结果, +不探测 GPU 硬件。 + +LinkIntent 把不同发现阶段分开: + +| 字段 | ELF | Mach-O | PE/Windows | +|---|---|---|---| +| `link_library_dirs` | `-L` | `-L` | `-L` 或 `/LIBPATH:` | +| `transitive_needed_dirs` | `-Wl,-rpath-link` | 无 flag | 无 flag | +| `runtime_search_dirs` | 只进 RUNPATH/rpath,绝不进 `-L` | 只进 rpath | 无 flag | +| `frameworks` | 无 flag | `-framework` | 无 flag | +| `deploy_files` | copy edge | copy edge | 复制到产物旁,绝不成为 linker flag | + +一个兼容发布周期内仍读取旧字段:`library_dirs` 只映射到运行期搜索; +`dlopen_libs` 映射为必需的 run-phase soname requirement;`capabilities` 映射为必需的 +run-phase capability requirement。这些旧字段都不会创建 provider。 + +`target///resolution.json` schema 2 持久化 RuntimeBinding、canonical +requirements/providers/artifacts、LinkIntent、平台搜索机制与链接后 verdict。 +`mcpp why runtime` 只是最新存储文件的纯解释器:不重新解析 manifest,也不启动图形/ +硬件 probe。需要重新诊断所选 host provider 时使用 `xlings doctor`。 + +每个 artifact 还带一个仅由路径算出的 `identity` 判定: + +| `identity` | 含义 | +|---|---| +| `ok` | 声明的路径(穿过符号链接后)落在声明的那个版本里 | +| `mismatch` | 它解析到了别处 —— **该 binding 已陈旧**,后来的某次安装把它重新指向了别的地方 | +| `missing` | 声明了,但那个路径上什么都没有 | +| `unverified` | 声明时没有可供比对的版本 | + +这就是 mcpp 早已施加于私有 libc 的那条规则的推广(`glibc@2.44` 解析到那一份载荷; +陈旧或缺失是错误,而绝不是「已安装版本里哪个看起来能用就用哪个」)。它不需要知道 +该 artifact 做什么。`unverified` **有意**不等于 `ok`:一个解析到了却没有 artifact +在其背后的 provider 并未被核验过,因此 `mcpp why runtime` 打印 +`(not declared by the environment — nothing to verify)` 而不是 `(none)`。 + +能力名使用分层小写 `domain.sub.role`(如 `display.present`)和前缀类 +`abi:`(如 `abi:glibc`,参与工具链 ABI 强制)。 + +### 2.12 `[package] platforms` — 平台声明 + +```toml +[package] +platforms = ["linux", "macos", "windows"] +``` + +声明包支持的平台(CI 矩阵提示,经 `mcpp why` 展示)。词表由 mcpp 固定 +(它拥有 target/triple 体系):`linux | macos | windows`;未知值 warning, +`--strict` 下报错。 + +对库目标执行 `mcpp pack` 时,会拿这条声明与**实际产出的腿**核对 —— 那是第一个 +有证据可核的时刻: + +| 情况 | 结果 | +|---|---| +| 某条腿的平台不在此列 | warning —— manifest 否认了一个包明明能服务的平台 | +| 声明了某平台却没有对应的腿,**且本宿主本来就能构建它** | warning —— 该平台的消费者会解析到这个包却找不到产物 | +| 声明了某平台却没有对应的腿,而本宿主根本构建不了它 | **不说话** | + +第三行才是这个检查可用的原因。正常的发布流程是 CI 上每平台各跑一次 +`mcpp pack`,于是 Linux runner 永远不会产出 macOS 腿 —— 为此告警会在每个跨平台 +包的每一次运行中触发,而**永远触发的告警会把真正该看的那条盖掉**。「本宿主能不能 +构建」与 `--target` 回答的是同一个问题(docs/08 §7.4)。 + +两者都只是 warning,绝不报错:覆盖度属于发布纪律,而能作判断的人看的是发布, +不是这一次构建。 + +### 2.12b `[package] accelerators` — 加速器声明 + +```toml +[package] +accelerators = ["cuda", "rocm"] +``` + +声明该包支持的加速器后端。与 `platforms` 同形:一个意图声明与 CI 矩阵提示, +由 `mcpp why` 展示,**不是门**。 + +与产物的 `accel` 字段刻意不同。声明由人手写、可以是期望值;`accel` 是从产生该二进制的 +那次构建测量出来的,并且是消费者被拒绝时所依据的东西。见 +[42 — 异构硬件构建](42-heterogeneous-builds.md)。 +### 2.13 `[xlings]` —— 工程的环境 + +已移入 [23 —— 项目环境](23-the-project-environment.md)。 + +### 2.14 依赖产出的 host 工具 + +已移入 [30 —— build.mcpp](30-build-mcpp.md)。 + + +### 2.15 `[resources]` —— 编译进产物的元数据与资产(2026.8.7.1+) + +exe 图标,以及 Windows 在文件「属性」里显示的版本信息,就是 `mcpp.toml` 里的一个路径: + +```toml +[resources] +icon = "assets/app.ico" +``` + +常见场景到此为止。`FILEVERSION`、`ProductName`、`FileDescription`、`CompanyName`、 +`LegalCopyright` 全部从 `[package]` 取默认值,资源脚本由 mcpp 生成。 + +| 键 | 类型 | 含义 | +|---|---|---| +| `icon` | 路径 | 作为应用图标嵌入(资源序号 1) | +| `files` | 路径列表 | 工程自带的 `.rc` 脚本,mcpp 编译并**跟踪**为构建输入 | +| `extra-inputs` | 路径列表 | `.rc` 扫描器看不见的输入(见下) | +| `version-info` | 布尔 | `false` 表示不要生成版本资源 | +| `[resources.version-info]` | 表 | `company`、`product`、`description`、`copyright`、`original-filename`、`internal-name` | + +**只有 PE 目标会*编译*这一节。** 在 Linux/macOS 上它**不适用**:不产资源单元、 +不出诊断、构建逐字节不变。**无需**(也不能)加 `cfg(windows)` 谓词 —— +无条件写一次即可。 + +**声明了却不存在的文件会让构建失败 —— 在每个目标上都是。** 资源和源码一样是 +构建输入;mcpp 不会悄悄产出一个缺了它的二进制。校验刻意**不**按 PE 设门: +路径是否存在是关于工作树的事实,不是关于目标的事实,所以 `icon = "assets/app.ico"` +里的拼写错误由 Linux/macOS 构建(以及对应的 CI job)当场抓住,而不是等 +Windows 那条。不想要图标,把那一行删掉。 + +**版本字段。** `FILEVERSION` 取 `[package].version` 的四段数值,每段必须放得进 +16 位;字符串字段保留版本原文,所以数值字段装不下的形态(`1.0.0-rc1`)在属性 +对话框里照样看得到。 + +#### 自写 `.rc` + +```toml +[resources] +files = ["res/app.rc"] +``` + +写了 `files`,mcpp 就不再生成版本资源 —— 资源 ID 空间由工程自行支配。两者都需要时同时写 +`version-info = true`(注意冲突:序号 1 的 `RT_VERSION` 只能有一个)。 + +想从生成的脚本起步而不是从空文件起步:把它从构建目录里拷出来 +(`target///res/.mcpp.rc`)填进 `files`。结果**字节相同**, +所以从「生成」走到「手写」不会改变产物。 + +> **`VS_VERSION_INFO` 需要 ``。** 手写脚本里如果写 +> `VS_VERSION_INFO VERSIONINFO` 而没有 `#include `,版本资源会被存成 +> **字符串名**而不是序号 1。所有工具依然报告 `Type: VERSIONINFO`,但 +> `GetFileVersionInfo` 查的是序号,于是 PowerShell 的 `FileVersionInfo` 里每个字段 +> 都是空的。要么 include ``,要么直接写 `1 VERSIONINFO`。mcpp 见到这个 +> 形状会警告;它自己生成的脚本用的是字面 `1`。 + +#### 被跟踪的输入 + +mcpp 会读 `.rc`,把引号形式的 `#include` 和资源语句(`ICON`、`RCDATA`、 +`MANIFEST` …)点名的文件都变成构建输入,所以改图标会重链。尖括号形式 +(``)属于工具链,由工具链 fingerprint 覆盖。 + +通过宏间接引用的文件名(`1 ICON APP_ICON`)扫描看不见。mcpp 会**指名**它没能解析 +的东西,并要求显式声明: + +```toml +extra-inputs = ["assets/app.ico"] +``` + +#### 其余一切:`role = "object"` + +不是资源脚本的输入 —— `objcopy` 嵌入的 blob、生成的 `.def`、预编译对象 —— +可以由构建程序声明一个产出接到链接的图节点: + +```cpp +mcpp::action o; +o.id = "blob"; o.role = "object"; +o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o") + .input("blob.bin") + .output("${mcpp.out_dir}/blob.o") + .target("myapp") // 省略:接到每个镜像,含测试二进制 + .submit(); +``` + +见 [30 — build.mcpp](30-build-mcpp.md)。把这类文件写进 `[build].ldflags` 也「能用」, +但 ldflags 是链接命令里的一串字符:没有任何东西跟踪它,改了它得到的是 +`ninja: no work to do`。 +### 2.16 `[hooks]` —— 项目构建生命周期命令 + +已移入 [09 —— 按场景选命令](09-commands-by-scenario.md)。 + + +## 3. 实战示例 + +其中四个是**可运行的工程**而不是片段,而工程是更好的答案:它能构建,而且由 CI 检查。 + +| 形态 | 运行 | +|---|---| +| 一个 hello world | [`examples/01-hello`](../../examples/01-hello/) | +| 带测试的模块化库 | [`examples/11-features`](../../examples/11-features/) | +| 带依赖的应用 | [`examples/02-with-deps`](../../examples/02-with-deps/) | +| 交叉编译的静态发布 | [`examples/03-pack-static`](../../examples/03-pack-static/) | + +还有两种形态暂时没有对应示例,以 manifest 的形式留在这里。 + +### 3.4 纯 C 库 + +```toml +[package] +name = "myc" +version = "0.1.0" + +[build] +c_standard = "c99" +include_dirs = ["include"] +sources = ["src/**/*.c"] + +[targets.myc] +kind = "lib" +``` + +### 3.5 混合 C / C++23 模块项目 + +```toml +[package] +name = "hybrid" +version = "0.1.0" + +[build] +include_dirs = ["include"] +c_standard = "c11" + +[dependencies] +lua = "5.4.7" # 纯 C 库,mcpp 自动用 C 编译器编译 .c 文件 + +[targets.hybrid] +kind = "bin" +``` + + +## 4. 约定与默认值速查 + +| 项目 | 默认值 | 说明 | +|---|---|---| +| 源文件 | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | 自动递归扫描 | +| 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 | +| 库根 | `src/.cppm` | 可用 `[lib].path` 覆盖 | +| C++ 标准 | `c++23` | 用 `[package].standard` 配置; 支持 `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly`(实验试验场) | +| C 标准 | `c11` | `.c` 文件自动走 C 编译器 | +| 静态 stdlib | `true` | 便携二进制 | +| 头文件 | `include/`(如果存在) | 自动加到 `-I` | +| 测试 | `tests/**/*.cpp` | `mcpp test` 自动发现 | +| 依赖命名空间 | `mcpplibs`(默认) | 裸 selector 只表示该精确 ns | + +### 4.1 旧 `[language]` 兼容层 + +旧配置仍可读取: + +```toml +[language] +standard = "c++26" +``` + +新项目请使用 `[package].standard`。如果两个位置都出现,`[package].standard` 是权威配置。 diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md new file mode 100644 index 00000000..1909c6dc --- /dev/null +++ b/docs/zh/05-dependencies.md @@ -0,0 +1,271 @@ +# 05 —— 依赖与解析 + +**读者:**构建里已经不只有自己代码的作者。 + +**本章回答的那一个问题:**一个依赖从哪里来,版本约束是什么意思,以及两个约束 +不一致时会发生什么。 + +**不在这里:**什么使两个包成为同一个包 —— 那是 +[SPEC-001](../specs/package-identity.md),本章施用它而不复述它;以及怎么发布一个包, +那是 [11 —— 发布一个库](11-publishing-a-library.md)。 + +在此之前:[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md) 是这些表与其余表同处的 +地方。在此之后:[06 —— Feature 与能力](06-features-and-capabilities.md) 讲一个依赖 +怎么变成可选的。 + +## `[dependencies]` — 运行时依赖 + +**推荐写法是带精确版本的 dotted selector。** dotted 形式点名的是**一个身份** —— +最后一段是包名,之前所有段都是 namespace —— 因此解析到什么,不取决于当前配置了 +哪些 namespace。 + +```toml +[dependencies] +compat.gtest = "1.15.2" +imgui.core = "0.0.1" +imgui.backend.glfw_opengl3 = "0.0.1" +mcpplibs.capi.lua = "0.0.3" +``` + +裸名也被接受,它经由默认 namespace(`mcpplibs`)解析 —— 在只用那一个 namespace 的 +工程里方便,在不是的工程里有歧义: + +```toml +[dependencies] +cmdline = "0.0.2" # 解析为 mcpplibs.cmdline +``` + +
+等价拼法:namespace 子表 + +把同一 namespace 下的条目归组。它解析到的身份与 dotted 形式完全相同,在许多依赖 +共享一个 namespace 时值得用。 + +```toml +[dependencies.mcpplibs] +cmdline = "0.0.2" +tinyhttps = "0.2.2" +llmapi = "0.2.5" + +[dependencies.compat] +glfw = "3.4" # 显式 namespace,不做回退搜索 +``` + +
+ +```toml +# 路径依赖(本地开发) +[dependencies] +mylib = { path = "../mylib" } +``` + +```toml +# Git 依赖 —— tag / branch / rev 三选一 +[dependencies] +mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } +applib = { git = "https://github.com/user/applib.git", branch = "develop" } +``` + +```toml +# 长式 dep spec:features 与 backend 旋钮 +[dependencies] +imgui = { version = "0.0.3", features = ["docking"] } # 请求该依赖的 feature +widget = { version = "1.0", backend = "glfw_opengl3" } # 糖:= features=["backend-glfw_opengl3"] +``` + +`backend = ""` 是**通用约定糖**:1:1 脱糖为请求该依赖的 `backend-` +feature(库若支持该旋钮,应在自己的 `[features]` 中声明 `backend-*` 系列)。 +若目标包声明了 `[features]` 但不含所请求的 feature(含 backend 脱糖结果), +默认给出 warning,`mcpp build --strict` 下报错。 + +**Git 依赖与 `mcpp.lock`**:`tag` 和 `rev` 本身就指向历史中的固定点,而 `branch` +是会动的。首次构建把分支解析成一个 commit 并写进 `mcpp.lock`,此后每次构建都重建 +**那个** commit —— lock 是权威而不是缓存提示,所以删掉 `~/.mcpp/git` 或换一台机器 +都不会静默切换到更新的分支头。需要新的分支头时,必须显式指定: + +```bash +mcpp update mylib # 丢掉记录的 commit,下次构建重新解析 +mcpp update # 同上,对所有依赖 +``` + +既然记录的 commit 已经足以决定构建什么,那么在 `~/.mcpp/git` 里已有克隆的情况下, +重新构建完全不发网络请求,`--offline` 下照常工作。只有两件事需要网络:解析一个在 +lock 里没有 commit 的分支,以及克隆一个尚未缓存的 commit。`git =` 若指向本地目录 +(或 `file://` URL),这两件事都不需要网络,因此离线下也绝不会被拒绝。 + +**SemVer 约束**: + +```toml +[dependencies] +foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret,默认) +bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde) +baz = "=1.2.3" # 精确匹配 +qux = ">=1.0, <2.0" # 范围组合 +``` + +### 命名空间解析规则 + +每个包的身份是**命名空间 + 名字**二元组。每个 selector 都只规范化成一个身份: + +- `cmdline` → `(mcpplibs, cmdline)`;省略 namespace 只表示默认 `mcpplibs`。 +- `compat.gtest` → `(compat, gtest)`。 +- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`。 + +不存在有序回退或按短名的全索引模糊搜索: + +```toml +# 正确 —— 点式选择器 +[dependencies] +chriskohlhoff.asio = "1.38.1" + +# 正确 —— 命名空间子表(同一组织有多个包时更推荐) +[dependencies.chriskohlhoff] +asio = "1.38.1" + +# 错误 —— 裸名永远到不了 chriskohlhoff 命名空间 +[dependencies] +asio = "1.38.1" +``` + +第三种写法会明确报错,指出实际尝试的 `(mcpplibs, asio)`;若该短名存在于别处,错误信息会给出可直接复制的显式 selector。 + +#### 裸名过渡期(`2026.8.10.1` 起,`2026.9` 移除) + +索引里已发布的 `compat.*` 包与既有 manifest **全部**写成裸名(`gtest = "1.15.2"`)。 +升级后直接失败,等于让一次程序发布把**已经发布、且无法追溯修改**的数据作废, +所以有一个版本的过渡期:裸名在 `mcpplibs` 未命中时仍可到达 `compat.`, +不声明 namespace 的 descriptor 也仍可被裸名解析。 + +但它不再静默: + +``` +warning: dependency 'gtest' resolved to 'compat.gtest' through the deprecated +bare-name search; namespace omission means `mcpplibs` only. Write the exact +package: + [dependencies.compat] + gtest = "1.15.2" + (or run `mcpp add compat.gtest@1.15.2`). This fallback is removed in 2026.9. +``` + +写进 `mcpp.lock`、install 与 cache 的是**规范身份**;歧义拼写只存在于工程的 +`mcpp.toml` 中,直到被改写 —— `mcpp add gtest@1.15.2` 会完成这次改写。 + +过渡期**不适用于**写明 namespace 的 selector(`mcpplibs.gtest` 未命中就是未命中), +裸名也**仍然**到不了第三方 namespace。 + +**为什么只允许一个身份?** 因为依赖解析必须可复现。候选搜索会让同短名包受索引状态影响,新增索引还可能悄悄重定向既有依赖。 + +**给 xpkg 作者:** 索引描述符里,身份是 `(package.namespace, package.name)` 二元组。命名空间是点分路径,**`name` 是单一原子段**: + +```lua +package = { + namespace = "chriskohlhoff", + name = "asio", -- 单一段;不是 "chriskohlhoff.asio" +} + +package = { + namespace = "mcpplibs.capi", -- 层级放这里 + name = "lua", +} +``` + +文件名只是提示 —— 描述符按声明的身份被发现,所以 `pkgs/c/chriskohlhoff.asio.lua` 与 `pkgs/z/anything.lua` 解析结果完全相同。推荐 `.lua` 或 `..lua`(命中 mcpp 的快路径),但不强制。 + +旧的完全限定拼写(`name = "chriskohlhoff.asio"`)仍被接受,已发布的描述符无需改动。`mcpp xpkg parse` 会校验该规则,请在索引 CI 里跑它。描述符身份需要 mcpp >= 0.0.106,精确 selector 需要 mcpp >= 2026.8.10.1,两者使用 xlings >= 0.4.69;规范全文见 `docs/specs/package-identity.md`。 + +`mcpp new --template` 刻意复用同一身份模型,而不是另造包文法: +`[ns.]name[@version][:tname]`。其中裸名同样只表示 `mcpplibs`,version 与模板名可分别 +省略。省略 `tname` 时选择唯一显式 default;若未写 `default = true` 且只有一个模板, +该单模板自动成为默认。多个未标默认的模板会报错,绝不按目录顺序选择。规范表见 +`docs/specs/package-identity.md` §4.4。 +### mcpp 何时刷新包索引 + +`mcpp build` / `run` / `test` **只在依赖无法用本地索引解析时**刷新包索引,绝不会 +因为"时间到了"就刷。具体地说,只有三种情况会触发:本地根本没有索引、依赖的描述符 +不在其中、或 SemVer 约束在本地已知版本里无解。只要所有依赖都能在本地解析出来, +无论本地索引多旧,构建都不会发起任何网络请求。 + +由此带来的一个需要知道的语义:`^1.2` 这类约束是对**本地索引已知的版本**求解的。 +若上游在上次刷新之后发布了 `1.3.0`,需要主动获取: + +```bash +mcpp index update # 同步索引 +mcpp update # 同步索引,并重新解析依赖 +mcpp index status # 看本地现状:状态、年龄、修订号 +``` + +三个开关,优先级从高到低: + +| 开关 | 作用 | +|---|---| +| `--offline`(任意命令) | 完全不碰网络——不刷索引、不下载、不自动装工具链,也不发 `git ls-remote`/`clone`。已安装的东西照常构建,包括 commit 已在 `mcpp.lock`、克隆已在缓存里的 git 依赖 | +| `MCPP_OFFLINE=1` | 同上,作用于整个 shell 会话或 CI job | +| `~/.mcpp/config.toml` 里 `[index] auto_refresh = false` | 永不自动刷新索引,但下载仍然可用 | + +`MCPP_NO_AUTO_INSTALL=1` 作为 `--offline` 的旧式窄化拼写仍然有效(它只管工具链的 +自动安装)。 + +任意命令加 `-v` 可以看到每个依赖的判定结果与原因。 + +## `[dev-dependencies]` —— 测试依赖 + +```toml +[dev-dependencies.compat] +gtest = "1.15.2" +``` + +`mcpp build` 忽略这些依赖;`mcpp test` 解析并使用它们。`mcpp test` 自动发现 +`tests/**/*.cpp` 并把它们编译为测试二进制。运行器与框架无关:每个文件是一个独立的 +二进制,以退出码判定 —— 裸 `main`、gtest(经 `[dev-dependencies]` + `gtest_main`) +或任何其他框架的行为完全一致,`-- args` 会转发给每个测试二进制 +(例如 `-- --gtest_filter=...`)。注意:合成的测试目标名可能包含 `/` +(`tests/00-a/0.cpp` → `00-a/0`),这与 `[targets.*]` 名不同 —— 两个命名空间是 +刻意分开的(测试目标从不进入 manifest,也不参与发布)。测试按其相对 `tests/` 的 +路径命名(`tests/00-a/0.cpp` → `00-a/0`),每个测试独立编译(一个测试写坏只让它 +自己失败;包或依赖损坏则报告为构建错误),`mcpp test ` 与 +`--message-format json` 分别提供过滤与机器可读输出。 + +## `[build-dependencies]` —— 构建期依赖(mcpp 2026.8.29.1+) + +```toml +[build-dependencies] +protobuf = { version = "35.1", tools = ["protoc"] } +``` + +段与边上的请求回答的是**两个不同的问题**,把它们混为一谈是建模上的错误,不是写法之争。 + +- **段**回答:这个包本身进不进目标。`[dependencies]` 进;`[build-dependencies]` 永不进, + 只能经由它到达的东西也一样。 +- **边上的请求**回答:要它的哪一种构建期产物。`tools = [...]` 要一个宿主可执行文件, + `host-module = true` 要一个构建程序可以 import 的模块。 + +一个包可以同时在两个轴上取值,而 protobuf 正是证明这两个轴必须分开的例子:工程既链接 +`libprotobuf`,构建期又需要 `protoc`。它只写一次,写在 `[dependencies]` 里: + +```toml +[dependencies] +protobuf = { version = "35.1", tools = ["protoc"] } +``` + +`[build-dependencies]` 用于第一个轴无法表达的那种组合 —— 某个包的库不得进入目标, +而它的工具或规则仍然需要。同一个包同时出现在两张表里不是错误:普通声明胜出,因为 +一行 `[build-dependencies]` 不应该悄悄拿掉目标真正需要的库。 + +与 `[dev-dependencies]` 不同,这些依赖**会**被传递遍历:一个构建期依赖自己的依赖正是 +让它能工作的东西,并且继承它「只服务构建」的性质。 + +feature 可以为构建期请求划定范围而无需第二个声明处 —— `[feature-deps.]` 可以给 +一条已经无条件声明的依赖追加 `tools`,所以「按需才要」不需要另开一张表。 + +> 这个段很早就能被解析,而直到 2026.8.29.1 之前没有任何做决定的代码读它:写下它得到的是 +> 一份能加载的清单、零诊断、零效果。 + +## 当前边界 + +- **只有两件事需要网络,也只有这两件:**解析一个在锁里没有 commit 的分支,以及克隆 + 一个尚未缓存的 commit。指向本地目录或 `file://` URL 的 `git =` 两者都不需要,因此 + 离线时从不会被拒绝。 +- 索引刷新窗口**不适用于写明了 namespace 的选择器**。`mcpplibs.gtest` 一旦未命中就 + 保持未命中,直到下一次刷新。 +- `mcpp.lock` 记录并核验一次解析,但不约束解析。见 + [51 —— 受支持的版本与兼容性](51-supported-versions.md)。 diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md deleted file mode 100644 index bdff7b08..00000000 --- a/docs/zh/05-mcpp-toml.md +++ /dev/null @@ -1,2672 +0,0 @@ -# mcpp.toml 工程文件指南 - -`mcpp.toml` 是 mcpp 构建工具的项目配置文件,类似 Cargo 的 `Cargo.toml` 或 Node 的 `package.json`。放在项目根目录下,`mcpp build` 会自动发现并读取它。 - -## 1. 最小化示例 - -mcpp 的设计原则是 **约定优于配置** —— 大多数字段都有合理默认值,最简单的 `mcpp.toml` 只需几行: - -### 1.1 可执行程序(最简) - -```toml -[package] -name = "hello" -version = "0.1.0" -``` - -mcpp 自动推断: -- 源文件: `src/**/*.{cppm,cpp,cc,c,S,s,asm}` -- 入口: `src/main.cpp` → 生成 `hello` 二进制 -- 标准: C++23 -- 模块: 扫描 `export module ...` 声明自动建立依赖图 - -### 1.2 库项目(最简) - -```toml -[package] -name = "mylib" -version = "0.1.0" - -[targets.mylib] -kind = "lib" -``` - -lib-root 约定:主模块接口默认在 `src/mylib.cppm`(包名的最后一段)。 - -## 2. 完整字段参考 - -### 2.1 `[package]` — 包元数据 - -```toml -[package] -name = "myapp" # 包名(必填) -version = "0.1.0" # 语义化版本(必填) -standard = "c++23" # C++ 标准(默认 c++23; 可设 c++20 / c++26) -description = "My awesome app" # 简介(可选) -license = "MIT" # 许可证(可选) -authors = ["Alice", "Bob"] # 作者列表(可选) -repo = "https://github.com/user/myapp" # 仓库地址(可选) -``` - -`standard` 是 C++ 语言标准的一等配置。推荐值: - -- `c++23`:默认值,适合当前模块化默认模板。 -- `c++20`:mcpp 接受的最低档位——命名模块本身是 C++20 特性,再往下这套构建模型就不存在了。当外部约束(公司内规、只到 C++20 的第三方 API)必须压低档位时使用。**`import std;` 在这一档依然可用**:它虽然是 C++23 的*库*特性,但 GCC(≥ 15)、Clang + libc++(≥ 17)与 MSVC STL(VS 2022 17.8 起)都在 C++20 模式下提供 `std` 模块。代价是 C++23 库设施(`std::print`、`std::expected` 等)不可用——包括 `mcpp new` 生成的模板代码。 -- `c++26`:需要 C++26 语言特性时使用。 -- `c++2a` / `c++2c`:兼容别名,解析后分别归一为 `c++20` / `c++26`。 -- `gnu++20` / `gnu++23` / `gnu++26`:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。 -- `c++latest`:跟随当前 mcpp 支持的最新标准,适合本地试验,不推荐要求可复现的发布包使用。 -- `c++fly`:`c++latest` **再加上该工具链能开启的全部实验性标准特性**(语言 + 标准库)。GCC ≥ 16 上会打开 C++26 反射(`-freflection`)与契约;Clang/libc++ 上追加 `-fexperimental-library`;不支持的门会跳过并打印 summary。刻意是工具链相关的——最前沿的试验场模式,永远不要用于发布包。 - -两条需要知道的性质: - -- **标准是模块图全局的。** 根包的 `standard` 作用于本次构建的每一个 TU,依赖也不例外—— - 依赖自己 manifest 里的 `standard` 在它作为依赖被构建时不生效。这不是简化:BMI 跨档位 - 不兼容(GCC 直接报 `language dialect differs`),同一张图物理上不可能存在两个档位。 -- **档位之间从不共用缓存。** 标准同时进入 fingerprint、`import std` 的 BMI 身份和依赖构建 - 缓存键,所以在 `c++20` 与 `c++23` 之间切换只会各自拿到独立的产物目录和独立的 std BMI, - 不会出现错误命中。 - -如果源码在某个档位上 `import std;` 而解析出的工具链在该档位不提供 `std` 模块, -mcpp 会在编译前失败,并同时报出工具链与工程档位。 - -值的两种拼法都接受:`standard = "c++26"` 与 `standard = 26`。 - -当**依赖声明的档位高于当前图**时,mcpp 会在编译前说出来,而不是让它在那个依赖的源码里 -某处失败。见 [workspace §4.2](06-workspace.md)。 - -#### 方言标志与 `import std` BMI - -有些标志会改变标准库头文件**声明出什么**,因此预编译的 `import std` BMI 也必须带着它们一起 -构建。这就是 `[build] dialect_cxxflags` 的用途:它会被施加到 std BMI 预编译、模块扫描 -**以及**图中每一个 TU(依赖也包括在内)。 - -```toml -[build] -dialect_cxxflags = ["-fno-exceptions"] -``` - -其中少数几个标志,mcpp 在 `cxxflags` 里发现时会自动提升进这条通道 -(`-freflection`、`-fchar8_t`、`-D_GLIBCXX_USE_CXX11_ABI=…`)—— 混用这些标志的图本来就是 -病态的,任何依赖都不可能对它们持有另一种自洽的意见。 - -`-fno-exceptions` 与 `-fno-rtti` **不会**被自动提升,因为依赖可以合法地不同意:它们移除的是 -依赖可能正在使用的语言设施,而消费者无权替它做这个决定。留在 `cxxflags` 里,它们会到达每一个 -TU 却到不了预编译,于是构建不可能成功 —— mcpp 在编译前就拒绝,并指出该用哪个键: - -``` -error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is - precompiled without it, so every importing translation unit will fail with - "language dialect differs". - Declare it as a dialect flag instead: - - [build] - dialect_cxxflags = ["-fno-exceptions"] -``` - -这项检查读的是**生效后的**标志集合,所以同一个标志写在 `[profile.] cxxflags` 或 -`[target.…]` 块里同样会被抓到。而当图中根本没有 `import std` 时它不触发 —— 那里它就是一个 -正常工作的按 TU 选项。 - -### 2.2 `[targets.]` — 构建目标 - -```toml -# 可执行程序(默认,有 src/main.cpp 时自动推断) -[targets.myapp] -kind = "bin" -main = "src/main.cpp" # 可选,默认 src/main.cpp - -# 静态库 -[targets.mylib] -kind = "lib" - -# 共享库 -[targets.mylib] -kind = "shared" -soname = "libmylib.so.1" # 可选: Linux/ELF ABI 名称,运行时会生成同名 alias -``` - -`soname` 用于共享库的 ABI 名称,类似 Autotools/CMake 中的 -`SOVERSION`/`SONAME`。在 Linux 上,mcpp 会向链接器传递 -`-Wl,-soname,`,并在输出目录生成 ` -> lib.so` alias, -让下游程序可通过标准 ABI 名称 `DT_NEEDED` 或 `dlopen()` 加载该库。 -该字段只对 `kind = "shared"` 有效,值必须是文件名 basename。 - -共享库目标在三种二进制格式上都可用。ELF 产出带 `soname` 的 `.so` 与 `$ORIGIN` -搜索路径;Mach-O 产出 install name 为 `@rpath/` 的 `.dylib`,因此移动后 -仍能被找到;PE 同时产出加载器打开的 `.dll` 和链接器消费的 import library,并在 -MSVC ABI 上从对象生成导出表(该 ABI 没有 `__declspec(dllexport)` 或 `.def` 时 -不导出任何符号)。参见 `tests/e2e/08`、`257`、`259`。 - -#### `exports` —— 产物发布哪些符号(mcpp 2026.9.6.5+) - -```toml -[targets.mydriver] -kind = "shared" -soname = "libmydriver.so.1" -exports = "abi/mydriver.exports" # 或内联:exports = ["vk_icd*"] -``` - -**不写这个键就发布全部,而那正是两个平台今天的默认**——ELF 给符号默认可见性,PE 会 -自动生成列出全部符号的 `.def`。`exports` 把它收窄。 - -两类工程需要收窄。**有稳定 ABI 的运行时**只发布一份经过评审的集合,不在集合里的东西 -才保持可改。**与同类并存的插件**不能撞名:Vulkan loader 按名字找 -`vk_icdGetInstanceProcAddr`,一个把内部符号也导出的 ICD 会与 loader 以及同进程内另一个 -ICD 相撞。 - -文件一行一条符号模式,`#` 起注释,`*` 是唯一的通配符。内联数组说的是同一件事,用于 -只有两三个入口、单开一个文件反而是仪式的场合。 - -一句话,三种渲染: - -| 平台 | 渲染为 | -|---|---| -| ELF | version script,`-Wl,--version-script=` | -| Mach-O | `-Wl,-exported_symbols_list`(前导下划线由引擎补) | -| PE | `.def`,取代自动生成的全导出版本 | - -**它不改变编译期可见性,这是有意的。** 三种格式上收窄都是链接期属性,所以一个键只有 -一个效果。`-fvisibility=hidden` 仍可经 `[build] cxxflags` 使用以取得代码生成上的收益, -而它是一个**单独**的决定,因为它同时改变本库各翻译单元之间如何看见彼此。 - -**符号版本化不是这个键。** `foo@@LIB_1.0` 与 `foo@LIB_0.9` 并存是 ELF 独有的能力, -无法中立表达;需要它的包自己写 version script 经 `[build] ldflags` 传入,或者算出来后 -用 `mcpp:link-flag=` 发出(docs/07)。 - -`soname` 对 `kind = "lib"` 同样有意义 —— 见下文的 `dependency_linkage`, -库以何种形态出现是**消费者**的决定。 - -#### 按目标的键(per-target keys) - -```toml -[targets.server] -kind = "bin" -main = "src/server.cpp" -defines = ["BUILD_SERVER=1", "PORT=8080"] # -D 宏,只作用于该目标的入口 -cxxflags = ["-Wno-deprecated-declarations"] # 该目标入口的额外 C++ 标志(不要放 -std=...) -cflags = ["-DPURE_C"] # 该目标入口的额外 C 标志 - -[targets.gui] -kind = "bin" -main = "src/gui.cpp" -required_features = ["gui"] # 仅当 feature `gui` 激活时才构建 -``` - -| 键 | 含义 | -|---|---| -| `defines` | 预处理宏(`name` 或 `name=value`),脱糖为 `-D`,作用于该目标入口的 C 与 C++ 编译。 | -| `cxxflags` / `cflags` | 该目标的额外编译标志。**不要**放 `-std=...`——用 `[package].standard`。 | -| `required_features` | 仅当列出的 feature **全部**激活时才生成该目标,否则静默跳过。只是门禁——不激活 feature(用 `--features` / `[features].default`)。 | - -> **作用域(重要):** 目标上的 `defines` / `cxxflags` / `cflags` **只作用于该目标独占的入口源** -> (它的 `main`)——**绝不**作用于共享的模块/实现对象(那些只编译一次、被每个目标链接,即 mcpp 的 -> compile-once 模型)。当标志只需影响某个二进制(或测试)**自己的入口**时,这正是合适的工具 —— -> 例如某个测试的 `main` 里触发契约违规、需要按测试设置契约求值语义 -> (`-fcontract-evaluation-semantic=observe`),或入口独享的 feature 宏、局部告警抑制。 -> 若标志必须穿透**共享**代码,就不该放在这里 —— 改用 [workspace](06-workspace.md) member 或 -> `[features]`;若是整次构建的模式,用 `[profile.*]`(`mcpp test --profile ` 会让包括被测 -> 代码在内的整个测试镜像都在该 profile 下编译)。 -> -> `[targets.]` 下的不支持键会产生 warning(`--strict` 下为 error)。 - -**构建配置该放哪** —— 当多个二进制需要不同配置时: - -| 目标 | 使用 | -|---|---| -| 某二进制**自己入口**上的不同宏/标志 | per-target `defines` / `cxxflags`(见上) | -| 两个产品差异在它们**共享**的代码里 | 拆成 [workspace](06-workspace.md) member,各自 `[build]` 标志,共享一个 `lib` | -| **选择**某共享库的变体(如某后端) | 在该库上用 `[features]`(§2.8)——additive,作用到库自己的编译 | -| **整次构建的模式**(sanitizer、契约语义、优化档) | `[profile.]`(§2.9)+ `--profile`;`mcpp test --profile ` 同样支持 | - -mcpp 刻意不在一次构建里把同一个共享源编译成两份:一个源对应一个对象(模块还对应一个 BMI), -所以"必须穿透共享代码"的差异应放在包/feature 边界,而非单个目标上。 - -### 2.3 `[build]` — 构建配置 - -> **`sources` 匹配到的每一项都必须产出一个会被链接的对象。** mcpp 放不下的文件 —— -> 扩展名既不在内建表也不在 `module_extensions` 里 —— 会被拒绝,并点名文件、 -> 扩展名与该写的键。**不是忽略**:催生这条规则的失败不是「多编了一个文件」, -> 而是**编了却没人链** —— 扫描器读到 `export module` 就给那条边挂了 BMI, -> 而分类器说这个文件没有角色,作者看到的是一条模块修饰过的 `undefined reference`。 -> 头文件应放进 `include_dirs`,Windows 资源脚本放进 `[resources]`。 - -> **`sources = []` 与不写 `sources` 不是一回事。** 不写这条键选择默认 glob; -> 显式的空列表意味着**什么都不编** —— 那正是一个纯头文件的分发包需要表达的。 -> 在 mcpp 2026.8.18.1 之前两者逐字节等价,于是「什么都不编」无从表达, -> `src/` 下剩下的任何文件都会被扫进来。 - - -> **`sources` 的条目可以带上它所面向的加速器**(2026.9.5.2+): -> `{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`。glob 与其它条目一样 -> 进入列表;约束决定它是否适用于某一次构建。它必须至少匹配一个文件(空匹配会被拒绝: -> 那会让这个设备无东西可编,而只在链接时才说话)。`--no-accel` 下该 glob 被排除, -> 一个工程由此产出它的 CPU-only 变体。`--accel` 未覆盖该约束时构建被拒并给出两侧 -> (`accel-mismatch`)。有效集合匹配到的设备类源文件 —— CUDA 与 HIP、GLSL 各 stage、 -> HLSL、OpenCL C 与 Metal,完整清单见 [20 — 异构硬件构建](20-heterogeneous-builds.md) —— 引擎 -> 从不编译;它们以 `MCPP_DEVICE_SOURCES` 到达构建程序,由工程引入的规则包把每一个 -> 变成一条 `mcpp::action`。 - -```toml -[build] -sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/**/*.{cppm,cpp,cc,c,S,s,asm}) -module_extensions = [".ixx"] # 模块**接口**额外使用的扩展名(见下节) -build_program_timeout = 1800 # build.mcpp 的运行上限(秒);0 = 不限(见下节) -include_dirs = ["include", "third_party/include"] # 头文件搜索路径 -include_dirs_after = ["*"] # 排在系统目录之后搜索的头文件目录(-idirafter) -private_include_dirs = ["vendor/src/include"] # `include_dirs` 中不发布给消费者的那些 -c_standard = "c11" # C 源文件的标准(默认 c11) -cflags = ["-DFOO=1"] # 额外 C 编译参数 -cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数(不要放 -std=...) -ldflags = ["-lfoo"] # 额外链接参数 -defines = ["BIZ=1", "QUX"] # 作用于每个 TU 的预处理宏(脱糖为 -D;会进入模块扫描) -cxx_runtime = "self-contained" # C++ 运行时契约(见下节);static_stdlib 是旧拼写 -macos_deployment_target = "14.0" # macOS 产物的最低支持系统版本(仅 macOS 生效) -dependency_linkage = "static" # 依赖以何种形态进入:static(默认)| shared(见下文) -cache = "global" # 依赖的全局构建缓存:global(默认)| local | off(见 §2.10) -jobs = "auto" # 并发编译数:正整数,或 "auto"(见下节) -bmi_schedule = "auto" # 模块边调度:auto(= 关)| on | off(见下节) -``` - -#### `dependency_linkage` —— 静态还是动态由消费者决定 - -```toml -[build] -dependency_linkage = "shared" # 全图默认;缺省即 "static" - -[profile.dev] -dependency_linkage = "shared" # 按 profile 覆盖 - -[dependencies] -"compat.zlib" = { version = "1.3.2", linkage = "shared" } # 单个包 -``` - -在 mcpp 2026.8.28.2 之前,一个依赖只有一种形态,而且由**包作者**定死: -`kind = "lib"` 把它的对象并进每个消费者的链接,`kind = "shared"` 产出真正的 -共享库。这个决定放错了位置。一个库在运行期该不该是独立文件,是**被构建的那个 -程序**的性质 —— 它怎么分发、多久重链一次、进程里是不是已经有人提供了这个库。 - -- **`static`**(默认)—— 依赖的对象并进使用它的映像。与 mcpp 一直以来的行为 - 逐字节相同;不写这个键的工程构建结果不变。 -- **`shared`** —— mcpp 把依赖构建成产物旁边的共享库并链接它,由 `$ORIGIN` - (ELF)/ `@loader_path`(Mach-O)/ 可执行文件自身目录(PE)保证构建目录 - 移动后仍能找到它。 - -**这不是 `[target.].linkage`**(§2.7.1)。那个键回答的是听起来相同、 -实则关于 **C 库**的问题(musl 的 `-static`、MSVC 的 `/MT`)。两者并不独立,而且 -方向很重要:整链静态的映像没有解释器,根本装不下任何共享对象。因此在 C 库静态 -链接的目标上 —— 这是 **musl 的默认** —— `dependency_linkage = "shared"` 会被 -拒绝,并说明原因。 - -**包可以声明它必须是某一种形态**,而且只在确有理由时: - -| 包写了 | mcpp 读作 | -|---|---| -| `[targets.] kind = "shared"` | *必须* shared —— 进程里会有别人 `dlopen` 它,因此只能有一份(X11、Vulkan loader) | -| `ldflags` 里含 `-L` | *必须* static —— 包携带了 mcpp 没有编译的预构建归档,放不进 mcpp 自己构建的共享对象 | -| 分发包(`mcpp pack`) | 它实际随包的那些腿,取自 `[[runtime.artifacts]] role` | -| 其他 | 两种形态都可以 | - -`kind = "lib"` **不是**约束:它是默认值,大多数包写下它并没有做任何选择。 -**没有陈述不等于一条陈述。** - -依赖边上的 `linkage` 只在**根工程**的 `[dependencies]` 里生效。依赖图深处的包 -无权决定最终程序的布局;真正必须只有一份共享副本的包,应当在自己的 target 上 -声明。 - -#### library 目标上的 `soname` - -`soname`(§2.2)在 `kind = "lib"` 上同样可以声明。它是一个库被**找到**时用的 -名字,也是 mcpp 构建的那份与第三方携带的同一个库能解析到**同一个文件**的唯一 -途径 —— 而如果声明它就意味着这个包不能再作为静态库被消费,包就无法陈述这件事。 - -在非 shared 目标上写 `soname` 的描述符,**无法被 2026.8.28.2 之前的 mcpp 读取** -—— 失败的是整份 manifest,不只是这个键。因此把它发布进索引要等下限抬上去。 - -#### 符号提供者检查 - -链接之后,mcpp 会问:映像里的每个符号是不是**恰好有一个**提供者。在 ELF 上 -可执行文件排在最前,因此被静态并进程序的库,会在它与旁边加载的共享库共有的 -每个符号上获胜 —— 共享的那份永远不会被调用,而那个库里的代码跑在一份它并非 -针对其链接的构建上。链接器和加载器都不会为此报任何一句话。 - -这项检查是**测量**而不是声明:读产物的动态符号表,去掉 copy relocation,只报告 -产物自身闭包里**也**有定义的那些。进程里只有一份副本的安排保持静默。判定记录在 -`target///resolution.json` 的 `runtime.symbol_provision` 下,带计数 -与分母,CI 不需要 `readelf` 就能读。 - -默认是警告,`--strict` 下升级为错误。三条出路**有次序**,而次序是要紧的: - -1. **让其中一方不再提供这个库** —— 通常是那个携带了依赖图已经在构建的库的副本 - 的包。永远正确。 -2. **让两者解析到同一个文件**:在库的 target 上声明它真正的 `soname`。 -3. **`dependency_linkage`** 改变 mcpp 构建的形态。它会消掉**这一条**报告,但单 - 独用可能把一份变成**两份**:实测在一个暂存了 glib(其 `libgio` 需要 - `libz.so.1`)、同时静态构建 `compat.zlib` 的图上,切换形态让可执行文件的 88 - 个导出符号归零,然后 `libzlib.so` 与 `libz.so.1` **两个都被加载**。只有在 - (2) 同时成立时它才真的把两个提供者合成一个。 - -`private_include_dirs` 指出 **`include_dirs` 中**在本包边界处停住的那些条目: -本包用它们编译,消费者永远收不到。 - -绝大多数包发布的就是它编译时用的那一套,所以长期以来只有 `include_dirs` 就够了。 -两者不同的形状只有一种 —— 一个包**内嵌了带内部头覆盖层的库**。musl 通过 -`src/include` 到达它自己的声明,而那些头定义了 `hidden`、`weak`、`weak_alias`, -这些名字只对 musl 自己的源码有意义。把那个目录发布出去,等于把这些宏交给每一个 -消费者;而一个把 `hidden` 当普通标识符用的消费者会编不过,且看不出原因。 - -```toml -[build] -# 两类目录的**相对顺序**是承重的:本包自己构建时,内部覆盖层必须排在公共头之前。 -# 这正是它被设计成 `include_dirs` 的**子集**而不是第二个列表的原因 —— -# 两个数组表达不了一个顺序。 -include_dirs = ["port/include", "musl/src/include", "musl/include"] -private_include_dirs = ["musl/src/include"] -``` - -条目支持与 `include_dirs` 相同的 `*` glob 约定,并在**展开之后**比对 —— -所以一个 glob 可以恰好指名它展开出的那些目录。若某条目不在本包的 `include_dirs` -里,它什么也没扣下,mcpp 会把这件事说出来而不是让它悄悄通过。 - -**旧引擎会忽略这个键,而不会因此失败。** 在 2026.8.26.2 上实测:出现在依赖的清单里 -时被静默接受;出现在根清单里时给一条警告 —— `[build] has unsupported key -'private_include_dirs' (ignored)` —— 构建照常继续。所以一个包可以先用上这个键, -不必等消费者升级;还在旧引擎上的消费者只是像以前一样继续收到那个目录。**唯一不成立 -的地方**是已发布的 `xim` 描述符的 `target_cfg` 块:那里不认识的子键是硬错误,会让 -整份清单加载失败 —— 在索引下限指向认识它的引擎之前,不要把这个键写进那里。 - -`include_dirs_after`(#249)列出**排在工具链系统目录之后**搜索的头文件目录 -(GCC/Clang 发射为 `-idirafter`;MSVC 方言退化为排在末尾的 `/I`,NASM 汇编 -单元退化为普通 `-I`——两者都没有对应 flag,也都没有需要保护的系统头搜索链)。当目录是解压后的源码 tarball 根目录、且其中的文件名会与标准头冲突时, -用它代替 `include_dirs` —— 例如 ffmpeg 根目录的 `VERSION` 文件在大小写不敏感 -的 macOS 文件系统上会把 libc++ 的 `` 遮蔽(若该根目录挂在 `-I` 上)。 -使用 `include_dirs_after` 时系统头永远优先,而包自己的真实头文件 -(``)仍能找到。条目支持与 `include_dirs` 相同的 `*` glob -约定,并沿相同的依赖边传播给消费者 —— 消费者收到的仍是 after 目录, -永远不会被升级为 `-I`。 - -`macos_deployment_target` 设定产物 Mach-O 头里的最低系统版本 -(`LC_BUILD_VERSION minos`),即二进制能运行的最老 macOS。优先级与各生态 -惯例一致:环境变量 `MACOSX_DEPLOYMENT_TARGET`(单次调用的显式覆盖, -cargo/rustc、cc 等同样尊重该变量)> 本字段(项目默认,类似 SwiftPM 的 -`platforms:`)> **内建默认 `14.0`**(rustc 风格——每个 target 都有基线, -14.0 即 LLVM 官方静态库自身的下限)。该值会进入 BMI 指纹——切换 target -会自动重建模块缓存。 - -### 构建并发(`jobs`)与模块调度(`bmi_schedule`) - -```toml -[build] -jobs = "auto" # 或正整数;--jobs / MCPP_JOBS 覆盖它 -bmi_schedule = "off" # auto(默认,= 关)| on | off -``` - -`jobs` 是同时跑几个编译。`"auto"` **在构建这台机器上现算**,绝不冻进 manifest: -异构 CPU 上取物理核数(13900K 是 8 P-core + 16 E-core,它的 32 个线程不是 32 个 -等价的工人),再按可用内存夹一次 —— 单个模块接口编译峰值 0.5–1.0 GB。 -优先级:`--jobs` / `MCPP_JOBS` > 这个键 > 后端自己的默认值。写错的值会被 -**明确报出来,绝不静默当成默认值** —— 一个悄悄退回默认的拼写错误,表现是 -「构建莫名其妙比我要求的慢」。 - -`bmi_schedule` 决定**导入方什么时候被解锁**。 - -| 值 | | -|---|---| -| `"auto"` | **默认值,而它目前等于「关」** | -| `"on"` | 拆开模块边:BMI 一发布导入方就能开始,而不是等编译器退出 | -| `"off"` | 每个模块一条边 | - -只认这三种拼写。`"ON"`、`"true"`、`"yes"` 会被**拒绝并给出诊断**,而不是悄悄 -当成关 —— 而且它们不是无害的笔误:这个值会进构建指纹,所以一个被拒的拼写 -以前会选到**另一个构建目录**(即一次全量重建),同时对调度没有任何影响。 - -**`auto` 为何等于关闭。** 模块接口编译中约 86% 是任何导入方都不会读取的代码生成, -因此提前发布 BMI 收益显著 —— 在 mcpp 自身上实测:`cold` 86.7s → 35.7s、 -`edit-body` 80.9s → 29.8s。但调度错误的表现是静默失效:缺少一条依赖不会使构建 -报错,只会使某个目标不再重建。因此在所有平台完成 CI 验证前,该键保持 opt-in。 - -**该键无效的场景。** mcpp 本来就跳过级联的地方(`touch-hub`、`edit-comment`) -没有可以移出关键路径的必需工作,该键不产生收益。见 -[性能对比](../../README.zh-CN.md#性能对比)。 - -**实现方式**按编译器确定,无需用户选择:gcc 用 `rename()` 发布 BMI,所以代码 -生成被分离出去、边在发布时就返回;clang 换成两条普通边 —— 它把 BMI 直接 -`O_TRUNC` 写到最终路径,读的人可能看到写了一半的文件。MSVC 不动:`/ifcOnly` -的代价和 `.ifc` 是否原子发布都没测过,而这两件事猜错都是无声的。 - -### 模块接口扩展名(`module_extensions`) - -mcpp 把 `.cppm` 视为模块接口单元。C++ 生态并没有收敛到一种拼法 —— Clang 还认 -`.ccm` 和 `.cxxm`,MSVC 用 `.ixx` —— 所以接口用别的扩展名的工程自己声明: - -```toml -[build] -module_extensions = [".ixx", ".ccm"] -``` - -这个列表是**追加**的:`.cppm` 永远是模块接口,不能删。要让某个文件不参与构建, -用 `sources` 的 `!` 前缀 —— 那才是 `sources` 的职责。 - -声明一个扩展名会同时做三件事,这正是「一个键而不是几个键」的理由: - -1. `sources` 的约定默认值跟着变宽,文件才**能被找到**(`src/**/*.ixx` 自动进入默认 glob); -2. 这些单元用**模块**规则编译 —— 产出 BMI,其 `.o` 无条件进入链接; -3. 新鲜度快路径会扫描它们,所以给其中一个加 `import` 会让构建图作废, - 而不是静默复用一张过期的图。 - -**任何扩展名都接受**,唯独拒绝那些已经代表其他角色的 -(`.cpp` `.cc` `.cxx` `.c` `.m` `.mm` `.h` `.hpp` `.hh` `.hxx` `.S` `.s` `.asm`)—— -这是 manifest **错误**而不是警告,因为它会把(比如)C 文件送进 C++ 模块规则, -最终失败在一个既不提文件也不提这个键的地方。 - -扩展名**按字面匹配,不做大小写折叠** —— 在这个领域里 `.S` 和 `.s` 是两种不同的语言, -所以大小写从不被忽略。 - -mcpp 每次都会**显式告诉编译器**这个单元是模块接口(Clang 用 `-x c++-module`, -GCC 用 `-x c++`,MSVC 用 `/interface /TP`),所以即使编译器驱动从没听说过这个扩展名 -也能工作。这也是为什么任何扩展名都被允许:mcpp 不需要编译器认识它。 - -> **发布须知**:旧版 mcpp 不认识这个键 —— 它会警告、忽略,然后把那些文件当作普通 -> 翻译单元编译,得到一个**错误的构建**而不是一次干净的失败。发布一个用了 -> `module_extensions` 的包,请在它的索引描述符里声明 mcpp 版本下限。 - -### 构建程序超时(`build_program_timeout`) - -`build.mcpp` 默认有 **600 秒**,超时后 mcpp 杀掉它并让构建失败、点名是哪个包。 -构建程序确实需要跑更久的工程(大规模代码生成)自己抬高上限: - -```toml -[build] -build_program_timeout = 1800 # 秒;0 = 不限 -``` - -这个值读的是**拥有该 `build.mcpp` 的那个包**的 manifest —— 依赖的生成器由依赖自己的 -声明来限制,因为只有它的作者知道要跑多久。优先级与 `macos_deployment_target` 同构: - -``` -MCPP_BUILD_PROGRAM_TIMEOUT=<秒> 本次调用(最高) - > [build] build_program_timeout 该包自己的 manifest - > 600 内置默认 -``` - -**不写这个键**与**写 `0`** 不是一回事:不写表示「用默认上限」,`0` 表示「完全不设上限」。 - -这个值刻意**不进构建指纹** —— 它不改变图里的任何一条边,而把它折进指纹会让 -「抬高超时」触发全量重建,这恰好与抬高超时的人想要的相反。 - -只有构建**程序**受限,**编译**不受限。原因见 -[07-build-mcpp.md](07-build-mcpp.md)。 - -### C++ 运行时契约(`cxx_runtime`) - -`cxx_runtime` 声明的是**产物对运行它的机器做出的承诺**。它是**分发**属性而非 -构建属性 —— 它描述的是运行期依赖集,而兑现它的 flag 逐平台不同。 - -> **不含 C++ 的目标没有 C++ 运行时契约需要兑现。** mcpp 用 C 驱动链接它, -> 并且完全不发 C++ 运行时相关的 flag,因此一个纯 C 的共享库不会平白拿到 -> `libstdc++` / `libc++` 依赖。目标里只要有一个 C++ 翻译单元,整个目标就回到 -> C++ 驱动。这一判定由源码推导,没有对应的配置键。 - -```toml -[build] -cxx_runtime = "self-contained" # 作用于所有目标(默认值) - -# 或者按角色分别指定: -[build.cxx_runtime] -default = "self-contained" # 可执行文件 -tests = "host-coupled" # 测试二进制从不离开本机 -shared = "self-contained" # 共享库(见下 —— 默认值随目标格式而变) - -# 或者按目标三元组 —— 与 `linkage` 并列,因为它们是同一根轴: -[target.x86_64-linux-gnu] -cxx_runtime = "host-coupled" # 例如这次构建是为发行版打包 -``` - -| 取值 | 产物运行时需要 | 典型场景 | -|---|---|---| -| `self-contained`(默认) | 自身之外不需要任何 C++ 运行时 | 分发二进制 | -| `toolchain-coupled` | mcpp 装的那份工具链的 C++ 运行时 | 本地迭代 | -| `host-coupled` | 驱动默认解析到的那份(通常是系统运行时) | 发行版打包;必须与宿主共用同一份运行时的 `dlopen` 插件 | - -**默认即自包含(portable by default)**:macOS 上这会静态链入 LLVM 自带的 -libc++/libc++abi —— 系统 libc++ 会把实际可运行版本固定在构建机的 OS(老系统 -缺新符号,如 `std::print` 的支撑符号),只有静态化才能真正兑现 -`macos_deployment_target` 的 floor。Linux/MinGW 上它是 `-static-libstdc++` -(GCC)或整条链的 `-static`(MinGW);Linux 上的 clang/libc++ 工具链则显式链入 -libc++.a/libc++abi.a/libunwind.a。更低的 macOS floor(11–13)需自建 libc++ -归档(已验证可行,数据级切换,按需提供)。 - -**共享库是唯一一个默认值随目标格式变化的角色**,因为危害本身随格式变化。 -`.so`/`.dylib`/`.dll` 不是一个小号可执行文件 —— 它被加载**进**一个已经有 -C++ 运行时的进程。 - -| 目标格式 | `kind = "shared"` 的默认契约 | 原因 | -|---|---|---| -| ELF(Linux 等) | `toolchain-coupled` | ELF 只有一个全局符号命名空间,先加载的定义胜出。静态内嵌了 libstdc++ 的 `.so` 会把它**导出**,链接该库的可执行文件于是把自己的 `std::` 引用绑到那里 —— 它自己的 `self-contained` 契约静默变成空操作,它的 C++ 运行时变成"碰巧加载的那一份该库"。 | -| Mach-O | `self-contained` | 那里的机制本来就是 `-load_hidden`(hidden 可见性),dyld 不会归一这些符号;而且 macOS 上根本没有 toolchain-coupled 这一档(见下文注)。 | -| PE(Windows) | `self-contained` | PE 没有全局符号命名空间 —— 导入按 DLL 逐个按名解析,一个 DLL 的私有运行时不可能被别人捡走。 | - -在 ELF 上显式写 `shared = "self-contained"` 是支持的,而且就是字面意思:库会内嵌 -运行时。此时 mcpp 会额外发 `-Wl,--exclude-libs`(针对标准库归档),让内嵌的那份 -留在库的动态符号表之外,链接它的任何东西都捡不走。本工程代码产生的模板实例化 -(`std::string` 之类的 weak/COMDAT 符号)仍然会导出 —— 那是 C++ ABI 的预期行为, -不是这里要防的泄漏。 - -工程级的 `cxx_runtime = "…"`(或 `static_stdlib = false`)同样作用于共享库: -有人写下了整个工程的承诺。只有在**没人写**的时候,随格式变化的默认值才生效。 - -`static_stdlib` 是旧拼写,仍然有效:`true` 等价于 `self-contained`,`false` -等价于 `host-coupled`。显式写了 `cxx_runtime` 时以后者为准。 - -**兑现不了的契约会被报出来,绝不静默降级。** 若工具链不带 `libc++.a`,或某个 -契约在该平台上没有对应机制,构建会打印实际退到了哪一档,而不是悄悄交付一个与 -manifest 所述不同的产物。 - -#### 在 MSVC 运行时上 - -这里的机制就是 CRT 模型,而它是**整个工程级**的开关:cl 会把 `_MSVC_MT` / -`_MSVC_MD` 烘进工程唯一的那份 `std` 模块,所以与工程不一致的按角色契约无法兑现, -会被报出来而不是被忽略。 - -| 取值 | 在 MSVC 上是什么 | -|---|---| -| `self-contained` | `/MT`,静态 CRT。`linkage = "static"` 从 libc 那根轴选中的是同一件事。 | -| `host-coupled`(`/MD` 下的默认) | 由目标机器提供 `vcruntime140.dll` / `msvcp140.dll` —— 即那台机器装了 Visual Studio 或 redistributable。 | -| `toolchain-coupled` | toolset **自带**的那份 DLL 跟着产物走。 | - -`toolchain-coupled` 值得说清楚,因为直觉上的理解是错的。`ucrtbase.dll` **是** -Windows 组件(Win10 起),mcpp 从不分发它;而 `vcruntime140.dll` / -`msvcp140.dll` **不是**:每个 MSVC toolset 都在 -`VC\Redist\MSVC\\\` 下带着它们,和 gcc payload 带着 -`libstdc++.so` 是同一件事。在这个契约下 mcpp 会把它们放到产物旁边 —— 这正是让 -默认的 `/MD` 产物能在"只装了 pinned toolset、根本没有 Visual Studio"的机器上跑 -起来的原因。 - -调试版 CRT(`debug_nonredist\` 下的 `vcruntime140d.dll` 等)永远不会被放进去: -它不可再分发。 - -> **从 2026.8.15 或更早版本升上来?** 这条键在 MSVC ABI 上曾经是**空操作** —— -> 它会报 `not implemented for the MSVC runtime yet`,写什么都退回 `/MD`。 -> 自 2026.8.16 起它真的生效,于是一份从那个年代带着 -> `cxx_runtime = "self-contained"` 的 manifest **会在升级时换掉 CRT 模型**: -> 从 `/MD` 变成 `/MT`。它不是同一个模型的更严格版本,而且因为这个值一直是合法的, -> 切换是**静默**的。若工程是在这条键尚未生效时写下它的,应重新确认所需的取值。 - -把它和 `/MT` 一起用是**矛盾**而不是缺功能 —— 静态 CRT 根本没有 DLL 可以耦合 —— -所以会被报出来并落到 `self-contained`。另一半由 `mcpp pack` 兜底:什么都不打包的 -模式(`--mode system`、`--mode static`)兑现不了 `toolchain-coupled`,会直接拒绝。 - -**边界。** 该契约只管 C++ 运行时。静态 **libc** 是另一根轴(`linkage = "static"` -/ `--static`,如 musl 目标),部署下限是第三根轴(`macos_deployment_target`)。 -另外,`host-coupled` 只承诺 mcpp 不做任何"把 C++ 运行时打进产物"的动作,它不会 -去掉链接因其它原因已经携带的工具链 rpath —— 所以在 ELF 上这类产物仍可能优先 -找到工具链的库。 - -> **macOS + `self-contained` 与静态初始化次序。** Mach-O 没有按优先级排序的 -> 初始化段,而 libc++ 的 `` 也不像 libstdc++ / MSVC STL 那样自带 -> `ios_base::Init` 守卫 —— 于是从 `libc++.a` 里拉出来的流初始化器本来会排在 -> 程序自己的全局构造函数**之后**:一个在构造函数里碰 `std::cout` 的全局对象会 -> 读到尚未构造的流,进程启动即崩。mcpp 会把一个极小的生成对象排在链接最前面 -> 把流顶上去,调用方代码无需改动。详见 mcpp-community/mcpp#336。 - -`defines` 接受**裸**宏名(不带 `-D`),把每个条目脱糖为 `-D`,同时作用于 C 和 -C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入**编译器自己的** -P1689 模块扫描。 - -> **但它不会让被宏保护的 `import` 变得可用。** mcpp 在编译器看到文件之前先跑 -> 自己的词法预扫描,而那个扫描器对**任何** `#if` / `#ifdef` 块内的 `import` -> 一律拒绝,不求值条件: -> -> ``` -> error: import statement inside conditional preprocessor block (forbidden in M1) -> ``` -> -> 所以即使 `FOO` 写在 `defines` 里,`#ifdef FOO` / `import bar;` 仍然会失败。 -> 替代写法是把条件放在全局模块片段的 `#include` 上。见 -> mcpp-community/mcpp#421。 - -汇编单元同样能拿到。它是普通的构建 -输入,所以 `[target.'cfg(...)'.build]` 也能承载它: - -```toml -[build] -defines = ["APP_NAME=\"demo\""] - -[target.'cfg(windows)'.build] -defines = ["USE_WIN32", "WINVER=0x0A00"] -``` - -选择合适的轴: - -| 想让宏作用于… | 用 | -|---|---| -| 本包的每个 TU | `[build].defines`(本节) | -| 仅某个二进制自己的入口源 | `[targets.].defines` | -| 指定的一批文件 | `[build].flags` 配 `glob` + `defines` | -| 本包每个 TU **以及**消费者的 TU | `[features.].defines`(接口贡献) | - -`[build].defines` 是包私有的:不会传播给消费者。 - -`[build]` 下不支持的键会作为警告报出(`--strict` 下为错误),而不是被静默忽略。 - -C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: - -```toml -[package] -standard = "c++26" -``` - -mcpp 会把同一个标准用于普通 C++ 编译、模块扫描、`compile_commands.json` 和 `import std` 的标准库 BMI 构建。 - -**glob 排除**(`!` 前缀,mcpp 0.0.4+): - -```toml -[build] -sources = [ - "src/**/*.cpp", - "!src/**/*_test.cpp", # 排除测试文件 - "!src/**/*_fuzzer.cpp", # 排除 fuzzer -] -``` - -**per-glob 旗标**(mcpp 0.0.95+):`[build] flags` 是**有序**的内联表数组,把额外 -编译旗标只附加到 glob 命中的源文件——SIMD 多档 dispatch TU 与三方代码告警隔离的 -正解: - -```toml -[build] -flags = [ - { glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] }, - { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] }, - { glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] }, -] -``` - -每条目键:`glob`(相对包根,必填)+ `cflags` / `cxxflags` / `asmflags` / -`defines`(没有 `ldflags`——链接没有 per-TU 作用域)。声明顺序即应用顺序:靠后 -条目的旗标排在命令行更后,配合 GNU "后旗标胜",窄 glob 放在宽 glob 之后即可覆盖。 -所有命中条目都生效;这些是私有构建旗标,不会传播给消费者。glob 零命中会打印 -warning(打错的 glob 不允许静默无效)。 - -**生成文件**(mcpp 0.0.95+):`[generated_files]` 把相对路径映射到文件内容(支持 -TOML 多行字符串)。条目在源 glob 展开之前写入工程树——与 index 描述符合成模块 -包装文件是同一机制——内容进指纹,改内容即重建: - -```toml -[generated_files] -"src/gen/wrap.cppm" = """ -module; -#include -export module wrap; -""" -``` - -路径必须留在工程根之内(`..` / 绝对路径是解析错误)。 - -**汇编源**(mcpp 0.0.95+):`.S`/`.s`(GAS——由 C 驱动器预处理,覆盖 ARM 与 -AT&T 语法 x86)和 `.asm`(NASM——Intel 语法 x86)是一等源文件:默认 glob 收录、 -进指纹、增量并行构建、像任何对象一样链接。NASM 的输出格式由目标三元组推导 -(`elf64`/`win64`/`macho64`/...——交叉构建零特判);`nasm` 仅在存在 `.asm` 单元时 -惰性解析:先 `PATH`,再 mcpp 沙箱,再 `xlings install nasm`;找不到 ≥2.16 的 -nasm 则**硬失败**(汇编绝不静默跳过)。限制:`.asm` 仅限 x86 目标(其他目标硬 -报错——用条件 sources 门控)、MSVC 工具链不支持 `.S`、`.asm` 即 NASM 语法 -(MASM 源请用 `!` 排除)。 - -### 宿主代码页之外的文件名 - -glob 是窄字符串,编译命令和 `build.ninja` 也是。在 Windows 上这些字符串由进程的 -**ANSI 代码页**产生,因此一个名字在该代码页里无法拼写的文件,既匹配不了 glob,也 -写不进编译命令或构建文件。 - -这类条目会被跳过,并按目录报告一次: - -```text -warning: 'C:/.../pkg/test/www' contains names this system's active code page cannot represent - impact: those files take no part in the build - hint: Windows only: this is the process ANSI code page, which `chcp` does not change. ... -``` - -报告里给的是**最近一个代码页拼得出的祖先目录**,用通用(`/`)写法。拼不出的那个名字本身 -永远不会被打印:渲染它会抛出这条消息正在报告的同一个异常。 - -`chcp` 改的是**控制台**代码页,对此无效。若这些名字只是测试数据或文档,跳过是无害 -的——上游 tarball 里带一个日文夹具目录,在 en-US 宿主上照样构建。源文件则不然:需要 -改名,或换一台代码页覆盖得了的机器。 - -Linux 与 macOS 不做这种转换,因此那里不会跳过任何名字。一个包在一边能构建、在另一 -边报 `internal: unhandled exception` 并指向代码页,就是 mcpp#516。 - -### 2.3.1 `[build] accel` — 本次构建面向的加速器 - -```toml -[build] -accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" -``` - -本次构建为哪些设备后端与架构编译。单次构建可用 `--accel` 覆盖 —— -这与 `--target` 对 `[toolchain]` 的关系相同;`--no-accel` 是显式请求「不要加速器」, -也就是在一个同时发布了设备构建的包中选中 CPU-only 变体的方式。 - -该取值会与构建所消费的任何预建产物的 `accel` 字段比较,而请求为空的构建被任何产物满足。 -见 [20 — 异构硬件构建](20-heterogeneous-builds.md)。 - -### 2.4 `[lib]` — 库根模块约定 - -```toml -[lib] -path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置 -``` - -默认约定:`src/<包名最后一段>.cppm`(如包名 `mcpplibs.cmdline` → `src/cmdline.cppm`)。 - -### 2.5 `[dependencies]` — 运行时依赖 - -```toml -# 默认包空间(mcpplibs)下的包 -[dependencies] -cmdline = "0.0.2" # 精确版本 -templates = "0.0.1" - -# dotted selector 是单一精确身份:最后一段是包名,之前所有段都是 namespace。 -compat.gtest = "1.15.2" -imgui.core = "0.0.1" -imgui.backend.glfw_opengl3 = "0.0.1" -mcpplibs.capi.lua = "0.0.3" -``` - -```toml -# 命名空间子表写法 -[dependencies.mcpplibs] -cmdline = "0.0.2" -tinyhttps = "0.2.2" -llmapi = "0.2.5" - -[dependencies.compat] -glfw = "3.4" # 显式 namespace,无回退搜索 -``` - -```toml -# 路径依赖(本地开发) -[dependencies] -mylib = { path = "../mylib" } -``` - -```toml -# Git 依赖 —— tag / branch / rev 三选一 -[dependencies] -mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" } -applib = { git = "https://github.com/user/applib.git", branch = "develop" } -``` - -```toml -# 长式 dep spec:features 与 backend 旋钮 -[dependencies] -imgui = { version = "0.0.3", features = ["docking"] } # 请求该依赖的 feature -widget = { version = "1.0", backend = "glfw_opengl3" } # 糖:= features=["backend-glfw_opengl3"] -``` - -`backend = ""` 是**通用约定糖**:1:1 脱糖为请求该依赖的 `backend-` -feature(库若支持该旋钮,应在自己的 `[features]` 中声明 `backend-*` 系列)。 -若目标包声明了 `[features]` 但不含所请求的 feature(含 backend 脱糖结果), -默认给出 warning,`mcpp build --strict` 下报错。 - -**Git 依赖与 `mcpp.lock`**:`tag` 和 `rev` 本身就指向历史中的固定点,而 `branch` -是会动的。首次构建把分支解析成一个 commit 并写进 `mcpp.lock`,此后每次构建都重建 -**那个** commit —— lock 是权威而不是缓存提示,所以删掉 `~/.mcpp/git` 或换一台机器 -都不会静默切换到更新的分支头。需要新的分支头时,必须显式指定: - -```bash -mcpp update mylib # 丢掉记录的 commit,下次构建重新解析 -mcpp update # 同上,对所有依赖 -``` - -既然记录的 commit 已经足以决定构建什么,那么在 `~/.mcpp/git` 里已有克隆的情况下, -重新构建完全不发网络请求,`--offline` 下照常工作。只有两件事需要网络:解析一个在 -lock 里没有 commit 的分支,以及克隆一个尚未缓存的 commit。`git =` 若指向本地目录 -(或 `file://` URL),这两件事都不需要网络,因此离线下也绝不会被拒绝。 - -**SemVer 约束**: - -```toml -[dependencies] -foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret,默认) -bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde) -baz = "=1.2.3" # 精确匹配 -qux = ">=1.0, <2.0" # 范围组合 -``` - -#### 命名空间解析规则 - -每个包的身份是**命名空间 + 名字**二元组。每个 selector 都只规范化成一个身份: - -- `cmdline` → `(mcpplibs, cmdline)`;省略 namespace 只表示默认 `mcpplibs`。 -- `compat.gtest` → `(compat, gtest)`。 -- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`。 - -不存在有序回退或按短名的全索引模糊搜索: - -```toml -# 正确 —— 点式选择器 -[dependencies] -chriskohlhoff.asio = "1.38.1" - -# 正确 —— 命名空间子表(同一组织有多个包时更推荐) -[dependencies.chriskohlhoff] -asio = "1.38.1" - -# 错误 —— 裸名永远到不了 chriskohlhoff 命名空间 -[dependencies] -asio = "1.38.1" -``` - -第三种写法会明确报错,指出实际尝试的 `(mcpplibs, asio)`;若该短名存在于别处,错误信息会给出可直接复制的显式 selector。 - -##### 裸名过渡期(`2026.8.10.1` 起,`2026.9` 移除) - -索引里已发布的 `compat.*` 包与既有 manifest **全部**写成裸名(`gtest = "1.15.2"`)。 -升级后直接失败,等于让一次程序发布把**已经发布、且无法追溯修改**的数据作废, -所以有一个版本的过渡期:裸名在 `mcpplibs` 未命中时仍可到达 `compat.`, -不声明 namespace 的 descriptor 也仍可被裸名解析。 - -但它不再静默: - -``` -warning: dependency 'gtest' resolved to 'compat.gtest' through the deprecated -bare-name search; namespace omission means `mcpplibs` only. Write the exact -package: - [dependencies.compat] - gtest = "1.15.2" - (or run `mcpp add compat.gtest@1.15.2`). This fallback is removed in 2026.9. -``` - -写进 `mcpp.lock`、install 与 cache 的是**规范身份**;歧义拼写只存在于工程的 -`mcpp.toml` 中,直到被改写 —— `mcpp add gtest@1.15.2` 会完成这次改写。 - -过渡期**不适用于**写明 namespace 的 selector(`mcpplibs.gtest` 未命中就是未命中), -裸名也**仍然**到不了第三方 namespace。 - -**为什么只允许一个身份?** 因为依赖解析必须可复现。候选搜索会让同短名包受索引状态影响,新增索引还可能悄悄重定向既有依赖。 - -**给 xpkg 作者:** 索引描述符里,身份是 `(package.namespace, package.name)` 二元组。命名空间是点分路径,**`name` 是单一原子段**: - -```lua -package = { - namespace = "chriskohlhoff", - name = "asio", -- 单一段;不是 "chriskohlhoff.asio" -} - -package = { - namespace = "mcpplibs.capi", -- 层级放这里 - name = "lua", -} -``` - -文件名只是提示 —— 描述符按声明的身份被发现,所以 `pkgs/c/chriskohlhoff.asio.lua` 与 `pkgs/z/anything.lua` 解析结果完全相同。推荐 `.lua` 或 `..lua`(命中 mcpp 的快路径),但不强制。 - -旧的完全限定拼写(`name = "chriskohlhoff.asio"`)仍被接受,已发布的描述符无需改动。`mcpp xpkg parse` 会校验该规则,请在索引 CI 里跑它。描述符身份需要 mcpp >= 0.0.106,精确 selector 需要 mcpp >= 2026.8.10.1,两者使用 xlings >= 0.4.69;规范全文见 `docs/specs/package-identity.md`。 - -`mcpp new --template` 刻意复用同一身份模型,而不是另造包文法: -`[ns.]name[@version][:tname]`。其中裸名同样只表示 `mcpplibs`,version 与模板名可分别 -省略。省略 `tname` 时选择唯一显式 default;若未写 `default = true` 且只有一个模板, -该单模板自动成为默认。多个未标默认的模板会报错,绝不按目录顺序选择。规范表见 -`docs/specs/package-identity.md` §4.4。 -#### mcpp 何时刷新包索引 - -`mcpp build` / `run` / `test` **只在依赖无法用本地索引解析时**刷新包索引,绝不会 -因为"时间到了"就刷。具体地说,只有三种情况会触发:本地根本没有索引、依赖的描述符 -不在其中、或 SemVer 约束在本地已知版本里无解。只要所有依赖都能在本地解析出来, -无论本地索引多旧,构建都不会发起任何网络请求。 - -由此带来的一个需要知道的语义:`^1.2` 这类约束是对**本地索引已知的版本**求解的。 -若上游在上次刷新之后发布了 `1.3.0`,需要主动获取: - -```bash -mcpp index update # 同步索引 -mcpp update # 同步索引,并重新解析依赖 -mcpp index status # 看本地现状:状态、年龄、修订号 -``` - -三个开关,优先级从高到低: - -| 开关 | 作用 | -|---|---| -| `--offline`(任意命令) | 完全不碰网络——不刷索引、不下载、不自动装工具链,也不发 `git ls-remote`/`clone`。已安装的东西照常构建,包括 commit 已在 `mcpp.lock`、克隆已在缓存里的 git 依赖 | -| `MCPP_OFFLINE=1` | 同上,作用于整个 shell 会话或 CI job | -| `~/.mcpp/config.toml` 里 `[index] auto_refresh = false` | 永不自动刷新索引,但下载仍然可用 | - -`MCPP_NO_AUTO_INSTALL=1` 作为 `--offline` 的旧式窄化拼写仍然有效(它只管工具链的 -自动安装)。 - -任意命令加 `-v` 可以看到每个依赖的判定结果与原因。 - -### 2.6 `[dev-dependencies]` —— 测试依赖 - -```toml -[dev-dependencies.compat] -gtest = "1.15.2" -``` - -`mcpp build` 忽略这些依赖;`mcpp test` 解析并使用它们。`mcpp test` 自动发现 -`tests/**/*.cpp` 并把它们编译为测试二进制。运行器与框架无关:每个文件是一个独立的 -二进制,以退出码判定 —— 裸 `main`、gtest(经 `[dev-dependencies]` + `gtest_main`) -或任何其他框架的行为完全一致,`-- args` 会转发给每个测试二进制 -(例如 `-- --gtest_filter=...`)。注意:合成的测试目标名可能包含 `/` -(`tests/00-a/0.cpp` → `00-a/0`),这与 `[targets.*]` 名不同 —— 两个命名空间是 -刻意分开的(测试目标从不进入 manifest,也不参与发布)。测试按其相对 `tests/` 的 -路径命名(`tests/00-a/0.cpp` → `00-a/0`),每个测试独立编译(一个测试写坏只让它 -自己失败;包或依赖损坏则报告为构建错误),`mcpp test ` 与 -`--message-format json` 分别提供过滤与机器可读输出。 - -### 2.6.1 `[build-dependencies]` —— 构建期依赖(mcpp 2026.8.29.1+) - -```toml -[build-dependencies] -protobuf = { version = "35.1", tools = ["protoc"] } -``` - -段与边上的请求回答的是**两个不同的问题**,把它们混为一谈是建模上的错误,不是写法之争。 - -- **段**回答:这个包本身进不进目标。`[dependencies]` 进;`[build-dependencies]` 永不进, - 只能经由它到达的东西也一样。 -- **边上的请求**回答:要它的哪一种构建期产物。`tools = [...]` 要一个宿主可执行文件, - `host-module = true` 要一个构建程序可以 import 的模块。 - -一个包可以同时在两个轴上取值,而 protobuf 正是证明这两个轴必须分开的例子:工程既链接 -`libprotobuf`,构建期又需要 `protoc`。它只写一次,写在 `[dependencies]` 里: - -```toml -[dependencies] -protobuf = { version = "35.1", tools = ["protoc"] } -``` - -`[build-dependencies]` 用于第一个轴无法表达的那种组合 —— 某个包的库不得进入目标, -而它的工具或规则仍然需要。同一个包同时出现在两张表里不是错误:普通声明胜出,因为 -一行 `[build-dependencies]` 不应该悄悄拿掉目标真正需要的库。 - -与 `[dev-dependencies]` 不同,这些依赖**会**被传递遍历:一个构建期依赖自己的依赖正是 -让它能工作的东西,并且继承它「只服务构建」的性质。 - -feature 可以为构建期请求划定范围而无需第二个声明处 —— `[feature-deps.]` 可以给 -一条已经无条件声明的依赖追加 `tools`,所以「按需才要」不需要另开一张表。 - -> 这个段很早就能被解析,而直到 2026.8.29.1 之前没有任何做决定的代码读它:写下它得到的是 -> 一份能加载的清单、零诊断、零效果。 - -### 2.7 `[toolchain]` —— 工具链配置 - -```toml -[toolchain] -default = "gcc@16.1.0" - -# 交叉编译目标覆盖 -[target.x86_64-linux-musl] -toolchain = "gcc@16.1.0" -linkage = "static" -``` - -### 2.7.1 `[target.*]` —— 平台条件依赖与 flag - -用 `[target.]` 表把依赖与构建 flag 限定到某个平台。选择器 `` 有三种形式: - -| 选择器 | 含义 | 示例 | -|---|---|---| -| **裸 OS 别名** | 单个 OS / 族 —— 简洁且常用的形式 | `[target.windows]`、`[target.unix]` | -| **`cfg(...)` 谓词** | 复合条件(arch / env / 组合子) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | -| **精确三元组** | 某个具体目标(同时承载 `toolchain` / `linkage` / `sysroot` / `runner`,见 §2.7.3) | `[target.x86_64-linux-musl]` | - -一个选择器可以承载平台条件的**依赖**与**构建 flag**: - -```toml -# 简洁的裸别名形式 —— 仅在 Windows 上拉取并链接 OpenBLAS。 -[target.windows.dependencies.compat] -openblas = "0.3.33" -[target.windows.build] -ldflags = ["-Llib", "-llibopenblas"] - -# cfg(...) 用于复合谓词(文法:all/any/not 作用于 os/arch/family/env, -# 以及裸别名 windows/unix/linux/macos)。 -[target.'cfg(all(linux, not(arch = "aarch64")))'.build] -cxxflags = ["-march=x86-64-v2"] -``` - -`[target.windows]` 与 `[target.'cfg(windows)']` 完全等价 —— 裸别名 -`windows` / `linux` / `macos` / `unix` 都不是合法的目标三元组,因此不存在歧义。 -单个 OS/族用裸形式,arch/env 条件与组合子用 `cfg(...)`。 - -- **可用键**:`dependencies` / `dev-dependencies` / `build-dependencies` / - `feature-deps.`(mcpp 2026.8.6.2+ —— 见 §2.14;feature 本身无条件注册, - 只有它的依赖集合受限定),以及带 `cflags` / `cxxflags` / `ldflags` / `sources` - 的 `build`(mcpp 0.0.95+ —— 条件源码 glob,例如把 `src/x86/**/*.asm` 收在 - `cfg(arch = "x86_64")` 之后;`!` 排除 glob 在此同样有效),再加 `flags` 与 - `include_dirs` / `include_dirs_after`(mcpp 0.0.102+), - 以及 `private_include_dirs` 与 `std-module-flags`(mcpp 2026.9.1.1+)。 -- **`build` 接受的恰好是*可叠加的构建输入*集合** —— 那些以追加方式合并、 - 并在谓词求值之后被消费的东西,也就是 `BuildInputs` 的成员表。`linkage`、`target` - 与档案开关刻意不在其中:它们是**目标选择的输入**(用一个针对 `target` 求值的谓词 - 去条件化 `target` 是循环的),或者需要覆盖而非追加的语义。 - 集合之外的键会被报出并忽略;消息里列出的正是它比对用的那份集合,因此不会与检查漂移。 -- **按解析后的目标求值** —— 交叉构建取 `--target` 三元组,否则取宿主。因此原生 - Linux 构建**根本不会下载** `[target.windows]` 依赖。 -- **谓词的键**:`os`、`arch`、`family`、`env` —— 三元组的坐标 —— 以及自 mcpp - 2026.9.1.1 起的五个目标侧层名 `compiler`、`compiler-runtime`、`kernel-abi`、 - `c-abi`、`c++-abi`(见[14 —— 目标侧](14-target-side.md))。`accelerator` 同样 - 是这里的键,由本次构建自己的 `accel`(`--accel` 或 `[build] accel` 里的后端名) - 回答,因此它是对一个集合的成员判定;`accelerator = "none"` 则是一段用来说 - 「本次构建没有命名任何后端」的写法,而不必枚举它不是的那些后端。裸词 - `linux` / `macos` / `windows` / `unix` 是对应 `os` / `family` 判定的糖。 - 集合之外的键会被报成一条 schema 警告,且该段不生效 —— 它过去静默地求值为假, - 而那与「这一段本就不该匹配」读数完全相同。 -- **被解析的层的谓词不能选择依赖。** 层是**从**依赖图解析出来的,因此由它选出的 - 依赖会决定它正在询问的那个答案。`[target.'cfg(c-abi = "musl")'.dependencies]` - 会被报出并忽略;同一谓词下的 `build` 输入照常生效。`accelerator` 不在此列 - (mcpp 2026.9.6.5):它是构建的输入而不是图给出的答案,所以 - `[target.'cfg(accelerator = "cuda")'.dependencies]` 生效。 -- **优先级**:精确三元组表胜过 `cfg`/别名表;多个命中的谓词表,其 flag 按序拼接。 - 条件项追加在无条件 `[build]` 项**之后**,因此在 GNU「最后一个 flag 生效」的 - 规则下,条件规则会覆盖更宽的无条件规则。这正是让按 OS **移除**成为可表达的原因: - - ```toml - [build] - flags = [{ glob = "third_party/zlib/**", defines = ["HAVE_UNISTD_H=1"] }] - - # clang-MSVC 没有 :撤销基础 define,加上 windows 的那个。 - [target.'cfg(windows)'.build] - flags = [{ glob = "third_party/zlib/**", - defines = ["NO_FSEEKO"], cflags = ["-UHAVE_UNISTD_H"] }] - ``` - -- **未命中当前目标的条件 `flags` 条目根本不存在**,因此它不会产生 - 「glob 未匹配到任何源文件」的警告。于是一份 manifest 可以同时携带三个 OS 的 - flag 表,而不会在另外两个上制造噪声 —— 与未启用 feature 的条目根本不存在是 - 同一个道理。**无条件**表里的零命中 glob 仍然告警,因为那里它是真实缺陷。 -- **`toolchain` / `linkage` / `sysroot` 仅限精确三元组** —— 它们描述某一个具体的交叉目标, - 因此写在 `[target.]` 下(见上),而不是裸别名或 `cfg(...)` 下。 - -#### `sysroot` —— 目标的 C 库 - -`sysroot`(mcpp 2026.8.20.2+)覆盖目标表为某个三元组绑定的 C 库,与 `toolchain` -覆盖编译器 pin 同轴:一个指名目标所解析的编译器,另一个指名它的 C 库,两者在工程 -有理由与之分歧之前都只由引擎决定。 - -```toml -[target.riscv64-none-elf] -sysroot = "xim:newlib-riscv@4.4" # a different C library -``` - -```toml -[target.riscv64-none-elf] -sysroot = "" # no C library at all -``` - -**键缺席与键为空是两个不同的答案。** 缺席继承目标表的 C 库。存在且为空是 -**零 libc 档**:不解析任何 C 库,不加入头文件与库目录,链接行上只有工程与其依赖 -提供的内容,`#include ` 不再解析。内核与 bootloader 要的正是这一档,而把 -两种情形合并会让这类工程静默地把目标的 C 库拿回去。 - -取值是 xpkg 引用或空字符串;裸名在解析清单时即被拒绝,因为接受它会导致什么都不安装, -然后在很晚的时候以「缺少 libc」失败。 - -构建程序可以询问供给 sysroot 的是哪个 C 库**载荷**:`mcpp::target_libc()` 返回该包的 -名字,`mcpp::target_libc_profile()` 返回目标 ISA 档位对应的子目录。零 libc 档上两者均 -为空。 - -**这与「目标侧解析出的 C 库是哪一个」不是同一个问题。** `target_libc()` 命名的是 -mcpp 装上的那个载荷,而这个值是目标侧解析的一项**输入** —— 依赖图里的包可以改为供给 -C 库,那时解析出的 `c-abi` 就不是这里返回的东西。要按已解析的层分支,请用层谓词: -`[target.'cfg(c-abi = "musl")'.build]`(见[14 —— 目标侧](14-target-side.md))。 -这一段在 2026.9.1.1 之前写的是「解析到的是哪份 C 库」,那是两者里错的那一个。 -参见[13 —— 裸机与 freestanding 目标](13-baremetal.md)。 - -### 2.7.2 裸机(`os = none`)—— freestanding target - -`riscv64-none-elf` 与 `riscv32-none-elf` 是底下没有操作系统的 target。它们不需要 -逐宿主的交叉工具链:clang 与 lld 天生是交叉编译器,任何能装 llvm 载荷的宿主都能 -产出它们。 - -本节是清单参考。示例部分 —— 生成工程、运行、在目标上测试、freestanding 标准库 -子集,以及编写板级支持包 —— 在 -[13 — 裸机与 freestanding 目标](13-baremetal.md)。 - -```bash -mcpp build --target riscv64-none-elf -mcpp run --target riscv64-none-elf # 经 [target.].runner -``` - -**从板级支持包起步** - -下面这些几乎都不需要手写。板级支持包(BSP)自带 C 库、启动代码、内存布局和模拟器, -所以跑起一个镜像的最短路径是: - -```bash -mcpp new blinky --template riscv-virt-rt -cd blinky && mcpp run -``` - -生成的 manifest 里没有链接脚本、没有加载地址、没有 libc、没有模拟器 —— 连 -`[target.*]` 段都没有。本节余下的内容讲的是**这样一个包提供了什么**,也就是要给 -一块还没有 BSP 的板子写一个时该照着做什么。 - -**freestanding target 上有什么不同** - -| | | -|---|---| -| 链接线 | `-nostdlib -nostartfiles -static`,且不带任何 hosted 的东西 —— 没有 crt 文件、没有动态链接器、没有 C++ 运行时。链接器用**绝对路径**寻址(`-fuse-ld=<载荷>/bin/ld.lld`),因为 `-fuse-ld=lld` 走 `PATH` 解析,在任何 binutils 排前面的机器上都会找到 GNU ld。 | -| ISA flag | `-march` / `-mabi` / `-mcmodel` 来自 target 表,所以只写 `--target ` 就足以产出正确的目标文件。 | -| C 库 | **属于 target**,由 mcpp 从目标自己那一行解析,和解析编译器同理 —— 裸机工程不声明 libc,正如宿主工程不声明 glibc。它的头进入每一个翻译单元,它的目录进入链接搜索路径,所以板级包用**裸名**选库(`-lc`、`-lcrt0-semihost`)。**选哪个**启动对象、**用哪份**链接脚本仍然是板级决定。 | -| 异常与 RTTI | **关闭**,作用于每一个翻译单元,依赖的也不例外。没有 unwinder、没有 `libc++abi`,谁都抛不了;否则光是 `std::optional::value()` 就会拉进 `__cxa_throw` 等四个未定义符号。它属于 **target** 而不是工程的 `cxxflags`,因为 **BMI 会记录这个配置** —— 带异常编出来的依赖,不带异常的单元 import 不进来。 | -| `import std` | **不可用。** `std` 是覆盖整个库的一个模块 —— 线程、文件系统、iostreams 全在内 —— 没有 OS 就没有它的子集可编。取代它的是两个普通依赖:**板级包**包住目标的 C 库,**`std-freestanding`** 提供标准库里不需要 OS 的那部分(实测 libc++ 110 个头里的 103 个)。 | -| 入口点 | **只要有人提供 `crt0`,`int main()` 就能用** —— 板级支持包通常就提供它,于是固件的入口就是普通的 `main`,它的返回值经 semihosting 传回宿主。**只有零 libc 的板子**才需要显式声明 target 并把 `main` 指向携带 `_start` 的那个文件。 | - -**一个最小固件** - -```toml -[package] -name = "fw" -version = "0.1.0" - -[build] -ldflags = ["-T", "/abs/path/to/link.ld"] - -[targets.firmware] -kind = "bin" -main = "src/start.S" # 入口在汇编里,不在 main() - -[target.riscv64-none-elf] -runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", - "-no-reboot", "-bios", "default", "-kernel"] -``` - -**`runner` —— `mcpp run` 如何执行本机跑不了的东西** - -裸机镜像的 ISA 不对、没有 loader、且期望独占整个地址空间;直接 exec 它得到的是 -"Exec format error"。`runner` 就是挡在它前面的 argv 模板。产物路径会被**追加**, -或者在模板含 `{}` 时替换进去。 - -mcpp **刻意不提供默认 runner**。用哪个模拟器、哪个机器型号、哪种固件模式都是板级 -事实 —— 同一 ISA 的两块板需要不同 argv(OpenSBI 启动用 `-bios default`,picolibc -镜像用 `-bios none -semihosting`)—— 引擎一旦猜一个,另一块板就得跟它打架。板级 -支持包通常会提供它。 - -### 2.7.3 hosted 目标上的 `runner`(2026.9.2.1+) - -`[target.].runner` 对每一个精确三元组生效,不限于裸机。一个 hosted 交叉产物 -—— 在 x86_64 机器上构建的 `aarch64-linux-musl` —— 有的宿主能直接执行(binfmt_misc -注册了 qemu-user),有的宿主以 `Exec format error` 拒绝;属于哪一种是机器的性质,不是 -三元组的性质。mcpp 不预测它:要么通过工程声明的 runner 执行产物,要么尝试直接执行并 -报告内核的回答。 - -```toml -[target.aarch64-linux-musl] -runner = ["qemu-aarch64-static"] -``` - -规则对 `mcpp run` 与 `mcpp test` 相同: - -- **声明了 runner 就使用它。** 其第一个元素由 mcpp 定位:先在 `[xlings.workspace]`(§2.13) - 声明的每个载荷的 `bin/` 目录里找,再找 `PATH`。`PATH` 上的裸名会命中 xvm shim,而 - shim 按当前 SubOS 而非按包作答;先查载荷,runner 才能直接写工程声明过的程序名。 -- **声明的 runner 找不到或启动不了是错误**,错误里带程序名、搜索过的目录和 errno。 - 不回落到直接执行:让产物在另一个解释器下带着另一组参数运行,正是这个键要防止的 - 失败。 -- **没有 runner 且内核拒绝产物:** `mcpp run` 报告拒绝原因与应当写的键,退出码 2。 - `mcpp test` 把每个测试报告为未运行,原因只打印一次,退出码 2(§2.7.3.1)。 -- **`--no-runner`** 直接执行产物并忽略声明的 runner。它陈述的是关于本机的事实 —— - 这个三元组在本机是原生的 —— 清单没有承载它的轴;为 x86_64 开发者写的 runner 在 - aarch64 机器上仍可用。 - -通过 `[xlings.workspace]` 装模拟器是 CI 任务或单一宿主类别工程的形态。索引里的 -`qemu-user-aarch64` 只为 x86_64 Linux 构建,而这张表在每台构建本工程的宿主上都会 -provisioning,所以条目按平台写(§2.13): - -```toml -[xlings.workspace] -"xim:qemu-user-aarch64" = { linux = "" } # Linux 上存在即可,版本不限 - -[target.aarch64-linux-musl] -runner = ["qemu-aarch64-static"] -``` - -宿主装不了的包是硬构建错误,所以不带平台形式的条目会让工程在 macOS 与 Windows 上 -无法构建。同样没有这个包的 Linux/aarch64 宿主传 `--no-runner`。 - -#### 2.7.3.1 `mcpp test` 与未运行的测试 - -产物在本机无法执行的测试既没有通过也没有失败。`mcpp test` 把它报告为**未运行**, -在确立原因时打印一次,在汇总里重复原因的第一行,退出码 2: - -``` -warning: this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); declare [target.aarch64-linux-musl].runner, or pass --no-runner on a host that can -smoke ... not run -error: test result: NOT RUN. 0 passed; 0 failed; 1 not run (this host cannot execute aarch64-linux-musl artifacts: Exec format error (error 8); ...); finished in 0.41s (build 0.39s + run 0.00s) -``` - -退出码 1 含义不变 —— 有测试运行并失败;0 表示每个测试都运行并通过。 -`--message-format json` 在每条记录上带 `"status":"not_run"` 与 `reason`,在汇总记录上 -带 `not_run` / `not_run_reason`(见 [11 —— 机器可读输出](11-machine-output.md))。 - -### 2.8 `[features]` —— Feature(Cargo 风格,可加性) - -#### 表形式 —— 让 feature 贡献的不止是隐含 feature - -`[features]` 的条目除了写成数组,还可写成**表**,从而让该 feature 在隐含 feature -之外,携带包自有的预处理 `defines`、feature 门控的源 glob(`sources`,mcpp -0.0.95+——列出的 glob 离开默认构建,仅当 feature 激活时才编译,与 index 描述符的 -`features..sources` 完全对等;这正是 vendored 大库最高频的形态:*feature = -一组源文件 + 一个 define*)、feature 门控的 per-glob 编译旗标(`flags`,mcpp -0.0.101+),以及 capability 的 `requires` / `provides`(见 §2.8.1): - -```toml -[features] -default = [] -# 数组简写:仅隐含 feature。 -docking = ["extra"] -extra = [] -# 表形式:激活时贡献一个包自有的宏。 -mpl2only = { defines = ["EIGEN_MPL2_ONLY"] } -# 表形式:宏 + 一个隐含 feature。 -fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] } -# 表形式:feature 门控源 + 与其同居的 per-glob 旗标。 -simd = { sources = ["src/simd/**"], flags = [ - { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] } -``` - -- **表形式恰好接受** `implies`、`forward`、`defines`、`sources`、`flags`、 - `requires`、`provides`。其余键会被报成一条 schema 警告并忽略(mcpp 2026.9.1.1+); - `deps` 单独报为「保留」(它是计划中的而不是写错的),并指向 `[feature-deps.]`。在该版本之前,`[features]` - 是唯一一个完全没有 schema 检查的结构化段落 —— 把 `include_dirs` 误写进 feature 里 - 会零诊断地构建成功,而同样的错误写在 `[build]` 里会被报出来。 -- `defines` 为**裸**宏名(不带 `-D`);feature 激活时每个脱糖为 `-D`,加到该包 - 自己的编译上——与 `[targets.*] defines` 完全一致。按约定仅限包**自有**的带命名 - 空间宏:feature **不**注入自由的包级 `cflags`/`ldflags`,否则会破坏加性的 feature - 并集模型。链接旗标来自 provider 依赖(§2.8.1),而非 feature。 -- 每个激活的 feature 仍会得到自动的 `-DMCPP_FEATURE_`,`defines` 与之叠加。 -- `flags`(mcpp 0.0.101+)与 `[build].flags`(§2.3)共用同一有序 inline-table 数组 - 文法(`glob` 必填,加 `cflags`/`cxxflags`/`asmflags`/`defines`;与 - `[[build.flags]]` 一样也接受 `[[features..flags]]` 拼写)。feature 激活时 - 条目追加在 base `[build].flags` **之后**(feature 按名 - 序),"last flag wins" 使 feature 规则可覆盖更宽的 base 规则;未激活时条目根本 - 不存在(不会有死 glob 告警)。这让 feature 的组内专属旗标与其 `sources` 同居, - 而不必写成 base 规则、在 feature-off 构建里留下必死的 glob。与 `defines` 不同, - feature `flags` 是**私有 per-TU 构建旗标**——永不传播给消费者(与 `[build].flags` - 同契约),因此不破坏加性模型:glob 限定作用面、顺序确定、无跨包效应。 - - -#### 作为构建规则的 feature(mcpp 2026.9.7.1+) - -两个键把一个 feature 变成其它包可以使用的构建规则。它们是消费者只写一条依赖边、 -不写构建程序的原因。 - -```toml -[features.rules-spirv] -sources = ["rules/spirv.cppm"] -rule_module = "mcpp.rules.spirv" -device_extensions = [".comp", ".vert", ".frag", ".glsl"] -``` - -`device_extensions` 陈述这条规则编译哪些**设备源**扩展名。激活了该 feature 的消费者 -会把它们分类为设备源 —— 不扫描 import、不产 BMI、由 mcpp 不驱动的编译器编译。这与 -`[build] module_extensions` 是同一个形状:mcpp 知道设备源*是什么*,不知道 `.cu` 是 -CUDA,所以**一门新设备语言不需要引擎改动**。 -[20 — 异构硬件构建](20-heterogeneous-builds.md) 里那句「第六个后端是一个包而不是一次 -引擎改动」由此才成立;`.slang` 已从 mcpp 的内置表中移除,现在正是经由这条路到达的。 - -`rule_module` 给出消费者的构建程序为够到这条规则而 import 的模块,以及它调用的 -`compile()` 所在。它是**声明**的而不是从源码扫描出来的,因为那个程序必须在任何东西 -被编译**之前**写出来,而一次为了决定写什么而去扫描依赖源码的构建会把两者的顺序颠倒。 - -由此得出两件事,而且都不把任何包名放进 mcpp: - -- **`host-module = true` 被推出来。** 一个点名了规则模块的 feature 已经说过那是使用 - 它的唯一方式,所以依赖边不必再说一遍。 -- **没有 `build.mcpp` 的包会得到一个。** mcpp 把这些规则描述的程序写进构建目录并编译 - 它。自带程序的包保留自己的:合成只填补缺席、绝不覆盖;而生成出来的那份就是这个工程 - 本来要手写的那份,所以接管它是一次复制加一次编辑。 - -feature 仍然**按名字**请求: - -```toml -[build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"] } -``` - -早先的一版设计从工程源码里出现的扩展名推导这个集合。它被撤销了,因为两个包可能认领 -同一个扩展名 —— 第三方写一条处理 `.cu` 的规则是会发生的事 —— 也因为 manifest 的职责 -是描述这次构建,而派生出来的 feature 集合让文件不再陈述它。 - -两个键必须成对出现。只写其一是一条没有任何东西能据以行动的声明,会在解析期被拒绝, -而不是留到消费者的构建里。 - - -### 2.8.1 `provides` / `requires` —— 能力(后端选择) - -**capability(能力)** 是一个共享的抽象名字(如 `blas`)。包可以 *provide*(提供) -一种能力;feature 可以 *require*(需要)一种能力而非点名某个具体包,解析器会从依赖 -图中绑定**恰好一个** provider。这样就能在多个可互换后端(OpenBLAS / MKL / …)中选其 -一,而不必把选择写死进库里。 - -```toml -# provider 包为任何 require 它的依赖方满足某能力。 -[package] -name = "compat.openblas" -version = "0.3.0" -provides = ["blas", "lapack"] -``` - -```toml -# 消费方经由自己的某个 feature 来 require 这个抽象能力。 -[features] -use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } - -# 图中有 >1 个 provider 时,选其一(否则构建报错并列出候选)。 -[capabilities] -blas = "compat.openblas" # 等价于:mcpp build --cap blas=compat.openblas - -[dependencies] -compat.openblas = "0.3.0" # provider 必须是图中真实存在的依赖 -``` - -保留前缀 `mcpp:` 命名本引擎解析的目标侧层,这些名字对照一个闭集校验。 -包级 `requires` 数组承载对称的陈述 —— 某个目标侧层必须解析为什么, -本包才可用。 - -```toml -[package] -name = "acme.llvm-runtime" -version = "0.1.0" -provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"] -requires = ["mcpp:compiler=llvm"] -``` - -作为标准库的包在 `[build]` 下陈述它的 `std` 模块源, -其所需的 flag 在那里与任何其它构建输入一样可条件化。 - -```toml -[build] -std-module = "llvm-generated/std.cppm" -std-compat-module = "llvm-generated/std.compat.cppm" -std-module-flags = ["--no-default-config", "-nostdinc++"] - -[target.'cfg(c-abi = "musl")'.build] -std-module-flags = ["-D_GNU_SOURCE"] -``` - -五个层、约束它们的规则与相应诊断,见 [14 - 目标侧](14-target-side.md)。 - -绑定是**确定性**的: - -| 图中某被需要能力的 provider 数量 | 结果 | -|---|---| -| 恰好一个 | 自动绑定(无需配置) | -| `[capabilities]` pin / `--cap` 指定了一个 | 以 pin 为准 | -| 零个 | **报错**:没有包提供 `` | -| 两个及以上且未 pin | **报错**并列出候选——绝不静默猜测 | - -被绑定 provider 的链接/头文件旗标经由常规依赖机制流到消费方;capability 层是那道 -*选择与校验* 步骤,把"静默选错后端 / 缺后端"变成构建期的显式报错。 - -**绑定选中的是 provider,它不裁剪链接行。** 依赖包的目标文件一律进入消费方的链接, -与它的能力是否被绑定无关。实测:两个包都提供同一能力且都定义 `cap_probe`,未 pin 时 -解析按上表报错;按提示用 `[capabilities]` pin 其中一个之后,构建走到链接器才失败—— - -``` -ld: obj/mcpplibs_pa/src/impl.o: in function `cap_probe': - multiple definition of `cap_probe'; obj/mcpplibs_pb/src/impl.o: first defined here -``` - -这一点对**多个 provider 定义同一批符号**的能力有影响 —— 全程序单例(例如 -`operator new`),或名字集合固定的 C 接口。对这类能力,图中出现两个 provider 是**待修的 -缺陷**而非可 pin 的歧义:pin 会把一个点名两个候选的报错,换成一个点名 mangled 符号的报错。 -可互换的**库**(各 BLAS 实现导出不同的符号集合,按链接选其一)不受此影响。 - -#### `exclusive` —— 包声明自己是唯一提供者 - -上一段描述的是一个引擎**看不见**的缺陷:要看出两个 provider 定义了同一批符号, -需要它们的目标文件,而绑定 capability 时那些还不存在;而「一律拒绝重复 provider」 -又会打断同一段里那个合法的 BLAS 用例。 - -所以由包自己声明: - -```toml -[package] -name = "compat.cublas" -provides = ["gpu-blas"] -exclusive = ["gpu-blas"] -``` - -两个都提供 `gpu-blas` 的包,只要其中至少一个声明了独占,就在绑定 capability 时 -被拒绝 —— 在任何东西被编译之前,并点名该能力与双方: - -``` -error: capability 'gpu-blas' is provided by more than one package, and they - declare it EXCLUSIVE. - providers: [compat.cublas, compat.rocblas] - exclusive: [compat.cublas, compat.rocblas] -``` - -该拒绝在 `--format json` 里报 `exclusive-capability`(见第 11 章)。 - -#### `version-floor` —— 对机器的要求高于它所有 - -有些关于机器的事实约束着能为它构建什么,而忽略它们时的失败来得很晚: -一个针对比它将遇到的驱动更新的运行时构建出来的程序,**干净地链接**, -在第一次使用时失败,而消息里两侧都没有。 - -包声明它需要什么: - -```toml -[[runtime.requirements]] -kind = "version-floor" -value = "cuda.driver >= 12.0" -``` - -而某个在**安装期**(探测该发生的地方)确立了机器某项事实的包,声明它: - -```toml -[runtime] -provides = ["cuda.driver=12.4"] -``` - -mcpp 在绑定 capability 时比较二者,并在任何东西被编译之前拒绝, -报 `version-floor-unmet`: - -``` -error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4. - stated by: driverfact -``` - -**没有任何厂商词汇抵达引擎。** 它读到的是一个名字、一个关系和一个版本; -`cuda.driver` 是流过的数据,一个 mcpp 从未听说过的后端比较方式完全相同。 - -**没人回答的下界是沉默的。** 一台从未声明自己有什么的机器,不是「未满足下界」的机器, -而是「没人问过」的机器。把「我们不知道」变成「不行」正是这个机制要避免的失败, -并且有直接判据:`tests/e2e/603_version_floor.sh` 会构建一个下界指向无人提供之物的工程。 - -**它是关于这个包自己的符号的声明**,所以一条指向本包并不提供的能力的条目会被报为 -schema 警告:那里没有可独占的东西。而无人声明独占的能力行为完全不变 —— 两个 BLAS -实现照常共存,既有的「两个或更多、未 pin」报错也仍然只在**有人 require** 该能力时出现。 - -### 2.8.2 `[feature-deps.]` —— 由 feature 拉取的依赖 - -在 `[feature-deps.]` 下声明的依赖是**可选的**:仅当该 feature 激活时(根 `--features`, -或某依赖 spec 的 `features = [...]`)才会被解析。`[dependencies]` 中的依赖始终被解析; -可选性由声明的*位置*表达,而非某个标志位。 - -```toml -[features] -use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } -backend-openblas = { implies = ["use_blas"] } - -# 仅当 `backend-openblas` 激活时才拉取。每个条目都是完整的依赖 spec -#(version/path/git + 其自身的 features)。 -[feature-deps.backend-openblas] -compat.openblas = "0.3" -``` - -**写 `"^0.3.0"`,而不是 `"0.3.x"` 或 `"0.3"`。** 以索引中确定存在的包作对照, -判据取**构建成功**: - -| 写法 | 结果 | -|---|---| -| `cmdline = "0.0.1"` | 构建通过 | -| `cmdline = "^0.0.1"` | 构建通过 | -| `cmdline = "0.0"` | 解析通过,随后 `install path missing after fetch` | -| `cmdline = "0.0.x"` | `E_NOT_FOUND`,点名的是包 —— 而该包存在 | - -这三种结果值得分开,因为两个更弱的判据各自会放行一种不可用的写法: -「没有 `E_NOT_FOUND`」放行两段前缀,「解析通过」同样放行它。**只有对着真实索引构建 -一次**才能定论。 - -这一点在此处比在 `[dependencies]` 中更要紧:**实现取不回来的 feature 等于不存在的 -feature**,而开发期使用 **path** 依赖的工程根本不查索引 —— 该失败只在发布之后才出现, -而且是出现在别人身上。 - -该机制与能力(§2.8.1)组合:单个 `backend-openblas` feature 既**拉取** provider -(`compat.openblas`,其 `provides = ["blas"]`),又**开启**消费方开关 -(`implies = ["use_blas"]`,其 `requires = ["blas"]`)。当图中只有一个 provider 时, -能力自动绑定——消费方只需写 `features = ["backend-openblas"]`。 - -在索引包的 Lua 描述符中,等价写法为内联形式: - -```lua -features = { - use_blas = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } }, - ["backend-openblas"] = { - implies = { "use_blas" }, - deps = { ["compat.openblas"] = "0.3.x" }, - }, -} -``` - -#### 保持可替换的默认实现 - -同样这三件东西,也覆盖"库希望**提供**一份实现但不**强加**一份"的情形 —— 全程序单例, -例如 `operator new`、日志 sink、panic handler: - -```toml -[features] -default = [] -# 消费方开关:"我用到了本库中需要分配器的那部分"。 -alloc = { requires = ["freestanding-allocator"] } -# 内置默认:激活它就够了。 -alloc-kal = { implies = ["alloc"] } - -# 仅在 `alloc-kal` 激活时解析,因此库本体不携带对该实现的依赖。 -[feature-deps.alloc-kal] -std-freestanding-alloc-kal = "0.1.x" -``` - -三种用法各一行: - -| 消费方需要 | 清单里怎么写 | -|---|---| -| 不用会分配的那部分 | `std-freestanding = "0.2.0"` —— 分配器不进图 | -| 默认实现 | `features = ["alloc-kal"]` —— 实现随之进图,**无需知道其包名** | -| 自己的或第三方的 | `features = ["alloc"]` 加一个 `provides = ["freestanding-allocator"]` 的包 | - -有两条性质使该形状优于无条件随包提供实现。随包提供实现的库替程序做了本属程序的决定, -而且**撤销不掉**:feature 是**加性**的,消费方没有把某个默认**关掉**的手段。以及,由于 -依赖包的目标文件无条件参与链接(§2.8.1),随包的默认加上程序自备的那份是**重复定义** -而非替换 —— 让 C++ 标准库能提供可替换 `operator new` 的那套归档语义,对包依赖并不适用。 -把实现放在开关之后,意味着两者**从不共存**。 - -### 2.8.3 `[scan_overrides.""]` —— 作者断言的扫描结果 - -默认的模块扫描器是文本级的一遍扫描,它(刻意地)拒绝条件预处理块内部的 `import` -语句。有些合法的模块单元带着这种写法 —— 例如 fmt 官方的 `src/fmt.cc` 把 -`import std;` 收在 `#ifdef FMT_IMPORT_STD` 之后。当该文件的 import 集合已知且稳定时, -用声明取代扫描: - -```toml -[modules] -sources = ["src/**/*.cppm", "vendor/fmt.cc"] - -[scan_overrides."vendor/fmt.cc"] -provides = ["fmt"] # 每个单元至多提供一个模块 -imports = ["std"] -``` - -被 glob 命中的文件跳过文本扫描,声明的单元直接进入模块图。该声明**每次构建都被审计**: -编译器自己对该文件的 P1689 扫描结果(`.ddi` dyndep 输入)会与之比对,任何分歧都会让 -那条编译边失败并打印双方 —— 陈旧的声明无法静默污染模块图。未命中任何源文件的 -override glob 是错误。 - -同一个键在 xpkg 描述符(索引包)中同样存在: - -```lua -mcpp = { - sources = { "*/src/fmt.cc" }, - cxxflags = { "-DFMT_IMPORT_STD" }, - scan_overrides = { - ["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } }, - }, -} -``` - -要把 plan 与 ddi 的比对审计扩展到**每一个**模块单元(而不只是 override), -在生成构建时设置 `MCPP_VERIFY_MODGRAPH=1`。 - -### 2.9 `[profile.]` — 构建档案 - -```toml -[profile.dist] -opt = 3 # -O 级别(数字或 "s"/"z" 字符串) -debug = false # -g -lto = true # -flto(注意:部分打包 gcc 未启用 LTO 插件) -strip = true # 链接期 -s -# passthrough 逃生口(固定键、开放值): -cflags = ["-fno-plt"] -cxxflags = ["-fno-plt"] -ldflags = [] -``` - -- 选择与默认:裸 `mcpp build` 走 **`dev`** 档(`-O0 -g`)——主流惯例(参照 - Cargo/Meson/CMake/Zig/Bazel)。**release 为 opt-in:** `mcpp build --release`(短写)或 - `--profile release`;`--dev` 是 dev 的显式短写。`mcpp test --profile ` 同理 - (被测代码与测试二进制都在该 profile 下编译)。 -- **项目级默认** —— `[build].default-profile = ""`(别名 `profile`)设置该项目在不带 - flag 时的默认。典型用途是"以发布优化为常态"的工具/库:`[build] default-profile = "release"`。 - 优先级:`--profile`/`--release`/`--dev` flag **>** `[build].default-profile` **>** 全局 `dev`。 - (默认 dev 的项目在产出可分发物时应显式 `--release`。) -- 内置档案:`release`(-O2)/ `dev`、`debug`(-O0 -g)/ `dist`(-O3 + strip; - **不默认开 lto**)。`[profile.<内置名>]` 可整体覆盖内置定义。 -- **每个 profile 各占一个构建目录。** 解析后的 profile 开关参与指纹,所以 - `target//` 下每个 profile 一个哈希目录,来回切换是增量而不是全量重编; - 代价是磁盘占用随实际使用的 profile 数量增长。 - -### 2.10 `[build] cache` — 依赖的全局构建缓存 - -从索引获取的依赖,其编译产物按包缓存在 `$MCPP_HOME/build-cache/v1/` 下,跨工程共享。 -依赖的产物与"谁在消费它"无关,所以工具链、profile、依赖版本相同的两个工程复用同一条目。 - -```toml -[build] -cache = "global" # "global"(默认)| "local" | "off" -``` - -| 模式 | 读缓存 | 写缓存 | 先清构建目录 | -|---|---|---|---| -| `global`(默认) | 是 | 是 | 否 | -| `local` | 否 | 否 | 否 | -| `off` | 否 | 否 | 是 | - -`local` 把所有依赖都编在本工程 `target/` 内 —— 排障时一次性排除"是不是缓存的问题", -也给 CI 一个无共享的可复现基线。`off` 额外清掉本次的 `target///` 做冷构建; -`--no-cache` 是它的兼容别名。 - -优先级:`--cache ` **>** `MCPP_BUILD_CACHE` **>** `[build] cache` **>** `global`。 -无法识别的值会被报出来(`--strict` 下为错误),而不是静默回落到 `global`。 - -**不进缓存的**:`path` 与 `git` 依赖(任意深度)以及 workspace 成员。它们的源码可以在 -`name@version` 不变的情况下改变,任何基于该身份的键都看不见这种变化。 - -查看与回收: - -``` -mcpp cache dir # 缓存在哪 -mcpp cache list [--json] # 条目、体积、最后使用时间 -mcpp cache info @ # 单条目详情,含它是用什么键输入编出来的 -mcpp cache verify # 逐条目校验清单与磁盘 -mcpp cache gc --max-size 5GiB # 按 LRU 收到容量预算内 -mcpp cache gc --older-than 30d # 或按"多久没用过"回收 -mcpp cache clean [--deps|--std|--all|--legacy] -``` - -条目的磁盘布局是带版本的。改动布局的 mcpp 版本会**一次性作废全部旧条目**, -所以升级后的第一次构建会重编依赖并重新填充 —— 不需要手工清理。 -2026.8.3.4 就是这样一次:条目里对象的地址现在相对**包**自身, -而不再相对"最先填充这个条目的那个工程"的构建目录。 -`mcpp cache verify` 另外会报告任何逃出条目的记录地址, -使这条不变量可以离线审计。 - -### 2.11 `[runtime]` — provider-neutral 运行时契约 - -```toml -[runtime] -requirements = [ - { kind = "capability", value = "display.present", phase = "run", required = true }, - { kind = "soname", value = "libwidget.so.1", phase = "link", required = false }, -] -provides = ["display.present"] -artifacts = [ - { role = "library", path = "runtime/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:...", host_fingerprint = "host-1" }, -] - -# 平台无关 LinkIntent;路径相对本包根目录。 -libraries = ["widget"] -link_library_dirs = ["lib"] -transitive_needed_dirs = ["runtime/closure"] -runtime_search_dirs = ["runtime"] -frameworks = ["WindowKit"] -deploy_files = ["bin/widget.dll"] - -# 多 provider 时使用精确 canonical identity。 -[runtime."display.present"] -provider = "acme.widget-runtime@2.0.0" -``` - -`requirements` 记录非空 `kind`/`value`、`link` 或 `run` 阶段,以及是否强制 -(`required` 默认 `true`)。`artifacts` 必须含 `role`、`path`、`provenance`; -可选 requirement 仍保留为 provenance,但不会进入硬 ABI/doctor 输入。 -`libraries` 中显式的相对文件路径按声明包根目录解析;裸逻辑名仍按目标平台拼成库名。 -`abi`、`digest`、`host_fingerprint` 是可选证据。requester/provider 身份不由描述符 -填写:resolver 会用含 namespace、version、source/index provenance 的精确 PackageId -给 requirement 和 artifact 盖章。因此描述符不能冒充别的包, -`alpha.backend` 也不会与 `beta.backend` 混同。 - -只有 `provides` 会创建描述符侧 provider fact;需要某能力绝不会让 requester 自动 -成为 provider。显式 `[runtime.] provider=` 接受 canonical -`namespace.name@version`(或唯一无歧义的兼容拼写);不存在或同短名歧义都会 hard error。 -xlings SubOS 已选择的 provider/artifact fact 排在描述符 fallback 前。图形栈、driver、 -ICD、WSL 与 host provenance 选择由 xlings/xim 负责;mcpp 只记录、消费通用结果, -不探测 GPU 硬件。 - -LinkIntent 把不同发现阶段分开: - -| 字段 | ELF | Mach-O | PE/Windows | -|---|---|---|---| -| `link_library_dirs` | `-L` | `-L` | `-L` 或 `/LIBPATH:` | -| `transitive_needed_dirs` | `-Wl,-rpath-link` | 无 flag | 无 flag | -| `runtime_search_dirs` | 只进 RUNPATH/rpath,绝不进 `-L` | 只进 rpath | 无 flag | -| `frameworks` | 无 flag | `-framework` | 无 flag | -| `deploy_files` | copy edge | copy edge | 复制到产物旁,绝不成为 linker flag | - -一个兼容发布周期内仍读取旧字段:`library_dirs` 只映射到运行期搜索; -`dlopen_libs` 映射为必需的 run-phase soname requirement;`capabilities` 映射为必需的 -run-phase capability requirement。这些旧字段都不会创建 provider。 - -`target///resolution.json` schema 2 持久化 RuntimeBinding、canonical -requirements/providers/artifacts、LinkIntent、平台搜索机制与链接后 verdict。 -`mcpp why runtime` 只是最新存储文件的纯解释器:不重新解析 manifest,也不启动图形/ -硬件 probe。需要重新诊断所选 host provider 时使用 `xlings doctor`。 - -能力名使用分层小写 `domain.sub.role`(如 `display.present`)和前缀类 -`abi:`(如 `abi:glibc`,参与工具链 ABI 强制)。 - -### 2.12 `[package] platforms` — 平台声明 - -```toml -[package] -platforms = ["linux", "macos", "windows"] -``` - -声明包支持的平台(CI 矩阵提示,经 `mcpp why` 展示)。词表由 mcpp 固定 -(它拥有 target/triple 体系):`linux | macos | windows`;未知值 warning, -`--strict` 下报错。 - -对库目标执行 `mcpp pack` 时,会拿这条声明与**实际产出的腿**核对 —— 那是第一个 -有证据可核的时刻: - -| 情况 | 结果 | -|---|---| -| 某条腿的平台不在此列 | warning —— manifest 否认了一个包明明能服务的平台 | -| 声明了某平台却没有对应的腿,**且本宿主本来就能构建它** | warning —— 该平台的消费者会解析到这个包却找不到产物 | -| 声明了某平台却没有对应的腿,而本宿主根本构建不了它 | **不说话** | - -第三行才是这个检查可用的原因。正常的发布流程是 CI 上每平台各跑一次 -`mcpp pack`,于是 Linux runner 永远不会产出 macOS 腿 —— 为此告警会在每个跨平台 -包的每一次运行中触发,而**永远触发的告警会把真正该看的那条盖掉**。「本宿主能不能 -构建」与 `--target` 回答的是同一个问题(docs/08 §7.4)。 - -两者都只是 warning,绝不报错:覆盖度属于发布纪律,而能作判断的人看的是发布, -不是这一次构建。 - -### 2.12b `[package] accelerators` — 加速器声明 - -```toml -[package] -accelerators = ["cuda", "rocm"] -``` - -声明该包支持的加速器后端。与 `platforms` 同形:一个意图声明与 CI 矩阵提示, -由 `mcpp why` 展示,**不是门**。 - -与产物的 `accel` 字段刻意不同。声明由人手写、可以是期望值;`accel` 是从产生该二进制的 -那次构建测量出来的,并且是消费者被拒绝时所依据的东西。见 -[20 — 异构硬件构建](20-heterogeneous-builds.md)。 - -### 2.13 `[xlings]` — 工程的环境 - -```toml -[xlings.workspace] # 这个工程的环境里有什么 -cmake = "3.28" -"xim:picolibc-riscv" = "1.8.12" # 带命名空间的包 —— 必须带引号 -code = "" # 存在即可,版本不限 -llvm = { macosx = "20", default = "22" } -``` - -```toml -[xlings] -subos = "dev" # 指名的隔离环境 -``` - -`[xlings]` 是 mcpp 对 **xlings local project 机制**的书写面:让一个目录拥有自己 -环境的那份项目 `.xlings.json`。子段名与含义都是那份文件的,mcpp 原样物化进 -`/.mcpp/.xlings.json`,没有翻译层。 - -**`[xlings.workspace]` 是唯一的表。** 一条条目写出工程用哪个包、用哪个版本。 -mcpp 既供给它——机器上没有就装,有就映射——也把它物化成解析用的钉,于是工程写下 -的版本就是它的工具解析到的版本。 - -#### 条目的形式 - -| 形式 | 含义 | -|---|---| -| `cmake = "3.28"` | 该版本 | -| `llvm = "22"` | 已装的最高 `22.*`;版本前缀会被解析 | -| `code = ""` | 存在即可,版本不限 | -| `"xim:picolibc-riscv" = "1.8.12"` | 来自 `xim` 索引的包 | -| `llvm = { macosx = "20", default = "22" }` | 按宿主平台 | - -**带命名空间的包写成 `"<命名空间>:<名字>" = "<版本>"`,引号必需** —— TOML 的裸键 -不能含冒号。这是**推荐形态,也是所有官方包使用的形态**:一条条目先点名一个包, -再说用它的哪个版本,所以命名空间属于名字。 - -命名空间写在版本上(`picolibc-riscv = "xim:1.8.12"`)同样接受,因为物化出来的 -`.xlings.json` 里正是那种形态 —— 那里的键是 xvm target,scope 限定的是版本。 -两套词汇,同一条条目。两半都写且不一致是错误;同一个包用两种拼法出现两次也是错误。 - -平台键是 xlings 自己的 —— `linux`、`macosx`、`windows`,外加 `default`。`macos` -与 `macosx` 是同一个平台的两套词汇(mcpp 的三元组说前者,描述符与 xlings 的项目 -文件说后者),**凡是点名平台的地方两者都接受**。表里既没有本机这一项也没有 -`default`,就表示在这里什么都不声明。 - -#### 两条解析轴 —— 宿主与目标(mcpp 2026.9.6.4+) - -一条工具条目回答的是两个不同问题中的一个,写在哪张表里决定了是哪一个: - -| 写法 | 轴 | 按什么解析 | -|---|---|---| -| `[xlings.workspace]`,平台键写在值里 | 宿主 | 跑这次构建的机器 | -| `[target..xlings.workspace]` | 目标 | 解析后的目标(`--target`,否则是宿主) | - -两种写法都是正确的,谁也不取代谁。在构建机上执行的工具属于宿主轴;产物编译或链接 -时对着的载荷属于目标轴。 - -```toml -[xlings.workspace] -"xim:dpcpp" = "7.1.0" # 一个编译器,它在本机上跑 - -[target.'cfg(os = "linux")'.xlings.workspace] -"xim:glibc" = "" # 设备单元编译时对着的东西 -"xim:linux-headers" = "" -``` - -非交叉构建时两条轴指向同一个平台,所以把目标事实写在宿主轴上的工程是碰巧正确的, -而且照常工作。它在第一次被交叉构建时不再正确。**凡是产物编译或链接时对着的东西, -推荐写在目标轴上。** - -`[target..feature-xlings.]` 把条件与门组合起来,与 -`[target..feature-deps.]` 同形:selector 说的是哪些目标, -feature 说的是要不要。 - -```toml -[target.'cfg(os = "linux")'.feature-xlings.backend-vulkan] -"xim:shaderc" = "2026.3" -``` - -**这里的 selector 禁止命名被解析的层。** `c-abi`、`c++-abi`、`compiler`、 -`compiler-runtime`、`kernel-abi` 由依赖解析回答,而依赖解析发生在工具安装之后、 -构建程序运行之后。按这些层条件化的工具会被声明却永远装不上——构建照常成功,工具 -就是不在——所以这样的 manifest 会被拒绝,并把工具与谓词都点出来。改成按目标条件化, -或者用 feature 做门:`[feature-xlings.]` 在任何东西被供给之前就已知。 - -**`accelerator` 是例外,它被接受**(mcpp 2026.9.6.5)。它不由任何东西解析而来: -它是 `--accel`,或 `[build] accel`,在查找第一个包之前就已读入。以它为谓词的载荷 -与三元组谓词在同一趟合并,并像其它载荷一样被安装。 - -```toml -[target.'cfg(accelerator = "cuda")'.xlings.workspace] -"xim:cuda-nvcc" = "12.9.86" -"xim:cuda-cudart" = "12.9.79" -``` - -带设备孤岛的工程应当用这种写法。没有它,厂商工具包只能无条件声明或者干脆不声明, -于是不带加速器的 `mcpp build`——最便宜的那次构建,也是 CI 通常跑的那次——会为一个 -它根本没在编译的设备下载数 GB。 - -依赖同理:`[target.'cfg(accelerator = "cuda")'.dependencies]` 生效,而以被解析的层 -为条件的依赖不生效,因为后者会决定它正在询问的那个答案。加速器这条路径上没有任何 -循环。 - -条件只写在 selector 一处。selector 之下的值如果又带平台键,就是同一件事说了两遍, -会被拒绝,并把两半都指出来: - -``` -[target.cfg(os = "linux").xlings.workspace] xim:tool: the value carries platform -keys (linux, macosx), but [target.cfg(os = "linux")] already says which targets -this applies to. -``` - -`subos` 不按目标条件化:一个工程只有一个环境,所以 `[target..xlings]` -拒绝这个键,而不是把它丢掉。 - -**已发布的描述符不为目标轴条目携带边**,`mcpp publish` 会说明这一点。描述符按平台 -分块,而 selector 不是平台 —— `cfg(target_arch = "aarch64")` 不对应那份文件里的任何一块。 -**使用者**装到的东西来自顶层 `[xlings.workspace]`;目标轴对"本包自己的构建对着什么" -仍然是正确的。 - -这两条轴所属的一般规则见 [SPEC-004](../specs/manifest-semantics.md)。 - -#### `when` —— 哪些命令需要这个工具(mcpp 2026.9.4.2+) - -```toml -[xlings.workspace] -"xim:qemu-arm" = "9.2.4-1" # 每次构建都装,与从前一样 -"xim:codegen" = { version = "1.0", when = "build" } -"xim:probe-rs" = { version = "0.24.0", when = "run" } -"xim:clang-tidy" = { version = "20", when = "dev" } -``` - -包依赖从一开始就有这条轴 —— `[dependencies]`、`[build-dependencies]`、 -`[dev-dependencies]`。工具只有一张表,于是一个同时点名模拟器与调试探针的板级支持包 -会把两个都装给每一位消费者,包括只想把库编出来的那一位。 - -| `when` | 由谁安装 | 是否传播到消费者 | -|---|---|---| -| *(不写)* | 每个构建命令 | 是 | -| `build` | 每个构建命令 | 是 | -| `run` | `mcpp run`、`mcpp test` | 是 | -| `dev` | 只有声明它的那个包作为根时 | **否** | - -**不写 `when` 就是 2026.9.4.2 之前的行为**,所以没有任何清单需要改。收窄是可选动作, -不是作者必须回答的新问题。 - -`dev` 是唯一不传播的一档。它的含义是「声明它的那个包自己在被开发时」,所以依赖的 -`dev` 条目永远不会为消费者安装。其余各档都会到达消费者 —— 这正是板级包知道自己机器 -的意义:它声明一次模拟器,每一位消费者都拿得到。 - -档位写在**条目**上而不是另开一张表,理由与 `[dependencies]` 同时接受 `dep = "1.0"` -和 `dep = { version = "1.0", features = [...] }` 是同一条。带档位的条目**必须**写出 -`version`,哪怕留空(`version = ""` 表示「存在即可,版本不限」)—— 否则 -`{ when = "run" }` 与写错的 `version` 键无法区分。 - -#### `[feature-xlings.]` —— 某个 feature 才需要的工具 - -```toml -[features] -default = ["emulator"] -emulator = {} -hardware = {} - -[feature-xlings.hardware] -"xim:probe-rs" = "0.24.0" -``` - -同一张表,按 feature 门控,拼法沿用 `[feature-deps.]`。**不要 `hardware` -的消费者永远不会下载探针驱动。** 这里的条目同样接受 `when`。 - -`[features]` 里没有声明过的 feature 名会作为 schema 警告报出:它对谁都不激活、什么 -都不装,而这种工具的缺席只表现为「设备就是连不上」,是最难诊断的一种。 - -#### 规则包自带它的环境(2026.9.6.6+) - -上面那张表是工程**有主张**时写的。多数工程没有主张,写下的也就是空无一物: - -```toml -[build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda"], host-module = true } -``` - -这一条边就是全部声明。规则包在选中它的那个 feature、它所服务的加速器之下,声明自己 -需要哪些包、最低到哪一版: - -```toml -# 写在规则包里,不写在你的工程里 -[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] -"xim:cuda-nvcc" = ">=12.9.86" -"xim:cuda-cudart" = ">=12.9.79" -``` - -**两重门,都要开。** feature 说「要不要这个规则」,selector 说「哪些构建真的下载」。 -同一个工程的 CPU-only 构建两道门都不过,一个字节都不装。 - -「需要哪些包、最低到哪一版」是规则作者的知识。在每个用它的工程里重写一遍,是一份会 -悄悄过期的副本——规则动了,而那些工程不会跟着动。 - -#### 一个包一个版本(2026.9.6.6+) - -工具地址是 `[:][@<版本>]`,它的**身份是 `(namespace, name)` 二元组**。版本是 -这个包上的约束,不是它名字的一部分,所以 `xim:glibc`、`xim:glibc@2.40` 与 -`xim:glibc@>=2.38` 指的是同一个包。**一次构建只装它的一个版本。** - -装哪一个,分两步决定。 - -**裁决——离产物更近的声明赢。** 工程压过它依赖的包,于是一个钉覆盖规则的要求: - -```toml -# 工程侧,当它确实有主张时 -[target.'cfg(accelerator = "cuda")'.xlings.workspace] -"xim:cuda-nvcc" = "13.3.33" -``` - -**不带版本的声明弃权:** 它陈述了「要这个包」而没有陈述「要哪一版」,因此不会仅仅 -因为更近就压过一条下界。两条声明都带版本且不一致时,mcpp 会报出用了哪一条——一个 -只能表现为「声明了两个版本而目录里有一个」的覆盖,是要读者自己去文件系统里重建的 -事实。 - -**校验——赢家必须满足每一条落败的要求。** `>=`、`^`、`~` 以及逗号组合是**要求**。 -不满足的钉被拒绝,并同时点出两侧: - -``` -error: `xim:cuda-nvcc` is pinned to 12.0.0 by this project, and mcpp:plugins - requires >=12.9.86. - One version of a package is installed, so the two cannot both hold. - fix: pin a version satisfying >=12.9.86, or drop the pin and let the - requirement decide. -``` - -裸版本是**选择**而不是要求:两条互不相同的精确钉走裁决并被报告,不被拒绝。只有被 -陈述出来的要求才谈得上违反。 - -**这是一次比较,不是一次搜索。** 版本由裁决选定、再被检查,所以 mcpp 从不需要问索引 -「有哪些版本」,也就不带约束求解器。代价被写出来而不是藏起来:一个求解器本可满足的 -组合——工程写 `>=8.0`、规则写 `8.5.0`、而索引里最新是 8.3——会被拒绝,而拒绝消息里 -写着怎么往下走。 - -范围在两个方向上都被求解。`>=2026.1` 装到满足它的最高已发布版本,`>=2099.1` 作为 -不可满足被拒绝,而 `mcpp::xpkg_dir` 回答满足该范围的最高**已安装**版本——声明了下界 -的规则找得到下界带进来的东西。 - -#### 工程没点名的工具,其版本的来源 - -| 工程声明了 | 版本来自 | -|---|---| -| `[xlings.workspace]`,无 `subos` | 机器的环境,工程自己的条目叠在上面 | -| `[xlings.workspace]` 与 `subos = "<名>"` | 那个环境自己的 workspace;机器的不适用 | -| 两者都没有 | 机器的环境 | - -中间那行不是遗漏。指名的环境有自己的已安装集合,把机器的版本带进去会指向那里不 -存在的版本。**写 subos 就是要隔离,不写就是要机器的环境加上自己的条目。** - -在工程内执行的 `xlings use` 压过这张表,直到 mcpp 重写环境为止——它是最后合并的 -那一层。 - -#### `deps`,已被取代 - -`deps = ["xim:qemu-riscv@9.2.4-1"]` 是同一句话在 2026.9.3 之前的拼法。它仍然生效, -并且会被报告一次,同时给出该写的 `[xlings.workspace]` 那一行。**不拒绝**——拒绝会 -落到**依赖**的 manifest 上,而钉了那个包精确版本的工程改不了它。 - -#### `envs`,已移除 - -`[xlings.envs]` 曾被物化进 `.xlings.json`,而没有任何东西读它:程序的环境由它自己 -的包声明,环境的环境由那个环境声明。现在这个键是错误,并同时点名这两者。索引里没有 -任何包用过它。 - -### 2.14 依赖产出的 host 工具(mcpp 2026.8.5.1+) - -一个包能构建出消费者在**构建期**需要的二进制 —— `protoc`、`grpc_cpp_plugin`、 -`flatc`、`moc`、转译器。在依赖上声明: - -```toml -[dependencies] -protobuf = { version = "35.1", tools = ["protoc"] } -grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] } -``` - -每个名字必须是该包的一个 `kind = "bin"` target。mcpp 会**为构建机器**构建它, -并把绝对路径以 `MCPP_DEP__BIN_` 交给 `build.mcpp` —— 用 -`mcpp::dep_bin("protobuf", "protoc")` 读取(见 [07 — build.mcpp](07-build-mcpp.md))。 - -四条值得知道的性质: - -- **永远是 host 二进制。** 即使 `mcpp build --target `,工具依然为**本机** - 构建 —— 代码生成器必须在这里跑。它是一次独立的、面向 host 的子构建:工具包 - 自己的 `[toolchain]`、自己的依赖解析生效,不需要与当前构建一致。安全的原因是 - 可执行文件与工程代码**零 ABI 接触**。 -- **单一版本轴。** 工具的版本**就是**依赖的版本,所以「protoc 与其运行时不匹配」 - 这种情况**不可表达**。(把工具单独打包正是会出这个问题,而且它在**运行期**才咬人, - 不是编译期。) -- **默认关闭。** 没人要就什么都不构建,成本由消费者付。包用 `[features]` + - `required_features` 给昂贵的部分加门(protobuf 的 `protoc` 需要 libprotoc 的 - ~157 个额外 TU,只用运行时的人绝不该编译它)。 -- **全局缓存**,按 包版本 × host 工具链 × feature × 自身依赖闭包 键控 —— 每台机器 - 构建一次,而不是每个工程一次。 - -#### `[tools.overrides]` —— 使用已有的二进制 - -```toml -[tools.overrides] -"compat.protobuf:protoc" = "/usr/bin/protoc" -``` - -或者不改 manifest(CI、发行版打包): - -```bash -MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build -``` - -命中 override 会**完全跳过构建**。每个同类系统都提供这条逃生舱(vcpkg 的 -`VCPKG_HOST_TRIPLET`、CMake 的 `LLVM_NATIVE_TOOL_DIR`、Qt 的 `QT_HOST_PATH`), -理由一样:一个在本机构建不出来的工具**不能是死路**。它**刻意不进** cache key —— -逃生舱不是可复现输入。 - -#### `host-module = true` —— 可复用的构建规则以包分发 - -一条规则(比如「对这些 `.proto` 跑 protoc」)应该**写一次**,而不是复制进每个 -消费者的 `build.mcpp`。把它做成普通的 mcpp 库包再 import: - -```toml -[dependencies] -protobufgen = { version = "0.1.0", host-module = true } -``` - -```cpp -// build.mcpp -import mcpp; -import protobufgen; -int main() { return protobufgen::generate({"schema"}) ? 0 : 1; } -``` - -mcpp 会把该包的 lib 根模块**为 host 编译,且与 `build.mcpp` 在同一条命令里** —— -这正是 BMI 能用的前提:一个模块接口只对「在 standard / dialect / 编译器身份上与 -它一致」的编译可导入。 - -于是规则**有版本、能测试、能通过既有的包管理器分发**,而且是用 **C++** 写的 -—— 不引入第二门语言,这正是 `build.mcpp` 存在的理由。 - -**模块名是规则源码自己声明的那个**(mcpp 2026.8.29.1+)。`export module -acme.rules.protobuf;` 就以 `acme.rules.protobuf` 被 import,与包叫什么无关。 -模块名是作者定义的 API,不镜像包身份 —— 普通库包一直遵循的就是这条规则。 - -2026.8.29.1 之前 host 模块这条路径注册的是裸 `package.name`,于是声明名与包名 -分叉的规则包在 GCC 上能构建、在 Clang 与 MSVC 上失败:GCC 的 BMI 隐式落在 -`gcm.cache` 且按**声明名**索引,而另外两者拿到的是显式的 `=` 映射。 -因此包名不再承担任何 C++ 命名约束,`grpc-rules` 重新是合法包名。 - -**两个规则不得声明同一个模块名。** `import` 寻址的是模块,所以两个这样的包对编译器 -不可区分,而它们的 BMI 与对象文件同名 —— 后者覆盖前者,存活的那个对象被送进链接两次。 -mcpp 拒绝这种情形,并点名两个包与各自的 interface 路径。检查的范围是一次 `build.mcpp` -能看见的那些规则,不是索引级的全局唯一性 —— `path` 依赖与私有 registry 本来就绕得开。 - -**`mcpp.` 前缀保留给由 mcpp 项目维护的规则。** 不在 `mcpp` 命名空间下的包声明该前缀 -的模块名时给出一条同时点名两者的警告,构建继续。之所以是警告:引擎判定不了谁是官方, -`path` 依赖、私有镜像与内部 fork 都合法,而且从这里看都一样。 - -lib 根必须在 `src/.cppm`(或 `[lib] path` 指向的位置);缺失时报 -*"host module 'x': no interface unit at …"*。 - -**一个包可以提供多条规则,由 feature 选择**(mcpp 2026.9.5.3+)。包解析后的 -`[build] sources` 里 —— 含 feature 加入的源文件 —— 每一个模块接口单元都以它自己声明的 -名字编成一个 host 模块,lib 根排在最前。feature 单元可以 import lib 根;除此之外每个 -单元单独编译,因此只 import `std` 与 `mcpp`。只有写在清单里的源文件参与:未声明 -`sources` 的包所推断出的 `src/**` 不被读取,所以此前发布的规则包暴露的仍是它当时暴露 -的那一个模块。 - -```toml -# 集合包的 manifest -[build] -sources = ["src/plugins.cppm"] # export module mcpp.plugins; - -[features] -rules-cuda = { sources = ["rules/cuda.cppm"] } # export module mcpp.rules.cuda; -rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spirv; -``` - -```toml -# 消费者 -[build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } -``` - -**用 `[build-dependencies]` 而不是 `[dependencies]`** —— 规则包正是 §2.6.1 描述的那种 -情形:它的库绝不该到达目标,而它的规则仍然被需要。两条轴是分开的: -`host-module = true` 说的是**要哪一种构建期产物**,而 section 说的是**这个包是否到达 -目标**;规则包在第二条轴上的答案是"否",而 section 就是说这件事的地方。写在 -`[dependencies]` 里同样能工作 —— 这恰恰是为什么这条区分必须被**陈述**,而不能指望由 -一次失败来教会。 - -模块集合就是 feature 集合:feature 未激活的单元不编译,import 它会以未知模块失败。 -`mcpp:plugins` 是 mcpp 项目维护的集合(仓库 `mcpp-community/mcpp-plugins`);其成员 -命名为 `mcpp.rules.`(规则包)与 `mcpp.tools.`(构建期工具)。 - -*仅构建期:* `host-module = true` 的依赖**不会**被编进、也不会被链进本工程的 target, -它所依赖的东西也不会。它只在 `build.mcpp` 期间运行,别处都不出现。(2026.8.5.2 之前 -它还会被当作普通库再编一遍,这正是规则里 `import mcpp;` 失败的原因:在那第二次编译里 -内置模块并不存在。2026.8.29.1 之前被排除的只有规则本身,它自己的 `[dependencies]` -仍会被编译并链进消费者的二进制,而规则却 import 不到它们。) - -#### 依赖另一个规则的规则(mcpp 2026.8.29.1+) - -规则在自己的 `[build-dependencies]` 里声明所需之物,并可以 import 其中标了 -`host-module = true` 的条目: - -```toml -# 写在规则包自己的清单里 -[build-dependencies] -globbing = { path = "../globbing", host-module = true } -``` - -```cpp -// 规则自己的接口 -export module tidyrule; -import std; -import mcpp; -import globbing; -``` - -mcpp 先编译内层规则,同一条命令、同一套 flag,因此 BMI 的一致性仍是结构性事实而不是 -需要事后校验的性质。 - -消费者**不可以** import `globbing`:构建期的 provision 只在 `reexport = true` 的边上 -再跨一跳,而 mcpp 自己执行这条规则,不交给编译器 —— 在 GCC 上那个 import 会成功, -然后在别人的机器上失败。 - -*限制:* 每个 host 模块只有一个接口单元。带实现单元或多个模块的库还不能作为规则的 -构建期依赖。 - -#### `reexport = true` —— 由库替用户拉起整条工具链(2026.8.6.2+) - -上面这些都由**使用工具的人**声明。当知识本来属于库时,这个位置就错了:gRPC -的代码生成需要 protobuf 的 `protoc`,而 gRPC 包的任何使用者都不应该知道这件事。 - -`reexport = true` 把一条边上的构建期提供物 —— 它的 `tools`、它的 -`host-module`、以及该依赖的目录 —— 交给**本包自己的消费者**: - -```toml -# 写在 grpc 包自己的 manifest 里 -[feature-deps.codegen] -"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } -grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"], reexport = true } -grpcgen = { version = "1.83.0", host-module = true, reexport = true } -``` - -于是使用者只写一行,再 import 那个规则: - -```toml -[dependencies] -grpc = { version = "1.83.0", features = ["codegen"] } -``` - -```cpp -// build.mcpp -import mcpp; -import grpcgen; -int main() { return grpcgen::generate_all() ? 0 : 1; } -``` - -- **默认关闭,并且刻意不复用边上的 `visibility`。** `visibility` 本身默认就是 - `"public"`,复用该可见性意味着任意深度的依赖都能静默地向构建程序的工具 - 命名空间里塞东西。「把某样东西交给消费者」是一条供应链主张,必须写下来。 -- **一次声明只走一跳。** 被再导出的提供物到达声明它的那个包的消费者;要继续 - 往上走,下一个包必须自己也写 `reexport`。每个包只决定**它**交出什么。 -- **feature 可以往一条已经声明过的依赖上追加请求。** gRPC 无条件依赖 protobuf, - 而它的 `codegen` feature 往同一条边加 `tools = ["protoc"], reexport = true`。 - `tools` 与 `features` 取并集,`host-module` 与 `reexport` 取或;`version` / - `path` / `git` 不合并 —— feature 仍然无法静默覆盖无条件条目的身份。 -- **传播的是可见性,不是执行。** `dep_bin()` 只返回路径,跑不跑仍由消费者的 - `build.mcpp` 决定;谁构建了这个工具、tool store 怎么做键,都不改变。 -- **裸名由阶梯决定,而不是靠运气。** 一旦两个库都能再导出,它们可能同时提供 - 尾名 `protobuf`。全限定的 `MCPP_DEP___BIN_` 总是发布;裸名 - 依次绑定到 `mcpplibs.`、`compat.`、无命名空间的 ``,最后才是「剩下 - 的唯一候选」——存在争用时 mcpp 会说出来,而不是默默选一个。 - -##### 旧版 mcpp 读到用了这些键的 manifest - -不认识的依赖键会被**记为降级**并忽略(mcpp 2026.8.6.2+),因此一份为更新的 -mcpp 写的包仍然能加载,这个读取器认识的部分照常生效。在那之前它是**整份加载 -失败**且报错误导,这正是「已发布的包永远无法采用新键」的原因——与索引下限确立 -的是同一条性质:**数据不得决定程序是否可用**。 - -因此,一个**依赖** `reexport` 才有那套人机工程的包,仍然需要足够新的客户端; -变化在于该包的其余部分在旧客户端上不再一起失效。 - -##### 按平台裁剪提供物 - -一个包可能只在部分平台声明 `bin` 目标。既然现在是**库**决定请求什么,无条件的 -请求就会把「不支持的平台」变成用户改不掉的硬错。用条件段裁剪: - -```toml -[target.'cfg(not(windows))'.feature-deps.codegen] -"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } -``` - -`[target..feature-deps.]`(2026.8.6.2+)与 `[target.]` 下的 -其余依赖表(`dependencies` / `dev-dependencies` / `build-dependencies`)遵循同 -一套谓词规则,针对**解析后的 target** 求值。**feature 本身在所有平台都注册** -—— 只有它拉进来的东西是条件性的 —— 因此在没有任何谓词匹配的平台上请求它,不是 -「未知 feature」错误。 - -### 2.15 `[resources]` —— 编译进产物的元数据与资产(2026.8.7.1+) - -exe 图标,以及 Windows 在文件「属性」里显示的版本信息,就是 `mcpp.toml` 里的一个路径: - -```toml -[resources] -icon = "assets/app.ico" -``` - -常见场景到此为止。`FILEVERSION`、`ProductName`、`FileDescription`、`CompanyName`、 -`LegalCopyright` 全部从 `[package]` 取默认值,资源脚本由 mcpp 生成。 - -| 键 | 类型 | 含义 | -|---|---|---| -| `icon` | 路径 | 作为应用图标嵌入(资源序号 1) | -| `files` | 路径列表 | 工程自带的 `.rc` 脚本,mcpp 编译并**跟踪**为构建输入 | -| `extra-inputs` | 路径列表 | `.rc` 扫描器看不见的输入(见下) | -| `version-info` | 布尔 | `false` 表示不要生成版本资源 | -| `[resources.version-info]` | 表 | `company`、`product`、`description`、`copyright`、`original-filename`、`internal-name` | - -**只有 PE 目标会*编译*这一节。** 在 Linux/macOS 上它**不适用**:不产资源单元、 -不出诊断、构建逐字节不变。**无需**(也不能)加 `cfg(windows)` 谓词 —— -无条件写一次即可。 - -**声明了却不存在的文件会让构建失败 —— 在每个目标上都是。** 资源和源码一样是 -构建输入;mcpp 不会悄悄产出一个缺了它的二进制。校验刻意**不**按 PE 设门: -路径是否存在是关于工作树的事实,不是关于目标的事实,所以 `icon = "assets/app.ico"` -里的拼写错误由 Linux/macOS 构建(以及对应的 CI job)当场抓住,而不是等 -Windows 那条。不想要图标,把那一行删掉。 - -**版本字段。** `FILEVERSION` 取 `[package].version` 的四段数值,每段必须放得进 -16 位;字符串字段保留版本原文,所以数值字段装不下的形态(`1.0.0-rc1`)在属性 -对话框里照样看得到。 - -#### 自写 `.rc` - -```toml -[resources] -files = ["res/app.rc"] -``` - -写了 `files`,mcpp 就不再生成版本资源 —— 资源 ID 空间由工程自行支配。两者都需要时同时写 -`version-info = true`(注意冲突:序号 1 的 `RT_VERSION` 只能有一个)。 - -想从生成的脚本起步而不是从空文件起步:把它从构建目录里拷出来 -(`target///res/.mcpp.rc`)填进 `files`。结果**字节相同**, -所以从「生成」走到「手写」不会改变产物。 - -> **`VS_VERSION_INFO` 需要 ``。** 手写脚本里如果写 -> `VS_VERSION_INFO VERSIONINFO` 而没有 `#include `,版本资源会被存成 -> **字符串名**而不是序号 1。所有工具依然报告 `Type: VERSIONINFO`,但 -> `GetFileVersionInfo` 查的是序号,于是 PowerShell 的 `FileVersionInfo` 里每个字段 -> 都是空的。要么 include ``,要么直接写 `1 VERSIONINFO`。mcpp 见到这个 -> 形状会警告;它自己生成的脚本用的是字面 `1`。 - -#### 被跟踪的输入 - -mcpp 会读 `.rc`,把引号形式的 `#include` 和资源语句(`ICON`、`RCDATA`、 -`MANIFEST` …)点名的文件都变成构建输入,所以改图标会重链。尖括号形式 -(``)属于工具链,由工具链 fingerprint 覆盖。 - -通过宏间接引用的文件名(`1 ICON APP_ICON`)扫描看不见。mcpp 会**指名**它没能解析 -的东西,并要求显式声明: - -```toml -extra-inputs = ["assets/app.ico"] -``` - -#### 其余一切:`role = "object"` - -不是资源脚本的输入 —— `objcopy` 嵌入的 blob、生成的 `.def`、预编译对象 —— -可以由构建程序声明一个产出接到链接的图节点: - -```cpp -mcpp::action o; -o.id = "blob"; o.role = "object"; -o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o") - .input("blob.bin") - .output("${mcpp.out_dir}/blob.o") - .target("myapp") // 省略:接到每个镜像,含测试二进制 - .submit(); -``` - -见 [07 — build.mcpp](07-build-mcpp.md)。把这类文件写进 `[build].ldflags` 也「能用」, -但 ldflags 是链接命令里的一串字符:没有任何东西跟踪它,改了它得到的是 -`ninja: no work to do`。 - -### 2.16 `[hooks]` —— 项目构建生命周期命令(实验性) - -> **实验性。** Hook 目前**不能**决定一次构建成功与否。所有 Hook 失败都以 -> **warning** 报出,`mcpp build` 保留它自己挣来的结果;`side_effect = true` -> 会被报错拒绝,而不是被采纳。这个键保留在 schema 里,这样今天写下的 manifest -> 在该功能转正时无需改动。另有两条限制是永久的、不是临时的:**只有根项目的 Hook -> 会执行**,而且**只有 `mcpp build` 会执行它们**。 - -Hook 是 `mcpp build` **在一段区间内持有**的命令,事件名就是那段区间: - -```toml -[hooks] -build_start = "echo build started" -build_failed = "notify-send 'build failed'" -build_finished = "notify-send 'build finished'" - -# 可选;以下是默认值。 -timeout_seconds = 10 -enabled = true -side_effect = false # 实验期内 `true` 会被拒绝 -``` - -| 键 | 类型 | 默认值 | 它命名的区间 | -|---|---|---:|---| -| `build_start` | 命令 | — | 项目准备完成后开启,命令退出时闭合 | -| `build_finished` | 命令 | — | 构建成功后开启,命令退出时闭合 | -| `build_failed` | 命令 | — | 构建失败后开启,命令退出时闭合 | -| `during_build` | 命令 | — | 构建开始前开启,构建结束后闭合 | -| `timeout_seconds` | 整数,1–86400 | `10` | 单次运行的时限 | -| `enabled` | 布尔 | `true` | 是否启用本表中的全部命令 | -| `side_effect` | 布尔 | `false` | Hook 失败是否让本次构建失败。**保留键**——实验期内只接受 `false` | - -前三个区间是**自闭合**的——命令退出,区间就结束。"同步"在这里不是一种单独的模式, -它就是自闭合区间的样子。`during_build` 是唯一由别的东西闭合的区间,而那两个只对其中 -一种形状有意义的键,是从这一点推出来的,不是额外规定的例外。 - -命令写成字符串;需要选项时写成表: - -| 表内键 | 适用于 | 含义 | -|---|---|---| -| `cmd` | 所有事件 | 命令本身,必填 | -| `timeout_seconds` | 自闭合事件 | 覆盖本表默认值 | -| `loop` | `during_build` | 命令在区间闭合前退出时重新启动 | - -`loop` 写在自闭合事件上、`timeout_seconds` 写在 `during_build` 上,都是**错误**而不是 -被忽略的键:自闭合区间随命令退出而结束,没有东西可重启;而 `during_build` 已经由构建 -定界。一个被接受却什么都不做的键,读起来就是"这功能坏了"。 - -命令通过宿主 Shell(`/bin/sh` 或 `cmd.exe`)执行,工作目录是**项目根目录**——不是敲 -`mcpp build` 的那个目录,所以 Hook 里的相对路径在哪儿发起构建都指同一处。自闭合命令 -的标准输入、输出和错误沿用普通终端行为。没有配置的事件直接跳过。 - -生命周期为: - -```text -during_build 开启 -build_start - ├─ 构建成功 → during_build 闭合 → build_finished - └─ 构建失败 → during_build 闭合 → build_failed -``` - -`during_build` 在终止 Hook **之前**闭合,因此两条命令不会重叠执行。 - -`build_failed` 与 `build_finished` 互斥,而且两者都只在 `build_start` 已经执行之后 -才可达。项目**准备**阶段就失败的情况——manifest 非法、依赖无法解析、没有可用工具链 -——一个 Hook 都不触发:此时构建尚未开始,而 Hook 程序本身可能正是准备阶段要装的东西。 - -命令无法启动、返回非零或超过时限均视为 Hook 失败。`during_build` 还多一种:开了 `loop` -的命令**起不来**——连续五次在一秒内以非零状态结束——就不再重启,并被报出来。(很快就 -成功结束的命令,正是 `loop` 被要求重复的那件事,不算失败。)以上每一种都以 **warning** -报出,构建保留它自己挣来的结果——`[hooks]` 还在实验期,它没有投票权。Hook 自身失败不会 -再触发另一个 Hook。 - -改变这一点的正是 `side_effect = true`,而今天写它是一个错误: - -```text -error: mcpp.toml: error: [hooks].side_effect = true is not available yet: -[hooks] is experimental and cannot decide whether a build succeeded. … -``` - -是拒绝而不是悄悄降级,因为两种沉默的做法都更糟:采纳它等于让一个实验性功能对每一次 -构建都有否决权;忽略它则让项目以为自己的构建被通知程序把着关,而实际上没有。功能转正 -后,`true` 的含义是"Hook 失败让构建失败"——而构建自身失败时仍保留它自己的退出码,所以 -`mcpp build` 不会把一次编译错误报成通知程序的问题。 - -关于 `during_build` 有两件事值得单独知道: - -- **它的输出被丢弃**,因为它与构建并发写出,否则会插进某条编译诊断的中间。要看它的 - 输出就跑 `mcpp build --verbose`。 -- **停止的单位是进程树,不是进程。** `player & wait` 让播放器成为 mcpp 所启动那条命令 - 的孙子进程,只停掉后者会让音频设备在构建结束后仍被占着。mcpp 把命令放进它自己的 - 进程组(Windows 上是 job object)并停止整组,构建被 Ctrl-C 打断时也一样。 - -作用范围: - -- 只有 `mcpp build` 执行 Hook。`mcpp run`、`mcpp test` 和 - `mcpp build --configure-only` 同样会构建,但有意不执行。 -- Hook 属于**被构建的那个包**。workspace 展开时就是逐个成员:各自的 `[hooks]`、 - 各自的构建、各自的根目录。**虚拟** workspace 根(只有 `[workspace]` 没有 - `[package]`)不构建任何东西,写在那里的 `[hooks]` 永不触发。 -- 依赖的 `[hooks]` **一律跳过**,只有根项目的会执行。mcpp 解析的每一份 manifest 都 - 带着这一节,依赖的也带,而没有任何东西去读它——这正是"装一个包"不会变成"在我下次 - 构建时跑包作者的 Shell 命令"的原因。这是设计的性质,不是一个等着被打开的默认值。 -- 声明了生效的 Hook 就等于让项目放弃空转快路径,因为 `build_start` 规定在准备阶段之后 - 执行。对已经是最新状态的带 Hook 项目,`mcpp build` 的代价是一次准备,而不是毫秒级。 - -`[hooks]` 里、以及某个事件表里不认识的**键**都是 warning(`--strict` 下为错误),所以为更新版 mcpp 写的 -manifest 在这一版仍能加载;不认识的**值**——`cmd` 缺失或不是字符串、`timeout_seconds` 不在 -1–86400 之间、键写给了错误的区间——是 manifest 错误。 - -> **Hook 是代码,而 `mcpp.toml` 是仓库的一部分。** 构建一个刚克隆下来的项目,会以 -> 执行 `mcpp build` 的那个账户的权限,运行它 `[hooks]` 里写的任何东西。这与 -> `build.mcpp`([07 — build.mcpp](07-build-mcpp.md))已经要求的信任是同一份; -> `[hooks]` 扩大的是它的范围,而不是引入了一份新的信任。 - -Hook 程序可以作为普通 xlings 依赖安装。例如,音频通知程序可以把音频内置进自己的 -可执行文件,无需让 mcpp 处理媒体资源: - -```toml -[hooks] -build_finished = "mcpp-hooks-audioplayer niulai-mm" -build_failed = "mcpp-hooks-audioplayer niulai-niulai" -side_effect = false - -[xlings] -deps = ["xim:mcpp-hooks-audioplayer@0.0.1"] -``` - -根据构建成功或失败播放不同提示音。`side_effect = false` 写出来而不是靠默认值:它是 -这份 manifest 自己就想要的值——缺个音频设备不该让构建失败——所以等这个键有了不止 -一个可接受的值之后,它仍然会这么写。 - -## 附录 A. Schema 所有权原则(新字段准入标准) - -> **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。 - -- mcpp 只定义**机制**(features 并集/闭包、capability require/provide/override、 - profile→编译器旗标、platform→triple),键与形状固定;feature 名、能力名、 - 后端名等**领域词汇只出现在值里**,不进 mcpp 代码。 -- **不支持包自定义 toml 键**:键合法性不得依赖"先解析目标包",否则 manifest - 失去静态可解析性(lockfile/LSP/审计的前提)。包的扩展点 = 固定机制内的开放值域。 -- 包级旋钮统一收敛进 features;糖键(如 `backend=`)进入核心语法须满足: - ① 领域中立(跨生态通用模式)② 1:1 脱糖、零新增解析语义。 -- 字段归属总表与定型决策见 - `.agents/docs/2026-06-04-manifest-schema-ownership.md`。 - -## 3. 实战示例 - -### 3.1 简单 Hello World - -```toml -[package] -name = "hello" -version = "0.1.0" -``` - -```cpp -// src/main.cpp -import std; -int main() { std::println("Hello, mcpp!"); } -``` - -```bash -mcpp build && mcpp run -``` - -### 3.2 模块化库 + 测试 - -```toml -[package] -name = "mymath" -version = "1.0.0" - -[targets.mymath] -kind = "lib" - -[dev-dependencies.compat] -gtest = "1.15.2" -``` - -```cpp -// src/mymath.cppm -export module mymath; -export int add(int a, int b) { return a + b; } -``` - -```cpp -// tests/test_add.cpp -#include -import mymath; -TEST(Math, Add) { EXPECT_EQ(add(1, 2), 3); } -``` - -```bash -mcpp build # 编译库 -mcpp test # 编译 + 跑测试 -``` - -### 3.3 依赖其他包的应用 - -```toml -[package] -name = "myapp" -version = "0.1.0" - -[dependencies] -ftxui = "6.1.9" - -[dependencies.mcpplibs] -cmdline = "0.0.2" -llmapi = "0.2.5" -``` - -mcpp 自动: -1. 从 mcpp-index 下载源码 tarball -2. 按 `[build].include_dirs` 传播头文件路径 -3. 传递依赖自动入图(llmapi → tinyhttps → mbedtls 全自动) - -### 3.4 纯 C 库 - -```toml -[package] -name = "myc" -version = "0.1.0" - -[build] -c_standard = "c99" -include_dirs = ["include"] -sources = ["src/**/*.c"] - -[targets.myc] -kind = "lib" -``` - -### 3.5 混合 C / C++23 模块项目 - -```toml -[package] -name = "hybrid" -version = "0.1.0" - -[build] -include_dirs = ["include"] -c_standard = "c11" - -[dependencies] -lua = "5.4.7" # 纯 C 库,mcpp 自动用 C 编译器编译 .c 文件 - -[targets.hybrid] -kind = "bin" -``` - -### 3.6 跨编译静态发布 - -```toml -[package] -name = "mytool" -version = "1.0.0" - -[toolchain] -default = "gcc@16.1.0" - -[target.x86_64-linux-musl] -toolchain = "gcc@16.1.0" -linkage = "static" -``` - -```bash -mcpp build --target x86_64-linux-musl -# → 产出完全静态链接的二进制,可直接 scp 到任意 Linux x86_64 机器运行 -``` - -## 4. 约定与默认值速查 - -| 项目 | 默认值 | 说明 | -|---|---|---| -| 源文件 | `src/**/*.{cppm,cpp,cc,c,S,s,asm}` | 自动递归扫描 | -| 入口 | `src/main.cpp` | 有这个文件就推断为 `bin` 目标 | -| 库根 | `src/.cppm` | 可用 `[lib].path` 覆盖 | -| C++ 标准 | `c++23` | 用 `[package].standard` 配置; 支持 `c++20` / `c++26` / `c++2a` / `c++2c` / `gnu++NN` / `c++latest` / `c++fly`(实验试验场) | -| C 标准 | `c11` | `.c` 文件自动走 C 编译器 | -| 静态 stdlib | `true` | 便携二进制 | -| 头文件 | `include/`(如果存在) | 自动加到 `-I` | -| 测试 | `tests/**/*.cpp` | `mcpp test` 自动发现 | -| 依赖命名空间 | `mcpplibs`(默认) | 裸 selector 只表示该精确 ns | - -### 4.1 旧 `[language]` 兼容层 - -旧配置仍可读取: - -```toml -[language] -standard = "c++26" -``` - -新项目请使用 `[package].standard`。如果两个位置都出现,`[package].standard` 是权威配置。 diff --git a/docs/zh/06-features-and-capabilities.md b/docs/zh/06-features-and-capabilities.md new file mode 100644 index 00000000..7d6cd9d5 --- /dev/null +++ b/docs/zh/06-features-and-capabilities.md @@ -0,0 +1,370 @@ +# 06 —— Feature 与能力 + +**读者:**手上有可选内容的作者 —— 一份额外的源、一个额外的依赖,或者在多个后端 +之间做选择。 + +**本章回答的那一个问题:**一个包怎样提供可选内容,消费者又怎样要它。 + +**不在这里:**一次构建面向哪些设备后端 —— 那看起来像 feature 而不是 feature, +它是 [42 —— 异构硬件构建](42-heterogeneous-builds.md)。在此之前: +[05 —— 依赖与解析](05-dependencies.md)。在此之后:[07 —— 工作空间](07-workspace.md)。 + +Feature 是一个包提供可选内容的方式:一个编译宏、一份额外的源文件、一个额外的 +依赖,或者在多个后端之间做选择。本章是声明与消费 feature 的参考。 + +相关文档:[04 —— mcpp.toml](04-mcpp-toml.md) 是 manifest 其余部分的字段参考; +[`examples/11-features`](../../examples/11-features/) 是一个把三种形态都声明了 +一遍、并且用 dev-dependency 写测试的包;[42 —— 异构硬件构建](42-heterogeneous-builds.md) +是这套机制最大的消费者,因为每条加速器 lane 都是一个 feature。 + +## `[features]` —— Feature(Cargo 风格,可加性) + +```toml +[features] +default = ["base"] # 默认激活集合 +base = [] +docking = ["extra"] # 激活 docking 即隐含激活 extra(传递闭包) +extra = [] +``` + +- 激活来源:包自己的 `default` 集合 ∪ 显式请求(根包经由 + `mcpp build --features a,b`;依赖经由长形式依赖 spec 的 `features = [...]` + 与 `backend = "..."` 糖)。 +- 每个被激活的 feature 在该包编译时得到宏 `-DMCPP_FEATURE_`(名字大写, + 非字母数字变 `_`,例如 `backend-a` → `MCPP_FEATURE_BACKEND_A`)。 +- **严格校验**:目标包声明了 `[features]` 表时,请求一个未声明的 feature 产生 + 警告,在 `--strict` 下是错误。不声明 `[features]` 的包接受任意请求(纯宏用法)。 + +### 表形式 —— 让 feature 贡献的不止是隐含 feature + +`[features]` 的条目除了写成数组,还可写成**表**,从而让该 feature 在隐含 feature +之外,携带包自有的预处理 `defines`、feature 门控的源 glob(`sources`,mcpp +0.0.95+——列出的 glob 离开默认构建,仅当 feature 激活时才编译,与 index 描述符的 +`features..sources` 完全对等;这正是 vendored 大库最高频的形态:*feature = +一组源文件 + 一个 define*)、feature 门控的 per-glob 编译旗标(`flags`,mcpp +0.0.101+),以及 capability 的 `requires` / `provides`(见下文*`provides` / `requires`*): + +```toml +[features] +default = [] +# 数组简写:仅隐含 feature。 +docking = ["extra"] +extra = [] +# 表形式:激活时贡献一个包自有的宏。 +mpl2only = { defines = ["EIGEN_MPL2_ONLY"] } +# 表形式:宏 + 一个隐含 feature。 +fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] } +# 表形式:feature 门控源 + 与其同居的 per-glob 旗标。 +simd = { sources = ["src/simd/**"], flags = [ + { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] } +``` + +- **表形式恰好接受** `implies`、`forward`、`defines`、`sources`、`flags`、 + `requires`、`provides`。其余键会被报成一条 schema 警告并忽略(mcpp 2026.9.1.1+); + `deps` 单独报为「保留」(它是计划中的而不是写错的),并指向 `[feature-deps.]`。在该版本之前,`[features]` + 是唯一一个完全没有 schema 检查的结构化段落 —— 把 `include_dirs` 误写进 feature 里 + 会零诊断地构建成功,而同样的错误写在 `[build]` 里会被报出来。 +- `defines` 为**裸**宏名(不带 `-D`);feature 激活时每个脱糖为 `-D`,加到该包 + 自己的编译上——与 `[targets.*] defines` 完全一致。按约定仅限包**自有**的带命名 + 空间宏:feature **不**注入自由的包级 `cflags`/`ldflags`,否则会破坏加性的 feature + 并集模型。链接旗标来自 provider 依赖(见下文*`provides` / `requires`*),而非 feature。 +- 每个激活的 feature 仍会得到自动的 `-DMCPP_FEATURE_`,`defines` 与之叠加。 +- `flags`(mcpp 0.0.101+)与 `[build].flags`([04 §2.3](04-mcpp-toml.md))共用同一有序 inline-table 数组 + 文法(`glob` 必填,加 `cflags`/`cxxflags`/`asmflags`/`defines`;与 + `[[build.flags]]` 一样也接受 `[[features..flags]]` 拼写)。feature 激活时 + 条目追加在 base `[build].flags` **之后**(feature 按名 + 序),"last flag wins" 使 feature 规则可覆盖更宽的 base 规则;未激活时条目根本 + 不存在(不会有死 glob 告警)。这让 feature 的组内专属旗标与其 `sources` 同居, + 而不必写成 base 规则、在 feature-off 构建里留下必死的 glob。与 `defines` 不同, + feature `flags` 是**私有 per-TU 构建旗标**——永不传播给消费者(与 `[build].flags` + 同契约),因此不破坏加性模型:glob 限定作用面、顺序确定、无跨包效应。 + + +### 作为构建规则的 feature(mcpp 2026.9.7.1+) + +两个键把一个 feature 变成其它包可以使用的构建规则。它们是消费者只写一条依赖边、 +不写构建程序的原因。 + +```toml +[features.rules-spirv] +sources = ["rules/spirv.cppm"] +rule_module = "mcpp.rules.spirv" +device_extensions = [".comp", ".vert", ".frag", ".glsl"] +``` + +`device_extensions` 陈述这条规则编译哪些**设备源**扩展名。激活了该 feature 的消费者 +会把它们分类为设备源 —— 不扫描 import、不产 BMI、由 mcpp 不驱动的编译器编译。这与 +`[build] module_extensions` 是同一个形状:mcpp 知道设备源*是什么*,不知道 `.cu` 是 +CUDA,所以**一门新设备语言不需要引擎改动**。 +[42 — 异构硬件构建](42-heterogeneous-builds.md) 里那句「第六个后端是一个包而不是一次 +引擎改动」由此才成立;`.slang` 已从 mcpp 的内置表中移除,现在正是经由这条路到达的。 + +`rule_module` 给出消费者的构建程序为够到这条规则而 import 的模块,以及它调用的 +`compile()` 所在。它是**声明**的而不是从源码扫描出来的,因为那个程序必须在任何东西 +被编译**之前**写出来,而一次为了决定写什么而去扫描依赖源码的构建会把两者的顺序颠倒。 + +由此得出两件事,而且都不把任何包名放进 mcpp: + +- **`host-module = true` 被推出来。** 一个点名了规则模块的 feature 已经说过那是使用 + 它的唯一方式,所以依赖边不必再说一遍。 +- **没有 `build.mcpp` 的包会得到一个。** mcpp 把这些规则描述的程序写进构建目录并编译 + 它。自带程序的包保留自己的:合成只填补缺席、绝不覆盖;而生成出来的那份就是这个工程 + 本来要手写的那份,所以接管它是一次复制加一次编辑。 + +feature 仍然**按名字**请求: + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.3.0", features = ["rules-spirv"] } +``` + +早先的一版设计从工程源码里出现的扩展名推导这个集合。它被撤销了,因为两个包可能认领 +同一个扩展名 —— 第三方写一条处理 `.cu` 的规则是会发生的事 —— 也因为 manifest 的职责 +是描述这次构建,而派生出来的 feature 集合让文件不再陈述它。 + +两个键必须成对出现。只写其一是一条没有任何东西能据以行动的声明,会在解析期被拒绝, +而不是留到消费者的构建里。 + + +## `provides` / `requires` —— 能力(后端选择) + +**capability(能力)** 是一个共享的抽象名字(如 `blas`)。包可以 *provide*(提供) +一种能力;feature 可以 *require*(需要)一种能力而非点名某个具体包,解析器会从依赖 +图中绑定**恰好一个** provider。这样就能在多个可互换后端(OpenBLAS / MKL / …)中选其 +一,而不必把选择写死进库里。 + +```toml +# provider 包为任何 require 它的依赖方满足某能力。 +[package] +name = "compat.openblas" +version = "0.3.0" +provides = ["blas", "lapack"] +``` + +```toml +# 消费方经由自己的某个 feature 来 require 这个抽象能力。 +[features] +use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } + +# 图中有 >1 个 provider 时,选其一(否则构建报错并列出候选)。 +[capabilities] +blas = "compat.openblas" # 等价于:mcpp build --cap blas=compat.openblas + +[dependencies] +compat.openblas = "0.3.0" # provider 必须是图中真实存在的依赖 +``` + +保留前缀 `mcpp:` 命名本引擎解析的目标侧层,这些名字对照一个闭集校验。 +包级 `requires` 数组承载对称的陈述 —— 某个目标侧层必须解析为什么, +本包才可用。 + +```toml +[package] +name = "acme.llvm-runtime" +version = "0.1.0" +provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"] +requires = ["mcpp:compiler=llvm"] +``` + +作为标准库的包在 `[build]` 下陈述它的 `std` 模块源, +其所需的 flag 在那里与任何其它构建输入一样可条件化。 + +```toml +[build] +std-module = "llvm-generated/std.cppm" +std-compat-module = "llvm-generated/std.compat.cppm" +std-module-flags = ["--no-default-config", "-nostdinc++"] + +[target.'cfg(c-abi = "musl")'.build] +std-module-flags = ["-D_GNU_SOURCE"] +``` + +五个层、约束它们的规则与相应诊断,见 [22 - 目标侧](22-target-side.md)。 + +绑定是**确定性**的: + +| 图中某被需要能力的 provider 数量 | 结果 | +|---|---| +| 恰好一个 | 自动绑定(无需配置) | +| `[capabilities]` pin / `--cap` 指定了一个 | 以 pin 为准 | +| 零个 | **报错**:没有包提供 `` | +| 两个及以上且未 pin | **报错**并列出候选——绝不静默猜测 | + +被绑定 provider 的链接/头文件旗标经由常规依赖机制流到消费方;capability 层是那道 +*选择与校验* 步骤,把"静默选错后端 / 缺后端"变成构建期的显式报错。 + +**绑定选中的是 provider,它不裁剪链接行。** 依赖包的目标文件一律进入消费方的链接, +与它的能力是否被绑定无关。实测:两个包都提供同一能力且都定义 `cap_probe`,未 pin 时 +解析按上表报错;按提示用 `[capabilities]` pin 其中一个之后,构建走到链接器才失败—— + +``` +ld: obj/mcpplibs_pa/src/impl.o: in function `cap_probe': + multiple definition of `cap_probe'; obj/mcpplibs_pb/src/impl.o: first defined here +``` + +这一点对**多个 provider 定义同一批符号**的能力有影响 —— 全程序单例(例如 +`operator new`),或名字集合固定的 C 接口。对这类能力,图中出现两个 provider 是**待修的 +缺陷**而非可 pin 的歧义:pin 会把一个点名两个候选的报错,换成一个点名 mangled 符号的报错。 +可互换的**库**(各 BLAS 实现导出不同的符号集合,按链接选其一)不受此影响。 + +### `exclusive` —— 包声明自己是唯一提供者 + +上一段描述的是一个引擎**看不见**的缺陷:要看出两个 provider 定义了同一批符号, +需要它们的目标文件,而绑定 capability 时那些还不存在;而「一律拒绝重复 provider」 +又会打断同一段里那个合法的 BLAS 用例。 + +所以由包自己声明: + +```toml +[package] +name = "compat.cublas" +provides = ["gpu-blas"] +exclusive = ["gpu-blas"] +``` + +两个都提供 `gpu-blas` 的包,只要其中至少一个声明了独占,就在绑定 capability 时 +被拒绝 —— 在任何东西被编译之前,并点名该能力与双方: + +``` +error: capability 'gpu-blas' is provided by more than one package, and they + declare it EXCLUSIVE. + providers: [compat.cublas, compat.rocblas] + exclusive: [compat.cublas, compat.rocblas] +``` + +该拒绝在 `--format json` 里报 `exclusive-capability`(见第 11 章)。 + +### `version-floor` —— 对机器的要求高于它所有 + +有些关于机器的事实约束着能为它构建什么,而忽略它们时的失败来得很晚: +一个针对比它将遇到的驱动更新的运行时构建出来的程序,**干净地链接**, +在第一次使用时失败,而消息里两侧都没有。 + +包声明它需要什么: + +```toml +[[runtime.requirements]] +kind = "version-floor" +value = "cuda.driver >= 12.0" +``` + +而某个在**安装期**(探测该发生的地方)确立了机器某项事实的包,声明它: + +```toml +[runtime] +provides = ["cuda.driver=12.4"] +``` + +mcpp 在绑定 capability 时比较二者,并在任何东西被编译之前拒绝, +报 `version-floor-unmet`: + +``` +error: `toolkitnew` requires cuda.driver >= 13.0, and this machine has 12.4. + stated by: driverfact +``` + +**没有任何厂商词汇抵达引擎。** 它读到的是一个名字、一个关系和一个版本; +`cuda.driver` 是流过的数据,一个 mcpp 从未听说过的后端比较方式完全相同。 + +**没人回答的下界是沉默的。** 一台从未声明自己有什么的机器,不是「未满足下界」的机器, +而是「没人问过」的机器。把「我们不知道」变成「不行」正是这个机制要避免的失败, +并且有直接判据:`tests/e2e/603_version_floor.sh` 会构建一个下界指向无人提供之物的工程。 + +**它是关于这个包自己的符号的声明**,所以一条指向本包并不提供的能力的条目会被报为 +schema 警告:那里没有可独占的东西。而无人声明独占的能力行为完全不变 —— 两个 BLAS +实现照常共存,既有的「两个或更多、未 pin」报错也仍然只在**有人 require** 该能力时出现。 + +## `[feature-deps.]` —— 由 feature 拉取的依赖 + +在 `[feature-deps.]` 下声明的依赖是**可选的**:仅当该 feature 激活时(根 `--features`, +或某依赖 spec 的 `features = [...]`)才会被解析。`[dependencies]` 中的依赖始终被解析; +可选性由声明的*位置*表达,而非某个标志位。 + +```toml +[features] +use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] } +backend-openblas = { implies = ["use_blas"] } + +# 仅当 `backend-openblas` 激活时才拉取。每个条目都是完整的依赖 spec +#(version/path/git + 其自身的 features)。 +[feature-deps.backend-openblas] +compat.openblas = "0.3" +``` + +**写 `"^0.3.0"`,而不是 `"0.3.x"` 或 `"0.3"`。** 以索引中确定存在的包作对照, +判据取**构建成功**: + +| 写法 | 结果 | +|---|---| +| `cmdline = "0.0.1"` | 构建通过 | +| `cmdline = "^0.0.1"` | 构建通过 | +| `cmdline = "0.0"` | 解析通过,随后 `install path missing after fetch` | +| `cmdline = "0.0.x"` | `E_NOT_FOUND`,点名的是包 —— 而该包存在 | + +这三种结果值得分开,因为两个更弱的判据各自会放行一种不可用的写法: +「没有 `E_NOT_FOUND`」放行两段前缀,「解析通过」同样放行它。**只有对着真实索引构建 +一次**才能定论。 + +这一点在此处比在 `[dependencies]` 中更要紧:**实现取不回来的 feature 等于不存在的 +feature**,而开发期使用 **path** 依赖的工程根本不查索引 —— 该失败只在发布之后才出现, +而且是出现在别人身上。 + +该机制与能力(上文*`provides` / `requires`*)组合:单个 `backend-openblas` feature 既**拉取** provider +(`compat.openblas`,其 `provides = ["blas"]`),又**开启**消费方开关 +(`implies = ["use_blas"]`,其 `requires = ["blas"]`)。当图中只有一个 provider 时, +能力自动绑定——消费方只需写 `features = ["backend-openblas"]`。 + +在索引包的 Lua 描述符中,等价写法为内联形式: + +```lua +features = { + use_blas = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } }, + ["backend-openblas"] = { + implies = { "use_blas" }, + deps = { ["compat.openblas"] = "0.3.x" }, + }, +} +``` + +### 保持可替换的默认实现 + +同样这三件东西,也覆盖"库希望**提供**一份实现但不**强加**一份"的情形 —— 全程序单例, +例如 `operator new`、日志 sink、panic handler: + +```toml +[features] +default = [] +# 消费方开关:"我用到了本库中需要分配器的那部分"。 +alloc = { requires = ["freestanding-allocator"] } +# 内置默认:激活它就够了。 +alloc-kal = { implies = ["alloc"] } + +# 仅在 `alloc-kal` 激活时解析,因此库本体不携带对该实现的依赖。 +[feature-deps.alloc-kal] +std-freestanding-alloc-kal = "0.1.x" +``` + +三种用法各一行: + +| 消费方需要 | 清单中的写法 | +|---|---| +| 不用会分配的那部分 | `std-freestanding = "0.2.0"` —— 分配器不进图 | +| 默认实现 | `features = ["alloc-kal"]` —— 实现随之进图,**无需知道其包名** | +| 自己的或第三方的 | `features = ["alloc"]` 加一个 `provides = ["freestanding-allocator"]` 的包 | + +有两条性质使该形状优于无条件随包提供实现。随包提供实现的库替程序做了本属程序的决定, +而且**撤销不掉**:feature 是**加性**的,消费方没有把某个默认**关掉**的手段。以及,由于 +依赖包的目标文件无条件参与链接(上文*`provides` / `requires`*),随包的默认加上程序自备的那份是**重复定义** +而非替换 —— 让 C++ 标准库能提供可替换 `operator new` 的那套归档语义,对包依赖并不适用。 +把实现放在开关之后,意味着两者**从不共存**。 + +## 当前边界 + +**默认 feature 在 manifest 里关掉,不在命令行上关掉。** 没有 `--no-default-features`。 +`mcpp build --features metrics` 激活的是 `default ∪ {metrics}`;要在不带 `default` +某个成员的情况下构建,得改 `[features] default`。实测于 2026.9.8.1。 + +**依赖不能以加速器为条件。** `accelerator` 是从依赖图解析出来的,因此由它选择的 +依赖会决定它自己在问的那个答案。mcpp 会报告该谓词并忽略它。包要么无条件,要么以 +平台为条件;由加速器选择的是 `[build] sources`。 + diff --git a/docs/zh/06-workspace.md b/docs/zh/07-workspace.md similarity index 96% rename from docs/zh/06-workspace.md rename to docs/zh/07-workspace.md index baae2cff..45f4652f 100644 --- a/docs/zh/06-workspace.md +++ b/docs/zh/07-workspace.md @@ -1,4 +1,12 @@ -# 工作空间 (Workspace) +# 07 —— 工作空间 + +**读者:**仓库里不止一个包的作者。 + +**本章回答的那一个问题:**多个包怎样成为一次构建,以及一个成员与其余成员共享什么。 + +**不在这里:**把这些包发布出去,那是 [11 —— 发布一个库](11-publishing-a-library.md)。 +在此之前:[06 —— Feature 与能力](06-features-and-capabilities.md)。在此之后: +[08 —— 测试](08-testing.md)。 工作空间允许在同一个仓库中组织和管理多个相关的 mcpp 包(库或应用程序)。各成员包共享统一的依赖版本配置和工具链设置,同时保持独立的 `mcpp.toml` 工程文件。 @@ -275,7 +283,7 @@ mcpp run -p server -- --port 8080 `mcpp test --workspace` 逐成员独立汇报、遇失败继续,只要有任一成员失败即非零退出—— 非常适合作为「一个测试众多库的工作空间」的单条、无 shell 的 CI 步骤。 -#### 扇出汇报什么 +#### 扇出的汇报内容 ``` Workspace testing member 'libs/core' (3/97) @@ -343,3 +351,4 @@ myproject/ ## 8. 完整示例 参见 [`examples/04-workspace/`](../../examples/04-workspace/),包含一个三成员工作空间的完整可运行示例。 + diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md new file mode 100644 index 00000000..8d44470a --- /dev/null +++ b/docs/zh/08-testing.md @@ -0,0 +1,115 @@ +# 08 —— 测试 + +**读者:**任何有代码需要持续可用的人。 + +**本章回答的那一个问题:**测试怎么写、怎么跑,mcpp 认为什么是一个测试,以及 +在本机跑不了的东西怎么测。 + +**不在这里:**runner 怎么抵达一台设备 —— 那是 +[41 —— 抵达一台设备](41-devices.md);以及机器可读流的 schema,那是 +[50 —— 机器可读输出](50-machine-output.md)。本章只说明哪个旗标产生它,到此为止。 + +在此之前:[05 —— 依赖与解析](05-dependencies.md) 覆盖 `[dev-dependencies]`, +那是测试如何取到产物取不到的包。在此之后: +[09 —— 按场景选命令](09-commands-by-scenario.md) 是其余一切的查阅入口。 + +## 测试的定义 + +每一个 `tests/**/*.cpp` 都是一个测试:mcpp 把每个文件编译成它自己的程序并运行它。 +测试通过的判据是它的程序以 0 退出。 + +``` +myproject/ + mcpp.toml + src/… + tests/ + test_parse.cpp 一个程序 + unit/test_span.cpp 另一个 +``` + +没有框架,也不需要注册。测试**可以**用一个框架 —— `[dev-dependencies]` 是它取到 +框架的方式 —— 但 mcpp 持有的契约是退出码,这也是为什么为别的框架写的测试不需要 +适配层。 + +`mcpp new` 会生成 `tests/test_smoke.cpp`,让工程一开始就有这个目录。 + +## 运行它们 + +```bash +mcpp test # 构建并运行每个测试 +mcpp test parse # 只运行名字匹配的那些 +mcpp test --list # 列出会跑哪些,不构建也不运行 +mcpp test -- --verbose # `--` 之后的一切传给每个测试程序 +``` + +测试的构建轴与 `mcpp build` 相同,因此测试跑在它要检查的那个配置上,而不是默认 +配置上: + +| 旗标 | 选中的集合 | +|---|---| +| `--profile ` | `dev`(默认)、`release`、`dist`,或 manifest 声明的某个 `[profile.*]` | +| `--features ` | 这次测试构建的 feature 集合 | +| `--target ` | 宿主以外的目标 | +| `--accel ` / `--no-accel` | 本次构建面向的设备后端 | +| `--cap ` | 钉住某个能力的 provider | + +`--timeout ` 杀掉仍在运行的测试(默认 300;`0` 关闭),`--build-timeout ` +限制编译。一个挂住的测试被报为**以它自己的名字失败**,而不是一个停下来的任务。 + +## 取到产物取不到的包的测试 + +```toml +[dev-dependencies] +counters = { path = "../counters" } +``` + +`[dev-dependencies]` 的条目只为测试构建解析,别处一概不用:它不在产物里,包的 +消费者也永远看不见它。这就是「测试的依赖」与「包自己的依赖」之间的区别,也是 +测试框架不会变成一个库所发布内容的一部分的原因。 + +[`examples/11-features`](../../examples/11-features/) 声明了一个并使用它。 + +## 在本机跑不了的目标上测试 + +面向交叉目标或裸机板子的测试,会被编译到那个目标,并经由一个 **runner** 执行 —— +runner 是板级支持包提供的一串 argv,mcpp 把测试二进制附加在其后执行它。 + +```bash +mcpp test --target thumbv7em-none-eabihf # 为板子构建,经它的 runner 运行 +mcpp test --no-runner # 忽略 runner,直接执行 +``` + +测试本身一个字都不用改。同样的 `tests/**/*.cpp` 为设备编译,判据仍然是退出码 —— +这正是裸机 runner 被选成「能产生 semihosting 退出码或 QEMU 退出码」的原因。 + +`--no-runner` 是给「本机就能原生执行这些二进制、不该为模拟器付代价」的宿主准备的。 + +runner 本身、具名 runner,以及一块板子声明什么,见 +[41 —— 抵达一台设备](41-devices.md)。 + +## 不能彼此并排运行的测试 + +`mcpp test` 在一个工作池上运行测试程序。一块板子接一个探针、一块 GPU、一个串口, +或者一份单座许可证,同一时刻只容一个使用者;两个 worker 同时去拿的结果是交错, +而不是干净地失败。 + +拥有该资源的那个包**自己声明**这一点,`mcpp test` 随后把这些测试串行化。工程方 +永远不需要记得加 `-j1`。 + +## 报告给程序 + +```bash +mcpp test --message-format json +``` + +每个测试一条 NDJSON 记录,供 CI 任务或编辑器消费。schema 及其版本见 +[50 —— 机器可读输出](50-machine-output.md);属于本章的只有「这个旗标存在」以及 +「人类可读格式是默认」。 + +## 当前边界 + +- 一个测试是一个 `.cpp` 产出一个程序。mcpp 不发现文件内部的用例,因此框架的 + 逐用例选择发生在程序内部,经由 `--` 之后的参数。 +- `--build-timeout` 只在 POSIX 上有效。 +- `--workspace-timeout` 限制 `--workspace` 的扇出并报告跑到了哪些;它不把超时 + 归因到某个成员。 diff --git a/docs/zh/09-commands-by-scenario.md b/docs/zh/09-commands-by-scenario.md new file mode 100644 index 00000000..0de5ae28 --- /dev/null +++ b/docs/zh/09-commands-by-scenario.md @@ -0,0 +1,359 @@ +# 09 —— 按场景选命令 + +**读者:**已经认识那些名词、现在想找动词的人。 + +**本章回答的那一个问题:**手上这件事该用哪个命令 —— 回收磁盘、解释一次解析、 +校验一个描述符、诊断环境。 + +**不在这里:**每个命令的含义细节。一个场景点名命令,并链接到拥有它的那一章。 +在此之前:[08 —— 测试](08-testing.md)。 + +命令清单是 `mcpp --help`,每个子命令还有自己的 `--help`。本章回答的是另一个问题: +某个情形已经发生时该用哪条命令 —— 构建目录一直变大、解析结果出乎意料、描述符即将 +发布、索引可能陈旧。这里收的都是名字本身没有说出它所属场景的命令。 + +相关文档:[01 — 快速开始](01-getting-started.md)(日常构建与测试循环)、 +[20 — 工具链管理](20-toolchains.md)、 +[11 — 发布库到 mcpp-index](11-publishing-a-library.md)、 +[50 — 机器可读输出](50-machine-output.md)。 + +下面每段输出都由本章所在版本的 mcpp 实际产生。 + +## 回收磁盘而不触发重编 + +有两个存储会增长,增长的原因不同,各由一条命令清空。把两者弄混的代价是一次全量重编。 + +| 存储 | 作用域 | 增长时机 | 清空方式 | +|---|---|---|---| +| `target/<三元组>/<指纹>/` | 单个工程 | 配置指纹变化,开出新目录 | `mcpp clean`、`mcpp clean --stale` | +| 构建缓存(`mcpp cache dir`) | 整台机器 | 任何工程编译依赖、`std` 模块,或构建一个 host 工具 | `mcpp cache gc`、`mcpp cache prune`、`mcpp cache clean` | + +`mcpp clean` 整个删掉 `target/`,下次构建重编一切。`mcpp clean --stale` 只删已无构建 +记录使用的指纹目录,在用的配置保留: + +``` +$ mcpp clean --stale --dry-run +would remove target/x86_64-linux-gnu/0123456789abcdef (0.0 B) +Would remove 1 directory (0.0 B) +``` + +「在用」指被 `target/.build_cache` 记录 —— 它由 `mcpp build` 写入,由快路径读取。 +这个定义带来三个后果: + +- 没有记录并不足以让一个目录被删。`mcpp test` 走的构建路径不写记录,`--no-cache` + 构建同样不写。未被记录但在 `--older-than`(默认一天)之内写过的目录保留;更旧的 + 会被删,而判断错误的代价是重编一个此后无人碰过的配置。 +- 完全没有记录时,命令拒绝执行而不是猜测。跑一次 `mcpp build` 即可确立什么是当前的。 +- `target/` 下不是指纹目录的东西 —— 例如 `mcpp pack` 的 `dist/` —— 从不被访问。 + +`--dry-run` 只列出,不删除。`--stale`、`--dry-run`、`--older-than` 三者任一都选中这一 +档:`mcpp clean --older-than 3d` 是一次有范围的请求,不会被读成整删。`--older-than 0` +不保留任何未记录目录;负的时长被拒绝。 + +构建缓存是全机共享的,所以工程级命令不得清空它 —— `--stale` 与 `--bmi-cache` 不能同时 +给出。`mcpp cache list` 列出占用。行没有排序,而 `0.0 B (incomplete)` 那样的行,是被 +中断的构建留下的条目: + +``` +$ mcpp cache list +key kind size last used package +8a150ad49d666f94 std 29.6 MiB 6d ago std gcc@16.1.0 c++23 libstdc++ +9234eed9ef786c13 std 0.0 B 2d ago std (incomplete) +``` + +`mcpp cache gc` 要求给出 `--max-size`、`--older-than` 或两者,并且只驱逐包条目。一份 +`std` BMI 被机器上每个工程共享,实现以「重建它是用大量时间换少量磁盘」为由把它排除在 +按体积驱逐之外。`mcpp cache clean --std` 仍是显式移除它的做法。 + +## 一个包已发布的版本 + +`mcpp search` 按子串匹配,并在每个命中行后附上该包发布的版本 —— 跨描述符的 per-OS 表 +合并,按 semver 降序: + +``` +$ mcpp search imgui + compat:imgui Dear ImGui immediate-mode GUI library core sources (1.92.8, 1.92.8-docking) + mcpplibs:imgui C++23 module package for Dear ImGui core and GLFW/OpenGL3 backends (0.0.6, 0.0.5, 0.0.4, ...) +``` + +末尾的 `, ...` 表示被截断:默认显示三个,没有这个标记就说明列表是完整的。 +`--all-versions` 打印全部。描述符读不到的包按两列输出 —— 版本列表是尽力而为的展示, +不会让 search 失败。 + +`mcpp add` 在名字解析不到时携带同样的信息。建议里给出该写的命名空间,以及它背后的版本: + +``` + a package with this name exists under another namespace: + compat.eui-neo (0.5.6, 0.5.5, 0.5.3) +``` + +这次扫描只在查找已经失败之后进行,结果只进入错误文本与 search 输出。裸名不会因此跨 +命名空间解析。 + +## 解释一次解析 + +`mcpp why` 报告一次构建会解析出什么,并且不构建任何东西: + +``` +$ mcpp why toolchain +toolchain: gcc 16.1.0 (x86_64-linux-gnu) + abi(libc)=glibc cxxstdlib=libstdc++ arch=x86_64 os=linux triple=x86_64-linux-gnu + reason: [toolchain] in mcpp.toml if set, else platform-native default +``` + +话题是 `toolchain`、`runtime`、`deps` 或 `runners`,不给话题时四者全报。`--target` 与 +`--toolchain` 把报告变成对当前目录并不使用的那一对的查询,目标矩阵正是这样逐格提问的。 + +诊断里的错误码可以用 `mcpp self explain` 展开: + +``` +$ mcpp self explain E0006 +E0006: index requires a newer mcpp + +The package index declares (index.toml [index].min_mcpp) that its +descriptors need a newer mcpp than this binary — parsing them would +silently misbehave, so resolution stops instead. Upgrade mcpp: +``` + +## 索引新鲜度与离线构建 + +`mcpp index status` 在不碰网络的前提下回答本地索引副本是否当前: + +``` +$ mcpp index status + index state refreshed revision path + xim fresh 28s ago 1f4b39d /home/speak/.mcpp/registry/data/xim-pkgindex + mcpplibs fresh 28s ago d4b36d7 /home/speak/.mcpp/registry/data/mcpplibs +``` + +`mcpp index update` 刷新它们。一个刚发布几分钟、刷新后仍然找不到的包,是传播问题而不是 +名字问题 —— 索引以 artifact 而非 git clone 的形式到达客户端。 + +`--offline`(或 `MCPP_OFFLINE=1`)在单次调用中禁止网络,宁可失败也不拉取。`--locked` 在 +解析结果与 `mcpp.lock` 不一致时失败而不是改写它,这正是 CI 作业需要的形状。 +`mcpp index pin ` 把自定义索引的某个 commit 记进 `mcpp.toml`; +`mcpp index unpin` 移除它。 + +## 发布前校验描述符 + +`mcpp xpkg parse` 用解析器自己的文法读描述符,所以它报告的就是解析时会看到的: + +``` +$ mcpp xpkg parse mcpp.plugins.lua +package mcpp.plugins (namespace 'mcpp') +versions linux 0.1.1, 0.1.0, latest +versions macosx 0.1.1, 0.1.0, latest +versions windows 0.1.1, 0.1.0, latest +form A — no mcpp segment (build info from the source's mcpp.toml) +parse OK +``` + +per-OS 列表分开打印是有意的:一个版本只加进了某一个平台表而在其余表里被遗漏,在缺它的 +平台上读起来就是「找不到」,而文件里明明含有这个版本字符串。`--json` 以同样的事实供脚本 +使用: + +``` +$ mcpp xpkg parse mcpp.plugins.lua --json +{"namespace":"mcpp","name":"plugins","versions":{"linux":["0.1.1","0.1.0","latest"],"macosx":["0.1.1","0.1.0","latest"],"windows":["0.1.1","0.1.0","latest"]},"form":"A"} +``` + +`mcpp emit xpkg` 生成要提交的条目。完整路径见 +[11 — 发布库到 mcpp-index](11-publishing-a-library.md)。 + +## 环境诊断 + +`mcpp self doctor` 检查工具链、`std` 模块、registry、缓存健康与最近一次运行期闭包判定, +并报告它查到了什么,而不只报告失败的部分: + +``` +$ mcpp self doctor + Checking toolchain + ok gcc 13.3.0 (x86_64-linux-gnu) at /usr/bin/g++ + Checking cache health + ok build cache size = 2.5 GiB +warning: pre-v1 cache at '/home/speak/.mcpp/bmi' occupies 167.5 MiB and is no longer used — `mcpp cache clean --legacy` reclaims it +``` + +`mcpp self env` 打印路径与已解析的工具链,含 `--format json`。 +`mcpp self config --mirror CN|GLOBAL` 选择下载镜像;mcpp 与 xlings 各自持有这个设置, +为其中一个选定不会为另一个选定。 + +## `[hooks]` —— 项目构建生命周期命令(实验性) + +> **实验性。** Hook 目前**不能**决定一次构建成功与否。所有 Hook 失败都以 +> **warning** 报出,`mcpp build` 保留它自己挣来的结果;`side_effect = true` +> 会被报错拒绝,而不是被采纳。这个键保留在 schema 里,这样今天写下的 manifest +> 在该功能转正时无需改动。另有两条限制是永久的、不是临时的:**只有根项目的 Hook +> 会执行**,而且**只有 `mcpp build` 会执行它们**。 + +Hook 是 `mcpp build` **在一段区间内持有**的命令,事件名就是那段区间: + +```toml +[hooks] +build_start = "echo build started" +build_failed = "notify-send 'build failed'" +build_finished = "notify-send 'build finished'" + +# 可选;以下是默认值。 +timeout_seconds = 10 +enabled = true +side_effect = false # 实验期内 `true` 会被拒绝 +``` + +| 键 | 类型 | 默认值 | 它命名的区间 | +|---|---|---:|---| +| `build_start` | 命令 | — | 项目准备完成后开启,命令退出时闭合 | +| `build_finished` | 命令 | — | 构建成功后开启,命令退出时闭合 | +| `build_failed` | 命令 | — | 构建失败后开启,命令退出时闭合 | +| `during_build` | 命令 | — | 构建开始前开启,构建结束后闭合 | +| `timeout_seconds` | 整数,1–86400 | `10` | 单次运行的时限 | +| `enabled` | 布尔 | `true` | 是否启用本表中的全部命令 | +| `side_effect` | 布尔 | `false` | Hook 失败是否让本次构建失败。**保留键**——实验期内只接受 `false` | + +前三个区间是**自闭合**的——命令退出,区间就结束。"同步"在这里不是一种单独的模式, +它就是自闭合区间的样子。`during_build` 是唯一由别的东西闭合的区间,而那两个只对其中 +一种形状有意义的键,是从这一点推出来的,不是额外规定的例外。 + +命令写成字符串;需要选项时写成表: + +| 表内键 | 适用于 | 含义 | +|---|---|---| +| `cmd` | 所有事件 | 命令本身,必填 | +| `timeout_seconds` | 自闭合事件 | 覆盖本表默认值 | +| `loop` | `during_build` | 命令在区间闭合前退出时重新启动 | + +`loop` 写在自闭合事件上、`timeout_seconds` 写在 `during_build` 上,都是**错误**而不是 +被忽略的键:自闭合区间随命令退出而结束,没有东西可重启;而 `during_build` 已经由构建 +定界。一个被接受却什么都不做的键,读起来就是"这功能坏了"。 + +命令通过宿主 Shell(`/bin/sh` 或 `cmd.exe`)执行,工作目录是**项目根目录**——不是敲 +`mcpp build` 的那个目录,所以 Hook 里的相对路径在哪儿发起构建都指同一处。自闭合命令 +的标准输入、输出和错误沿用普通终端行为。没有配置的事件直接跳过。 + +生命周期为: + +```text +during_build 开启 +build_start + ├─ 构建成功 → during_build 闭合 → build_finished + └─ 构建失败 → during_build 闭合 → build_failed +``` + +`during_build` 在终止 Hook **之前**闭合,因此两条命令不会重叠执行。 + +`build_failed` 与 `build_finished` 互斥,而且两者都只在 `build_start` 已经执行之后 +才可达。项目**准备**阶段就失败的情况——manifest 非法、依赖无法解析、没有可用工具链 +——一个 Hook 都不触发:此时构建尚未开始,而 Hook 程序本身可能正是准备阶段要装的东西。 + +命令无法启动、返回非零或超过时限均视为 Hook 失败。`during_build` 还多一种:开了 `loop` +的命令**起不来**——连续五次在一秒内以非零状态结束——就不再重启,并被报出来。(很快就 +成功结束的命令,正是 `loop` 被要求重复的那件事,不算失败。)以上每一种都以 **warning** +报出,构建保留它自己挣来的结果——`[hooks]` 还在实验期,它没有投票权。Hook 自身失败不会 +再触发另一个 Hook。 + +改变这一点的正是 `side_effect = true`,而今天写它是一个错误: + +```text +error: mcpp.toml: error: [hooks].side_effect = true is not available yet: +[hooks] is experimental and cannot decide whether a build succeeded. … +``` + +是拒绝而不是悄悄降级,因为两种沉默的做法都更糟:采纳它等于让一个实验性功能对每一次 +构建都有否决权;忽略它则让项目以为自己的构建被通知程序把着关,而实际上没有。功能转正 +后,`true` 的含义是"Hook 失败让构建失败"——而构建自身失败时仍保留它自己的退出码,所以 +`mcpp build` 不会把一次编译错误报成通知程序的问题。 + +关于 `during_build` 有两件事值得单独知道: + +- **它的输出被丢弃**,因为它与构建并发写出,否则会插进某条编译诊断的中间。要看它的 + 输出就跑 `mcpp build --verbose`。 +- **停止的单位是进程树,不是进程。** `player & wait` 让播放器成为 mcpp 所启动那条命令 + 的孙子进程,只停掉后者会让音频设备在构建结束后仍被占着。mcpp 把命令放进它自己的 + 进程组(Windows 上是 job object)并停止整组,构建被 Ctrl-C 打断时也一样。 + +作用范围: + +- 只有 `mcpp build` 执行 Hook。`mcpp run`、`mcpp test` 和 + `mcpp build --configure-only` 同样会构建,但有意不执行。 +- Hook 属于**被构建的那个包**。workspace 展开时就是逐个成员:各自的 `[hooks]`、 + 各自的构建、各自的根目录。**虚拟** workspace 根(只有 `[workspace]` 没有 + `[package]`)不构建任何东西,写在那里的 `[hooks]` 永不触发。 +- 依赖的 `[hooks]` **一律跳过**,只有根项目的会执行。mcpp 解析的每一份 manifest 都 + 带着这一节,依赖的也带,而没有任何东西去读它——这正是"装一个包"不会变成"在我下次 + 构建时跑包作者的 Shell 命令"的原因。这是设计的性质,不是一个等着被打开的默认值。 +- 声明了生效的 Hook 就等于让项目放弃空转快路径,因为 `build_start` 规定在准备阶段之后 + 执行。对已经是最新状态的带 Hook 项目,`mcpp build` 的代价是一次准备,而不是毫秒级。 + +`[hooks]` 里、以及某个事件表里不认识的**键**都是 warning(`--strict` 下为错误),所以为更新版 mcpp 写的 +manifest 在这一版仍能加载;不认识的**值**——`cmd` 缺失或不是字符串、`timeout_seconds` 不在 +1–86400 之间、键写给了错误的区间——是 manifest 错误。 + +> **Hook 是代码,而 `mcpp.toml` 是仓库的一部分。** 构建一个刚克隆下来的项目,会以 +> 执行 `mcpp build` 的那个账户的权限,运行它 `[hooks]` 里写的任何东西。这与 +> `build.mcpp`([30 — build.mcpp](30-build-mcpp.md))已经要求的信任是同一份; +> `[hooks]` 扩大的是它的范围,而不是引入了一份新的信任。 + +Hook 程序可以作为普通 xlings 依赖安装。例如,音频通知程序可以把音频内置进自己的 +可执行文件,无需让 mcpp 处理媒体资源: + +```toml +[hooks] +build_finished = "mcpp-hooks-audioplayer niulai-mm" +build_failed = "mcpp-hooks-audioplayer niulai-niulai" +side_effect = false + +[xlings] +deps = ["xim:mcpp-hooks-audioplayer@0.0.1"] +``` + +根据构建成功或失败播放不同提示音。`side_effect = false` 写出来而不是靠默认值:它是 +这份 manifest 自己就想要的值——缺个音频设备不该让构建失败——所以等这个键有了不止 +一个可接受的值之后,它仍然会这么写。 + +## 失败所属的阶段 + +一次构建要跨过若干阶段,而消息会点名失败的那一段。先读这一点,可以省掉打开错误 +章节的功夫。 + +| 消息里出现 | 阶段 | 参考章节 | +|---|---|---| +| 包名、版本,或「没有候选」 | 解析 | [05](05-dependencies.md)、[11](11-publishing-a-library.md) | +| 下载、载荷,或版本下界 | 供给 | [20](20-toolchains.md)、[23](23-the-project-environment.md) | +| 三元组,或「不支持的目标」 | 目标 | [21](21-the-target-triple.md) | +| 某个模块读不到或没有人提供它 | 模块图 | [30](30-build-mcpp.md) | +| 工程自己某个文件内部的编译或链接错误 | 一段都不是 | 编译器自己的消息 | + +最后一行是有用的那一行:当错误是关于代码本身时,mcpp 的任何一段都没有参与,它的 +文档不会有帮助。 + +## 当前边界 + +- `mcpp why --format json` 只对 `toolchain` 话题有定义。其余话题报 + `'' has no machine-readable shape yet` 并以非零退出。 +- `mcpp search` 按子串匹配;没有字段选择器,也没有把搜索限定到单个命名空间的方式。 +- `mcpp clean --stale` 读 `target/.build_cache`,它保存的近期条目数量有上限。一个工程 + 如果构建过的 (目标, profile) 组合多于这个上限,最旧的条目会被挤掉;条目被挤掉的目录 + 随后按未记录处理 —— 在 `--older-than` 之内保留,超出则删除。 +- `mcpp cache gc --older-than 0` 以 `bad --older-than value '0' + (expected {s,m,h,d})` 被拒绝,而 `mcpp clean --stale --older-than 0` 接受。 + 两个选项共用一个 parser,但这一种取值上不一致。 + +`mcpp emit xpkg` 写出一个 `mcpp xpkg parse` 不认识的键。对一个自带 `mcpp.toml` +的包,产出的 `mcpp` 段以 `manifest = "mcpp.toml"` 结尾,而描述符解析器把它报为 +未知键: + +``` +error: unknown mcpp-segment key 'manifest' — silently ignored at build time + by this mcpp version +error: synthesised manifest missing sources (mcpp segment must declare + `sources = { ... }`) +``` + +第二个错误由第一个导出:键被忽略,于是没有从它点名的那份 manifest 推导出任何 +源。手工补上 `sources = { … }` 只消掉第二个,消不掉第一个,`mcpp xpkg parse` +仍然以 1 退出。 + +`mcpp-index` 里没有任何描述符使用那个键 —— 218 个里 0 个。自带 `mcpp.toml` 的包 +**整个省略 `mcpp` 字段**,由 mcpp 在版本目录下查找那份 manifest。实测于 +2026.9.8.1。 + diff --git a/docs/zh/02-pack-and-release.md b/docs/zh/10-pack-and-release.md similarity index 95% rename from docs/zh/02-pack-and-release.md rename to docs/zh/10-pack-and-release.md index 1993679d..62e5d6c1 100644 --- a/docs/zh/02-pack-and-release.md +++ b/docs/zh/10-pack-and-release.md @@ -1,4 +1,13 @@ -# 02 — 发布打包 +# 10 —— 发布打包 + +**读者:**要把一个程序交付到没有 mcpp 的机器上的人。 + +**本章回答的那一个问题:**怎样把一次构建变成另一台机器能运行的东西,以及每种打包 +模式各自携带什么。 + +**不在这里:**交付一个供其它包构建时使用的**库**,那是 +[12 —— 分发预编译库](12-binary-distribution.md)。在此之后: +[11 —— 发布一个库到 mcpp-index](11-publishing-a-library.md)。 > 默认的动态链接 `mcpp build` 产物会把 loader 与 RUNPATH 指向构建沙盒。它是 > 开发产物,不是交付物。有三条路把它变成交付物 —— **没有一条使用宿主的 C 库**。 @@ -9,7 +18,7 @@ mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决于底下是哪个发行版; 如果最后一步又伸手去拿宿主的 libc,前面这件事就白做了。 -| | 方式 | 命令 | C 运行时来自哪里 | 何时选它 | +| | 方式 | 命令 | C 运行时的来源 | 适用条件 | |---|---|---|---|---| | **A** | 走生态 | `mcpp emit xpkg` → `xlings install ` | 目标机自己的 xlings 载荷 | 目标机装了 xlings | | **B** | 静态单文件 | `mcpp build --target x86_64-linux-musl` | 不来自任何地方 —— 已链进去 | 想要一个无任何运行时依赖的单文件 | @@ -55,7 +64,7 @@ mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决 「自包含」有一个下限。有些库只能来自目标机器:图形驱动的用户态部分与正在运行的 内核模块版本绑定,而对专有栈而言,再分发是不被允许的。这类依赖应声明为运行期能力 -需求(`docs/zh/05-mcpp-toml.md` §2.11),模式表随之多出一列: +需求(`docs/zh/04-mcpp-toml.md` §2.11),模式表随之多出一列: | Mode | 需要宿主提供能力的程序 | |---|---| @@ -109,7 +118,7 @@ mcpp pack --debug-symbols dbg/ # 把分离出的 *.debug 写到 dbg/ 完整选项参见 `mcpp pack --help`。 -### 打包产物用什么构建,里面带什么走 +### 打包产物的构建输入与随包内容 与 `mcpp build` 有两点不同,都因为「这个产物要离开本机」: @@ -129,7 +138,7 @@ profile 的工程仍然拿到它声明的那个,`mcpp pack` 也不会产出一 DWARF 带着发布者源码树与构建目录的绝对路径。剥什么取决于产物**是什么** —— 这是 dh_strip 的分档,而其中归档那一行是要命的: -| 产物 | strip 参数 | 为什么不能更狠 | +| 产物 | strip 参数 | 不能更进一步的原因 | |---|---|---| | 可执行文件 | `--strip-all` | 没有人链接它 | | 共享库 | `--strip-unneeded` | 保留 `.dynsym` —— 那**就是**导出表 | @@ -270,7 +279,7 @@ Win32 loader 解析 DLL 的第一顺位就是**可执行文件所在目录**,而 `vcruntime140.dll` / `msvcp140.dll` **不是** Windows 自己的:它们属于 MSVC toolset,就像 `libstdc++.so` 属于 gcc。它们要不要跟着产物走,由 `cxx_runtime` -决定(见 `docs/zh/05-mcpp-toml.md`),不由这张表决定 —— 而 `mcpp pack` 会拒绝 +决定(见 `docs/zh/04-mcpp-toml.md`),不由这张表决定 —— 而 `mcpp pack` 会拒绝 那些无法兑现契约的组合: ``` @@ -348,3 +357,4 @@ macOS **程序** bundling(Mach-O 依赖闭包,走 `otool -L` / `LC_LOAD_DYLIB`, 会在该格式上拒绝,而不是产出一个只是看起来像 bundle 的东西。当前 `.zip` 之外的 Windows DLL 分发,以及 `.deb` / `.rpm` / AppImage 等格式,同样在规划中。本文档随 `mcpp pack` 实现演进,最新选项以 `mcpp pack --help` 为准。 + diff --git a/docs/zh/10-publishing-a-library.md b/docs/zh/11-publishing-a-library.md similarity index 89% rename from docs/zh/10-publishing-a-library.md rename to docs/zh/11-publishing-a-library.md index 8a059172..7c5428f6 100644 --- a/docs/zh/10-publishing-a-library.md +++ b/docs/zh/11-publishing-a-library.md @@ -1,10 +1,19 @@ -# 10 - 发布一个库到 mcpp-index +# 11 —— 发布一个库到 mcpp-index -[English](../10-publishing-a-library.md) | **简体中文** +[English](../11-publishing-a-library.md) | **简体中文** + +**读者:**希望自己的包能被别人写进 `[dependencies]` 的库作者。 + +**本章回答的那一个问题:**从打好 tag 到成为一个可解析的包,要走哪些步骤,以及它们 +必须按什么顺序发生。 + +**不在这里:**什么使两个包成为同一个包 —— [SPEC-001](../specs/package-identity.md); +以及交付编译产物,那是 [12](12-binary-distribution.md)。在此之前: +[10 —— 发布打包](10-pack-and-release.md)。 一个库如何变成 `[dependencies]` 可以写出来的东西。这是**库作者**的链路; -[09 - 发布 mcpp](09-release.md) 讲的是发布 mcpp 自身, -[02 - 发布打包](02-pack-and-release.md) 讲的是 `mcpp pack` 打包应用。 +[92 - 发布 mcpp](92-release.md) 讲的是发布 mcpp 自身, +[10 - 发布打包](10-pack-and-release.md) 讲的是 `mcpp pack` 打包应用。 ## 发布顺序 @@ -143,3 +152,4 @@ mcpp 的构建沙箱是网络隔离的,`file://` 和 `http://127.0.0.1` 形式 - [ ] `publish-artifact.yml` 成功 - [ ] 冷解析(删掉播种拷贝后)能下载并编译 - [ ] 消费方已升版本 + diff --git a/docs/zh/12-binary-distribution.md b/docs/zh/12-binary-distribution.md index 7adaf0da..48ca5c7a 100644 --- a/docs/zh/12-binary-distribution.md +++ b/docs/zh/12-binary-distribution.md @@ -1,12 +1,19 @@ -# 12 - 分发预编译库 +# 12 —— 分发预编译库 [English](../12-binary-distribution.md) | **简体中文** +**读者:**交付编译产物而不是源码的发布方。 + +**本章回答的那一个问题:**怎样交付二进制,以及消费方的构建如何判断其中哪一个合用。 + +**不在这里:**发布源码,那是 [11 —— 发布一个库](11-publishing-a-library.md);以及 +兼容性 tag 的加速器字段,那是 [42 —— 异构硬件构建](42-heterogeneous-builds.md)。 + > 把一个库以**接口 + 预编译二进制**的形式分发,而不是发源码。 > 适用于闭源分发、离线环境,以及构建产物已在构建农场生成的场景。 > -> 相关文档:[02 - 打包应用](02-pack-and-release.md) 说明**程序**的打包; -> [10 - 发布一个库](10-publishing-a-library.md) 说明源码分发通路。 +> 相关文档:[10 - 打包应用](10-pack-and-release.md) 说明**程序**的打包; +> [11 - 发布一个库](11-publishing-a-library.md) 说明源码分发通路。 ## 概述 @@ -28,7 +35,7 @@ mcpp pack mathkit --target x86_64-linux-gnu \ | `kind` | `mcpp pack ` 产出 | `--mode` | |---|---|---| -| `bin` | 应用 bundle(见 [02](02-pack-and-release.md)) | 四档 | +| `bin` | 应用 bundle(见 [10](10-pack-and-release.md)) | 四档 | | `lib` | **静态库包** | — | | `shared` | **动态库包** | — | @@ -231,7 +238,7 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"] 这是**降级**而不是变砖,方向是对的。但它意味着**闸门只保护新客户端**, 面向混合版本用户群发布时,这一条应写进发布说明。 -## 包里带什么走,以及刻意不带什么 +## 随包内容与刻意排除的部分 发布出去的包必须能在**不是发布者的**机器上工作。两个步骤保证这件事, 它们作用在打包器暂存的每一个产物上。 @@ -257,7 +264,7 @@ error while loading shared libraries: libstdc++.so.6: cannot open shared object **`$ORIGIN` 不是解药。** 在真实的包上、把构建机的 store 变成不可达之后实测: -| 发货 `.so` 上的状态 | 消费方 `DT_RPATH` 被继承? | 结果 | +| 发货 `.so` 上的状态 | 消费方 `DT_RPATH` 的继承 | 结果 | |---|---|---| | 失效的绝对路径 `DT_RUNPATH` | 否 | 失败 | | **完全没有这条 tag** | **是** | **能跑** | @@ -279,7 +286,7 @@ Mach-O 上打包器会读出 `LC_RPATH` 并在包会携带它时告警;自动改 ### 调试信息会被剥掉 -参数、分档表与 `--debug-symbols` 见 [docs/02](02-pack-and-release.md)。 +参数、分档表与 `--debug-symbols` 见 [docs/02](10-pack-and-release.md)。 对**库**包最要紧的一条:静态归档只做 `--strip-debug`,因为 `--strip-all` 会删掉 归档的符号索引,消费方链接时会报 `archive has no index; run ranlib to add one`。 @@ -428,3 +435,4 @@ e2e 套件按宿主能力给每条测试开门,所以「套件是绿的」和「 xvm 的 **shim**,在 e2e 套件改过的环境里它回答「未安装」。所以老客户端检查的 **静态那半**(生成的 manifest 不含任何旧 mcpp 读不了的段)到处都跑,而**真实那半** —— 用上一版发布的 mcpp 去构建这个包 —— 是**手工跑的,不是 CI 跑的**。 + diff --git a/docs/zh/17-the-project-environment.md b/docs/zh/17-the-project-environment.md deleted file mode 100644 index f85458f8..00000000 --- a/docs/zh/17-the-project-environment.md +++ /dev/null @@ -1,161 +0,0 @@ -# 17 - 项目环境 - -项目可以声明自己在哪个环境里构建。这一条声明决定项目链接哪个 C 库、以及它的 -构建程序找到哪些工具——于是同一份 `mcpp.toml` 在开发机和 CI 上是同一个构建, -不论这两台机器上还装了别的什么。 - -```toml -[xlings] -subos = "tools" - -[xlings.workspace] -"xim:qemu-riscv" = "9.2.4-1" -``` - -可运行的工程:`examples/07-project-subos/`。 - -## 1. SubOS 是什么 - -SubOS 是一个目录,里面是一份用户态:它自己的 `bin`、自己的库视图、自己那套已 -装包版本,以及一个自述用的 `subos_info` 块。mcpp 把它当作「这个项目对着什么 -构建」的答案,而且是回答这个问题的唯一机制——不是编译器所在路径,不是 -`XLINGS_ACTIVE_SUBOS`,也不是当前 shell。 - -存在两种,区别在于目录落在哪里: - -| 声明 | 目录 | 与谁共享 | -|---|---|---| -| 未声明 | mcpp 初始化的 `subos/default` | 机器上的每个项目 | -| `subos = "default"` | 同一个目录,只是被显式点名 | 机器上的每个项目 | -| `subos = ""` | `/.mcpp/.xlings/subos//` | 不共享 | - -第三行是隔离的那种。它属于该项目,就放在清单旁边,删掉项目它也随之消失。 - -## 2. 这条声明决定什么 - -**C 库。** payload-first 的构建链接的是某一个确定的 glibc,而「哪一个」是项目 -的性质而非机器的性质。第 8 章讲绑定本身、降级规则,以及一个不自述的 SubOS 会 -让这些规则变成什么。 - -**构建程序看见哪些工具**(mcpp 2026.8.25.1+)。被声明环境的 `bin` 放在 -`build.mcpp` 运行时 `PATH` 的最前面: - -``` -PATH=<被声明环境的 bin>: -``` - -因此构建程序里把 `qemu-system-riscv64` 写成裸名,拿到的就是那个环境里的副本。 -这条通道的契约见第 7 章。 - -**只对声明了的项目生效。** 没有 `[xlings].subos` 的项目拿到的是 mcpp 启动时 -的 `PATH`,逐字节不变。把一个共享目录放到每个项目前面,会让「构建看见什么」 -取决于这台机器上还装过什么——同一台机器上的两个项目彼此一致,而同一个项目在 -两台机器上不一致。是声明本身把它放到了前面。 - -**前置而非替换。** 构建程序理应会调 `git`、`python3` 或 shell,这些都不在 -SubOS 里。前置让被声明的环境成为默认答案;其余的仍在它后面可达。 - -### 2.1 哪些版本钉生效(2026.9.3+) - -指名一个环境,同时改变了工具的版本从哪来。工程自己 `[xlings.workspace]` 里的条目 -总是胜出;不同的是它们叠在什么之上: - -| 工程声明了 | 它没点名的工具,版本来自 | -|---|---| -| `[xlings.workspace]`,无 `subos` | 机器的环境 | -| `[xlings.workspace]` 与 `subos = "<名>"` | 那个环境自己的 workspace;机器的不适用 | - -第二行就是隔离的含义。指名的环境有自己的已安装集合,把机器的版本带进去会指向那里 -不存在的版本 —— 所以原本依赖「机器上装了就能用」的工程,一旦指名环境,就必须把用到 -的都声明出来。 - -在工程内执行的 `xlings use` 压过这两者,直到 mcpp 重写环境为止:它是最后合并的那 -一层,而人做出的动作应当压过一份文件。 - -## 3. 这条声明不决定什么 - -`[xlings.workspace]` 声明的是「环境里要有哪些包」,而每个包的载荷目录另有通道交付, -即 `MCPP_XPKG__DIR`。这与 `PATH` 是两个问题,答案也保持分开:需要某个包 -的数据文件(比如 protoc 自带的 well-known `.proto`)的构建程序问目录,需要 -**运行**某个程序的构建程序问 `PATH`。 - -**工作区成员的声明不是工作区的声明。** 工作区构建中由工作区根持有这个选择;成员的 -`[xlings]` 只在该成员作为独立根被构建时生效。 - -**依赖的声明是另一回事,而且它被采纳**(`[xlings] deps` 自 2026.9.5.4,下面那条版本 -规则自 2026.9.6.6)。板级支持包知道哪个模拟器够得到它那台机器,规则包知道它驱动哪个 -工具包;要消费者把这些再写一遍,正是这类包存在的意义所反对的重复。依赖声明的东西会被 -装上,而 `MCPP_XPKG__DIR` 在那个依赖自己的构建程序里为它作答。 - -工程与依赖命名**同一个包**时,只装它的一个版本:身份是 `(namespace, name)`,版本是这个 -包上的约束。离产物更近的声明赢,并且覆盖会被报出来;不满足对方所陈述之要求的钉会被拒绝 -并点出两侧。见 [05 — mcpp.toml](05-mcpp-toml.md) 的「一个包一个版本」。 - -## 4. 只读取环境,从不创建环境 - -mcpp 解析被声明的名字,并读取它找到的东西。解析不到的名字是硬失败: - -``` -error: selected SubOS 'tools' does not exist at …/.mcpp/.xlings/subos/tools; -create/bootstrap that environment instead of falling back to active/default -``` - -回退到 default 或回退到当前活跃的那个,等于用另一个环境顶替清单点名的那个,而 -这恰恰会让一份 `mcpp.toml` 意味着两个不同的构建。创建并填充 SubOS 属于 xlings -这一层——`xlings subos new`——mcpp 去管理 SubOS 状态则是把分层倒置。 - -一个存在但**不携带 `subos_info` 块**的环境是**降级而非失败**:运行时绑定报 -`inconclusive`,没有 payload-first 绑定可用,打印一条提示,构建继续。完整规则见 -第 8 章。 - -## 5. 什么时候值得用私有环境 - -- **产物取决于版本的代码生成器。** `protoc`、`flatc`、着色器编译器:它的输出是 - 下游一切的输入,所以项目钉住生产者,而不是指望机器上那个恰好兼容。 -- **构建程序要运行的模拟器。** 若干裸机包把「在 QEMU 里启动产物」作为验证的一 - 部分;是哪个 QEMU 属于「验证了什么」的一部分。 -- **CI 与开发机不一致的项目**,两边都没错,而构建不该察觉到差异。 -- **同一台机器上两个项目需要同一工具的不同版本。** 共享目录意味着必有一方落 - 败;私有环境让这个问题不成立。 - -代价一侧:隔离环境是一个必须被创建并填充的目录,而这笔账由首次构建来付。 -2026.8.29 起 mcpp 会做这件事 —— 声明在 `[xlings.workspace]` 里的包在首次使用时被供给, -一个尚不存在的具名 `[xlings] subos` 会被创建而不是被拒绝 —— 但代价是实打实的: -干净机器上的第一次构建会先下载安装,然后才编译。工具很普通、版本也无所谓的项目, -不声明、直接继承机器的那份更划算。 - -在 `--offline` / `MCPP_OFFLINE` 或 `MCPP_NO_AUTO_INSTALL` 下,mcpp 转为拒绝而不是 -安装,并列出包名以便手动供给 —— 与 `[toolchain]` 遵守的是同样两个开关,理由也相同: -一次没被要求的下载,不该由构建替工程决定。 - -这份声明在每台构建本工程的宿主上都会供给,宿主装不了的包是错误,不是被跳过的条目。 -只存在于某一个宿主平台的工具因此按平台声明(2026.9.2.1): -`deps = [{ linux = "qemu-user-aarch64" }]` 在 Linux 上声明这个模拟器,在别处什么都不声明。 -键与解析规则见第 5 章 §2.13。 - -**哪些命令会安装它。** 一条条目可以带档位 —— `{ version = "0.24.0", when = "run" }` —— -`[feature-xlings.]` 则把工具挂在某个 feature 上。用不到的工具因此不会被下载: -见第 5 章 §2.13。不写档位就是从前的行为。 - -**runner。** `[xlings.workspace]` 下的程序也是 `[target.].runner` 查找其第一个元素 -的首选位置,在 `PATH` 之前(第 5 章 §2.7.3)。两个键合起来,在 CI 宿主上供给用户态模拟器, -并通过它执行交叉构建的产物,而清单不必写出载荷的路径。 - -## 6. 什么该写在别处 - -| 需求 | 写在哪里 | -|---|---| -| 程序链接的库 | `[dependencies]` | -| 编译器 | `[toolchain]`,第 3 章 | -| 依赖产出的宿主工具 | `tools = [...]`,第 7 章 | -| 环境里要有的工具 | `[xlings.workspace]` | -| 只有某个命令或某个 feature 需要的工具 | `when = "run"`、`[feature-xlings.]` | -| 用哪个环境 | `[xlings] subos` | - -## 7. 相关章节 - -- [7 - build.mcpp](07-build-mcpp.md) —— 构建程序收到的契约,含它运行时的 - `PATH`。 -- [8 - 工具链内部](08-toolchain-internals.md) —— 运行时选择、`RuntimeBinding` - 快照与降级规则。 -- [5 - mcpp.toml](05-mcpp-toml.md) —— 全部清单键,含 `[xlings]`。 diff --git a/docs/zh/03-toolchains.md b/docs/zh/20-toolchains.md similarity index 68% rename from docs/zh/03-toolchains.md rename to docs/zh/20-toolchains.md index 6c9e67c2..45c87e6b 100644 --- a/docs/zh/03-toolchains.md +++ b/docs/zh/20-toolchains.md @@ -1,4 +1,13 @@ -# 03 — 工具链管理 +# 20 —— 工具链管理 + +**读者:**在意「哪个编译器在跑」的人 —— 版本下界、第二套工具链,或者一台不许用 +自带编译器的机器。 + +**本章回答的那一个问题:**这个工程会被哪个编译器构建、它怎样被安装、以及怎样换成 +另一个。 + +**不在这里:**这套解析是怎么实现的,那是 [91 —— 工具链机制内幕](91-toolchain-internals.md); +以及目标怎么命名,那是 [21 —— 目标三元组](21-the-target-triple.md)。 > mcpp 维护一个独立的工具链沙盒,与系统 PATH 完全隔离。 @@ -79,7 +88,7 @@ mcpp toolchain default gcc@16 --target x86_64-linux-musl # "默认就要全静 `[toolchain] default = "gcc@16.1.0"` + `default_target = "x86_64-linux-musl"`。 (存量 config 里 `default = "gcc@15.1.0-musl"` 这类合并拼写原样可用。) -### 谁决定一次构建的编译器 +### 一次构建的编译器选定 有五种来源会给它命名。它们是分级的,而这套分级正是让工程能写下的那两条压过 mcpp 自己保管的一切的原因: @@ -156,7 +165,7 @@ Available toolchains (run `mcpp toolchain install `): `*` 标记当前的默认对。Targets 块是 target 词汇表的实时视图,共四种状态: -| 状态 | 含义 | 下一步做什么 | +| 状态 | 含义 | 后续步骤 | |---|---|---| | `installed` | 本机已有的载荷就能产出它 | 无 | | `available` | 本宿主存在可装的载荷 | `mcpp toolchain install` | @@ -200,7 +209,7 @@ mcpp toolchain default gcc@16 --target x86_64-windows-gnu `import std`)。该 target 默认 linkage 为 **static**——产出的 `.exe` 完全自包含(无需随包分发 `libstdc++-6.dll`,可直接在 wine 下运行); 要退出请写在 target 段上——`linkage` 只认精确 triple(见 -[mcpp.toml](05-mcpp-toml.md) §2.7),`[build] linkage` 这个键并不存在,写了会被静默忽略: +[mcpp.toml](04-mcpp-toml.md) §2.7),`[build] linkage` 这个键并不存在,写了会被静默忽略: ```toml [target.x86_64-windows-gnu] @@ -307,7 +316,7 @@ error: [toolchain] linux = "system" is not supported: mcpp builds only with `msvc@system` 是**唯一的例外**,而且是另一种拼法:它点名的是一个**族**,mcpp 负责定位并识别 其安装 —— 那是唯一一个编译器不能被重新分发的平台。见上一节。 -#### 为什么工具链与库得到的答案不同 +#### 工具链与库答案不同的原因 mcpp 对 host 依赖的规则并不是各条轴统一的,这个分叉是刻意的: @@ -398,7 +407,7 @@ STL,以及通过它的 `xim:windows-sdk` 依赖带来 ucrt/um 的头文件与导 **Windows SDK 跟着来源走**,因为两条来源回答的是不同的问题,SDK 也必须如此: -| 来源 | SDK 怎么选 | +| 来源 | SDK 的选定方式 | |---|---| | `msvc@` | **随该 toolset 一起装进 mcpp store 的** `xim:windows-sdk` payload。环境里的 `WindowsSdkDir` / `WindowsSdkVersion` 会被**忽略**,并且 mcpp 会打印一行 `note:` 说明。 | | `msvc@system` | 先 **`WindowsSdkDir`**(+ `WindowsSdkVersion`),再 `C:\Program Files (x86)\Windows Kits\10`。 | @@ -437,7 +446,7 @@ cxx_runtime = "self-contained" # C++ 运行时那根轴 CRT。 toolset 自带的那份可再分发 CRT(`vcruntime140.dll` / `msvcp140.dll`)可以跟着 -产物走 —— 见 `docs/zh/05-mcpp-toml.md` 的 `cxx_runtime = "toolchain-coupled"`。 +产物走 —— 见 `docs/zh/04-mcpp-toml.md` 的 `cxx_runtime = "toolchain-coupled"`。 ## 项目级版本锁定 @@ -489,7 +498,7 @@ error: target 'x86_64-linux-musl' takes its C library from the 'gcc@16.1.0' 有两类行回答的是另一个问题,它们的 pin 根本不可被推翻: -| 行 | 为什么 | +| 行 | 原因 | |---|---| | 所有 `*-none-elf` | 不存在按宿主分的交叉载荷;clang 与 lld 按构造就是交叉编译器,gcc 不是 | | `x86_64-windows-musl` | 没有任何 gcc 载荷发得出 PE + musl —— mingw 载荷发的是 PE + MinGW CRT,那是隔壁 `-gnu` 那一行 | @@ -506,7 +515,7 @@ error: target 'riscv64-none-elf' cannot be emitted by 'gcc@16.1.0'. 要给这些结果分类的程序读 `mcpp why toolchain --format json` 的 `data.reason` (`convention-unreplaced` / `capability-pin`),而不是那句话 —— -见[第 11 章](11-machine-output.md)。 +见[第 11 章](50-machine-output.md)。 项目还可以声明自己的*默认*构建 target——"本项目发布全静态"这类语义 就该放在这里(全静态是产物属性,不是编译器家族属性): @@ -603,3 +612,225 @@ inline MA& operator+=(MA& a, const MB& b); `tests/e2e/150_clang_module_operator_template.sh` 是一只跑在内置 LLVM 工具链上的金丝雀 —— 未来某次 Clang 升级修好(或再次弄坏)这一点时, 它会显式暴露出来,而不是悄悄改变包能表达的东西。 + +## C++ 运行时契约(`cxx_runtime`) + +`cxx_runtime` 声明的是**产物对运行它的机器做出的承诺**。它是**分发**属性而非 +构建属性 —— 它描述的是运行期依赖集,而兑现它的 flag 逐平台不同。 + +> **不含 C++ 的目标没有 C++ 运行时契约需要兑现。** mcpp 用 C 驱动链接它, +> 并且完全不发 C++ 运行时相关的 flag,因此一个纯 C 的共享库不会平白拿到 +> `libstdc++` / `libc++` 依赖。目标里只要有一个 C++ 翻译单元,整个目标就回到 +> C++ 驱动。这一判定由源码推导,没有对应的配置键。 + +```toml +[build] +cxx_runtime = "self-contained" # 作用于所有目标(默认值) + +# 或者按角色分别指定: +[build.cxx_runtime] +default = "self-contained" # 可执行文件 +tests = "host-coupled" # 测试二进制从不离开本机 +shared = "self-contained" # 共享库(见下 —— 默认值随目标格式而变) + +# 或者按目标三元组 —— 与 `linkage` 并列,因为它们是同一根轴: +[target.x86_64-linux-gnu] +cxx_runtime = "host-coupled" # 例如这次构建是为发行版打包 +``` + +| 取值 | 产物运行时需要 | 典型场景 | +|---|---|---| +| `self-contained`(默认) | 自身之外不需要任何 C++ 运行时 | 分发二进制 | +| `toolchain-coupled` | mcpp 装的那份工具链的 C++ 运行时 | 本地迭代 | +| `host-coupled` | 驱动默认解析到的那份(通常是系统运行时) | 发行版打包;必须与宿主共用同一份运行时的 `dlopen` 插件 | + +**默认即自包含(portable by default)**:macOS 上这会静态链入 LLVM 自带的 +libc++/libc++abi —— 系统 libc++ 会把实际可运行版本固定在构建机的 OS(老系统 +缺新符号,如 `std::print` 的支撑符号),只有静态化才能真正兑现 +`macos_deployment_target` 的 floor。Linux/MinGW 上它是 `-static-libstdc++` +(GCC)或整条链的 `-static`(MinGW);Linux 上的 clang/libc++ 工具链则显式链入 +libc++.a/libc++abi.a/libunwind.a。更低的 macOS floor(11–13)需自建 libc++ +归档(已验证可行,数据级切换,按需提供)。 + +**共享库是唯一一个默认值随目标格式变化的角色**,因为危害本身随格式变化。 +`.so`/`.dylib`/`.dll` 不是一个小号可执行文件 —— 它被加载**进**一个已经有 +C++ 运行时的进程。 + +| 目标格式 | `kind = "shared"` 的默认契约 | 原因 | +|---|---|---| +| ELF(Linux 等) | `toolchain-coupled` | ELF 只有一个全局符号命名空间,先加载的定义胜出。静态内嵌了 libstdc++ 的 `.so` 会把它**导出**,链接该库的可执行文件于是把自己的 `std::` 引用绑到那里 —— 它自己的 `self-contained` 契约静默变成空操作,它的 C++ 运行时变成"碰巧加载的那一份该库"。 | +| Mach-O | `self-contained` | 那里的机制本来就是 `-load_hidden`(hidden 可见性),dyld 不会归一这些符号;而且 macOS 上根本没有 toolchain-coupled 这一档(见下文注)。 | +| PE(Windows) | `self-contained` | PE 没有全局符号命名空间 —— 导入按 DLL 逐个按名解析,一个 DLL 的私有运行时不可能被别人捡走。 | + +在 ELF 上显式写 `shared = "self-contained"` 是支持的,而且就是字面意思:库会内嵌 +运行时。此时 mcpp 会额外发 `-Wl,--exclude-libs`(针对标准库归档),让内嵌的那份 +留在库的动态符号表之外,链接它的任何东西都捡不走。本工程代码产生的模板实例化 +(`std::string` 之类的 weak/COMDAT 符号)仍然会导出 —— 那是 C++ ABI 的预期行为, +不是这里要防的泄漏。 + +工程级的 `cxx_runtime = "…"`(或 `static_stdlib = false`)同样作用于共享库: +有人写下了整个工程的承诺。只有在**没人写**的时候,随格式变化的默认值才生效。 + +`static_stdlib` 是旧拼写,仍然有效:`true` 等价于 `self-contained`,`false` +等价于 `host-coupled`。显式写了 `cxx_runtime` 时以后者为准。 + +**兑现不了的契约会被报出来,绝不静默降级。** 若工具链不带 `libc++.a`,或某个 +契约在该平台上没有对应机制,构建会打印实际退到了哪一档,而不是悄悄交付一个与 +manifest 所述不同的产物。 + +### 在 MSVC 运行时上 + +这里的机制就是 CRT 模型,而它是**整个工程级**的开关:cl 会把 `_MSVC_MT` / +`_MSVC_MD` 烘进工程唯一的那份 `std` 模块,所以与工程不一致的按角色契约无法兑现, +会被报出来而不是被忽略。 + +| 取值 | 在 MSVC 上的含义 | +|---|---| +| `self-contained` | `/MT`,静态 CRT。`linkage = "static"` 从 libc 那根轴选中的是同一件事。 | +| `host-coupled`(`/MD` 下的默认) | 由目标机器提供 `vcruntime140.dll` / `msvcp140.dll` —— 即那台机器装了 Visual Studio 或 redistributable。 | +| `toolchain-coupled` | toolset **自带**的那份 DLL 跟着产物走。 | + +`toolchain-coupled` 值得说清楚,因为直觉上的理解是错的。`ucrtbase.dll` **是** +Windows 组件(Win10 起),mcpp 从不分发它;而 `vcruntime140.dll` / +`msvcp140.dll` **不是**:每个 MSVC toolset 都在 +`VC\Redist\MSVC\\\` 下带着它们,和 gcc payload 带着 +`libstdc++.so` 是同一件事。在这个契约下 mcpp 会把它们放到产物旁边 —— 这正是让 +默认的 `/MD` 产物能在"只装了 pinned toolset、根本没有 Visual Studio"的机器上跑 +起来的原因。 + +调试版 CRT(`debug_nonredist\` 下的 `vcruntime140d.dll` 等)永远不会被放进去: +它不可再分发。 + +> **从 2026.8.15 或更早版本升上来?** 这条键在 MSVC ABI 上曾经是**空操作** —— +> 它会报 `not implemented for the MSVC runtime yet`,写什么都退回 `/MD`。 +> 自 2026.8.16 起它真的生效,于是一份从那个年代带着 +> `cxx_runtime = "self-contained"` 的 manifest **会在升级时换掉 CRT 模型**: +> 从 `/MD` 变成 `/MT`。它不是同一个模型的更严格版本,而且因为这个值一直是合法的, +> 切换是**静默**的。若工程是在这条键尚未生效时写下它的,应重新确认所需的取值。 + +把它和 `/MT` 一起用是**矛盾**而不是缺功能 —— 静态 CRT 根本没有 DLL 可以耦合 —— +所以会被报出来并落到 `self-contained`。另一半由 `mcpp pack` 兜底:什么都不打包的 +模式(`--mode system`、`--mode static`)兑现不了 `toolchain-coupled`,会直接拒绝。 + +**边界。** 该契约只管 C++ 运行时。静态 **libc** 是另一根轴(`linkage = "static"` +/ `--static`,如 musl 目标),部署下限是第三根轴(`macos_deployment_target`)。 +另外,`host-coupled` 只承诺 mcpp 不做任何"把 C++ 运行时打进产物"的动作,它不会 +去掉链接因其它原因已经携带的工具链 rpath —— 所以在 ELF 上这类产物仍可能优先 +找到工具链的库。 + +> **macOS + `self-contained` 与静态初始化次序。** Mach-O 没有按优先级排序的 +> 初始化段,而 libc++ 的 `` 也不像 libstdc++ / MSVC STL 那样自带 +> `ios_base::Init` 守卫 —— 于是从 `libc++.a` 里拉出来的流初始化器本来会排在 +> 程序自己的全局构造函数**之后**:一个在构造函数里碰 `std::cout` 的全局对象会 +> 读到尚未构造的流,进程启动即崩。mcpp 会把一个极小的生成对象排在链接最前面 +> 把流顶上去,调用方代码无需改动。详见 mcpp-community/mcpp#336。 + +`defines` 接受**裸**宏名(不带 `-D`),把每个条目脱糖为 `-D`,同时作用于 C 和 +C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入**编译器自己的** +P1689 模块扫描。 + +> **但它不会让被宏保护的 `import` 变得可用。** mcpp 在编译器看到文件之前先跑 +> 自己的词法预扫描,而那个扫描器对**任何** `#if` / `#ifdef` 块内的 `import` +> 一律拒绝,不求值条件: +> +> ``` +> error: import statement inside conditional preprocessor block (forbidden in M1) +> ``` +> +> 所以即使 `FOO` 写在 `defines` 里,`#ifdef FOO` / `import bar;` 仍然会失败。 +> 替代写法是把条件放在全局模块片段的 `#include` 上。见 +> mcpp-community/mcpp#421。 + +汇编单元同样能拿到。它是普通的构建 +输入,所以 `[target.'cfg(...)'.build]` 也能承载它: + +```toml +[build] +defines = ["APP_NAME=\"demo\""] + +[target.'cfg(windows)'.build] +defines = ["USE_WIN32", "WINVER=0x0A00"] +``` + +选择合适的轴: + +| 想让宏作用于… | 用 | +|---|---| +| 本包的每个 TU | `[build].defines`(本节) | +| 仅某个二进制自己的入口源 | `[targets.].defines` | +| 指定的一批文件 | `[build].flags` 配 `glob` + `defines` | +| 本包每个 TU **以及**消费者的 TU | `[features.].defines`(接口贡献) | + +`[build].defines` 是包私有的:不会传播给消费者。 + +`[build]` 下不支持的键会作为警告报出(`--strict` 下为错误),而不是被静默忽略。 + +C++ 标准不要通过 `build.cxxflags = ["-std=..."]` 配置。请使用: + +```toml +[package] +standard = "c++26" +``` + +mcpp 会把同一个标准用于普通 C++ 编译、模块扫描、`compile_commands.json` 和 `import std` 的标准库 BMI 构建。 + +**glob 排除**(`!` 前缀,mcpp 0.0.4+): + +```toml +[build] +sources = [ + "src/**/*.cpp", + "!src/**/*_test.cpp", # 排除测试文件 + "!src/**/*_fuzzer.cpp", # 排除 fuzzer +] +``` + +**per-glob 旗标**(mcpp 0.0.95+):`[build] flags` 是**有序**的内联表数组,把额外 +编译旗标只附加到 glob 命中的源文件——SIMD 多档 dispatch TU 与三方代码告警隔离的 +正解: + +```toml +[build] +flags = [ + { glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] }, + { glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] }, + { glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] }, +] +``` + +每条目键:`glob`(相对包根,必填)+ `cflags` / `cxxflags` / `asmflags` / +`defines`(没有 `ldflags`——链接没有 per-TU 作用域)。声明顺序即应用顺序:靠后 +条目的旗标排在命令行更后,配合 GNU "后旗标胜",窄 glob 放在宽 glob 之后即可覆盖。 +所有命中条目都生效;这些是私有构建旗标,不会传播给消费者。glob 零命中会打印 +warning(打错的 glob 不允许静默无效)。 + +**生成文件**(mcpp 0.0.95+):`[generated_files]` 把相对路径映射到文件内容(支持 +TOML 多行字符串)。条目在源 glob 展开之前写入工程树——与 index 描述符合成模块 +包装文件是同一机制——内容进指纹,改内容即重建: + +```toml +[generated_files] +"src/gen/wrap.cppm" = """ +module; +#include +export module wrap; +""" +``` + +路径必须留在工程根之内(`..` / 绝对路径是解析错误)。 + +**汇编源**(mcpp 0.0.95+):`.S`/`.s`(GAS——由 C 驱动器预处理,覆盖 ARM 与 +AT&T 语法 x86)和 `.asm`(NASM——Intel 语法 x86)是一等源文件:默认 glob 收录、 +进指纹、增量并行构建、像任何对象一样链接。NASM 的输出格式由目标三元组推导 +(`elf64`/`win64`/`macho64`/...——交叉构建零特判);`nasm` 仅在存在 `.asm` 单元时 +惰性解析:先 `PATH`,再 mcpp 沙箱,再 `xlings install nasm`;找不到 ≥2.16 的 +nasm 则**硬失败**(汇编绝不静默跳过)。限制:`.asm` 仅限 x86 目标(其他目标硬 +报错——用条件 sources 门控)、MSVC 工具链不支持 `.S`、`.asm` 即 NASM 语法 +(MASM 源请用 `!` 排除)。 + +## 当前边界 + +- **macOS 宿主上完全没有面向 Linux 的载荷**,因此从那里够不到任何 Linux 目标。这是 + 发布边界,不是引擎边界。 +- manifest 里显式写的 `[toolchain]`,或 `[target.X].toolchain`,**永不被推翻**。工程 + 钉了一个、随后遇到这个钉子服务不了的目标时,会被拒绝,而不是悄悄换成另一个编译器。 diff --git a/docs/zh/21-commands-by-scenario.md b/docs/zh/21-commands-by-scenario.md deleted file mode 100644 index 14037f36..00000000 --- a/docs/zh/21-commands-by-scenario.md +++ /dev/null @@ -1,183 +0,0 @@ -# 21 — 按场景选命令 - -命令清单是 `mcpp --help`,每个子命令还有自己的 `--help`。本章回答的是另一个问题: -某个情形已经发生时该用哪条命令 —— 构建目录一直变大、解析结果出乎意料、描述符即将 -发布、索引可能陈旧。这里收的都是名字本身没有说出它所属场景的命令。 - -相关文档:[00 — 快速开始](00-getting-started.md)(日常构建与测试循环)、 -[03 — 工具链管理](03-toolchains.md)、 -[10 — 发布库到 mcpp-index](10-publishing-a-library.md)、 -[11 — 机器可读输出](11-machine-output.md)。 - -下面每段输出都由本章所在版本的 mcpp 实际产生。 - -## 回收磁盘而不触发重编 - -有两个存储会增长,增长的原因不同,各由一条命令清空。把两者弄混的代价是一次全量重编。 - -| 存储 | 作用域 | 增长时机 | 清空方式 | -|---|---|---|---| -| `target/<三元组>/<指纹>/` | 单个工程 | 配置指纹变化,开出新目录 | `mcpp clean`、`mcpp clean --stale` | -| 构建缓存(`mcpp cache dir`) | 整台机器 | 任何工程编译依赖或 `std` 模块 | `mcpp cache gc`、`mcpp cache prune`、`mcpp cache clean` | - -`mcpp clean` 整个删掉 `target/`,下次构建重编一切。`mcpp clean --stale` 只删已无构建 -记录使用的指纹目录,在用的配置保留: - -``` -$ mcpp clean --stale --dry-run -would remove target/x86_64-linux-gnu/0123456789abcdef (0.0 B) -Would remove 1 directory (0.0 B) -``` - -「在用」指被 `target/.build_cache` 记录 —— 它由 `mcpp build` 写入,由快路径读取。 -这个定义带来三个后果: - -- 没有记录并不足以让一个目录被删。`mcpp test` 走的构建路径不写记录,`--no-cache` - 构建同样不写。未被记录但在 `--older-than`(默认一天)之内写过的目录保留;更旧的 - 会被删,而判断错误的代价是重编一个此后无人碰过的配置。 -- 完全没有记录时,命令拒绝执行而不是猜测。跑一次 `mcpp build` 即可确立什么是当前的。 -- `target/` 下不是指纹目录的东西 —— 例如 `mcpp pack` 的 `dist/` —— 从不被访问。 - -`--dry-run` 只列出,不删除。`--stale`、`--dry-run`、`--older-than` 三者任一都选中这一 -档:`mcpp clean --older-than 3d` 是一次有范围的请求,不会被读成整删。`--older-than 0` -不保留任何未记录目录;负的时长被拒绝。 - -构建缓存是全机共享的,所以工程级命令不得清空它 —— `--stale` 与 `--bmi-cache` 不能同时 -给出。`mcpp cache list` 列出占用。行没有排序,而 `0.0 B (incomplete)` 那样的行,是被 -中断的构建留下的条目: - -``` -$ mcpp cache list -key kind size last used package -8a150ad49d666f94 std 29.6 MiB 6d ago std gcc@16.1.0 c++23 libstdc++ -9234eed9ef786c13 std 0.0 B 2d ago std (incomplete) -``` - -`mcpp cache gc` 要求给出 `--max-size`、`--older-than` 或两者,并且只驱逐包条目。一份 -`std` BMI 被机器上每个工程共享,实现以「重建它是用大量时间换少量磁盘」为由把它排除在 -按体积驱逐之外。`mcpp cache clean --std` 仍是显式移除它的做法。 - -## 一个包发布了哪些版本 - -`mcpp search` 按子串匹配,并在每个命中行后附上该包发布的版本 —— 跨描述符的 per-OS 表 -合并,按 semver 降序: - -``` -$ mcpp search imgui - compat:imgui Dear ImGui immediate-mode GUI library core sources (1.92.8, 1.92.8-docking) - mcpplibs:imgui C++23 module package for Dear ImGui core and GLFW/OpenGL3 backends (0.0.6, 0.0.5, 0.0.4, ...) -``` - -末尾的 `, ...` 表示被截断:默认显示三个,没有这个标记就说明列表是完整的。 -`--all-versions` 打印全部。描述符读不到的包按两列输出 —— 版本列表是尽力而为的展示, -不会让 search 失败。 - -`mcpp add` 在名字解析不到时携带同样的信息。建议里给出该写的命名空间,以及它背后的版本: - -``` - a package with this name exists under another namespace: - compat.eui-neo (0.5.6, 0.5.5, 0.5.3) -``` - -这次扫描只在查找已经失败之后进行,结果只进入错误文本与 search 输出。裸名不会因此跨 -命名空间解析。 - -## 解释一次解析 - -`mcpp why` 报告一次构建会解析出什么,并且不构建任何东西: - -``` -$ mcpp why toolchain -toolchain: gcc 16.1.0 (x86_64-linux-gnu) - abi(libc)=glibc cxxstdlib=libstdc++ arch=x86_64 os=linux triple=x86_64-linux-gnu - reason: [toolchain] in mcpp.toml if set, else platform-native default -``` - -话题是 `toolchain`、`runtime`、`deps` 或 `runners`,不给话题时四者全报。`--target` 与 -`--toolchain` 把报告变成对当前目录并不使用的那一对的查询,目标矩阵正是这样逐格提问的。 - -诊断里的错误码可以用 `mcpp self explain` 展开: - -``` -$ mcpp self explain E0006 -E0006: index requires a newer mcpp - -The package index declares (index.toml [index].min_mcpp) that its -descriptors need a newer mcpp than this binary — parsing them would -silently misbehave, so resolution stops instead. Upgrade mcpp: -``` - -## 索引新鲜度与离线构建 - -`mcpp index status` 在不碰网络的前提下回答本地索引副本是否当前: - -``` -$ mcpp index status - index state refreshed revision path - xim fresh 28s ago 1f4b39d /home/speak/.mcpp/registry/data/xim-pkgindex - mcpplibs fresh 28s ago d4b36d7 /home/speak/.mcpp/registry/data/mcpplibs -``` - -`mcpp index update` 刷新它们。一个刚发布几分钟、刷新后仍然找不到的包,是传播问题而不是 -名字问题 —— 索引以 artifact 而非 git clone 的形式到达客户端。 - -`--offline`(或 `MCPP_OFFLINE=1`)在单次调用中禁止网络,宁可失败也不拉取。`--locked` 在 -解析结果与 `mcpp.lock` 不一致时失败而不是改写它,这正是 CI 作业需要的形状。 -`mcpp index pin ` 把自定义索引的某个 commit 记进 `mcpp.toml`; -`mcpp index unpin` 移除它。 - -## 发布前校验描述符 - -`mcpp xpkg parse` 用解析器自己的文法读描述符,所以它报告的就是解析时会看到的: - -``` -$ mcpp xpkg parse mcpp.plugins.lua -package mcpp.plugins (namespace 'mcpp') -versions linux 0.1.1, 0.1.0, latest -versions macosx 0.1.1, 0.1.0, latest -versions windows 0.1.1, 0.1.0, latest -form A — no mcpp segment (build info from the source's mcpp.toml) -parse OK -``` - -per-OS 列表分开打印是有意的:一个版本只加进了某一个平台表而在其余表里被遗漏,在缺它的 -平台上读起来就是「找不到」,而文件里明明含有这个版本字符串。`--json` 以同样的事实供脚本 -使用: - -``` -$ mcpp xpkg parse mcpp.plugins.lua --json -{"namespace":"mcpp","name":"plugins","versions":{"linux":["0.1.1","0.1.0","latest"],"macosx":["0.1.1","0.1.0","latest"],"windows":["0.1.1","0.1.0","latest"]},"form":"A"} -``` - -`mcpp emit xpkg` 生成要提交的条目。完整路径见 -[10 — 发布库到 mcpp-index](10-publishing-a-library.md)。 - -## 环境诊断 - -`mcpp self doctor` 检查工具链、`std` 模块、registry、缓存健康与最近一次运行期闭包判定, -并报告它查到了什么,而不只报告失败的部分: - -``` -$ mcpp self doctor - Checking toolchain - ok gcc 13.3.0 (x86_64-linux-gnu) at /usr/bin/g++ - Checking cache health - ok build cache size = 2.5 GiB -warning: pre-v1 cache at '/home/speak/.mcpp/bmi' occupies 167.5 MiB and is no longer used — `mcpp cache clean --legacy` reclaims it -``` - -`mcpp self env` 打印路径与已解析的工具链,含 `--format json`。 -`mcpp self config --mirror CN|GLOBAL` 选择下载镜像;mcpp 与 xlings 各自持有这个设置, -为其中一个选定不会为另一个选定。 - -## 当前边界 - -- `mcpp why --format json` 只对 `toolchain` 话题有定义。其余话题报 - `'' has no machine-readable shape yet` 并以非零退出。 -- `mcpp search` 按子串匹配;没有字段选择器,也没有把搜索限定到单个命名空间的方式。 -- `mcpp clean --stale` 读 `target/.build_cache`,它保存的近期条目数量有上限。一个工程 - 如果构建过的 (目标, profile) 组合多于这个上限,最旧的条目会被挤掉;条目被挤掉的目录 - 随后按未记录处理 —— 在 `--older-than` 之内保留,超出则删除。 -- `mcpp cache gc --older-than 0` 以 `bad --older-than value '0' - (expected {s,m,h,d})` 被拒绝,而 `mcpp clean --stale --older-than 0` 接受。 - 两个选项共用一个 parser,但这一种取值上不一致。 diff --git a/docs/zh/16-the-target-triple.md b/docs/zh/21-the-target-triple.md similarity index 91% rename from docs/zh/16-the-target-triple.md rename to docs/zh/21-the-target-triple.md index 11741dfc..9707ca39 100644 --- a/docs/zh/16-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -1,4 +1,12 @@ -# 目标三元组 +# 21 —— 目标三元组 + +**读者:**为另一台机器构建的人。 + +**本章回答的那一个问题:**目标怎样被命名、支持哪些目标、各在什么档位,以及哪些 +构建机能服务它们。 + +**不在这里:**manifest 怎样以目标为条件,那是 [22 —— 目标侧](22-target-side.md); +以及没有操作系统的目标,那是 [40 —— 裸机](40-baremetal.md)。 目标三元组写作 `-` 或 `--`。本章说明每一段的含义、 第三段何时可以省略,以及为何这个答案在 mcpp 同时支持的两种体系下并不相同。 @@ -15,7 +23,7 @@ C 库。选中 `x86_64-linux-musl` 就是选中 musl-gcc 载荷,选中 **构建期体系。** 目标侧以包的形式出现在依赖图中,由正在运行的那个编译器 从源码构建。第三段不选中任何东西,因为图已经决定了。这是 -[第 15 章](15-openkal-cross.md)所描述的体系。 +[第 15 章](24-openkal-cross.md)所描述的体系。 两者的差别在于第三段**做什么**,而不在于它怎么拼。工程不声明自己属于哪一种; 由依赖图决定,构建则报告它解析出了什么。 @@ -103,7 +111,7 @@ $ mcpp build --target aarch64-linux-musl `--target aarch64-linux-gnu` 仍会撞上 `planned` 行的拒绝 —— 那正是用显式 `[target.] toolchain` 提前加入某一行的逃生口。 -### 该用哪种拼法 +### 采用的拼法 **在构建期体系下,省略它。** 图供给 C 库与各运行时,那一段陈述的是一个 不会被查询的请求。在这种体系下,`x86_64-windows` 不只是比 @@ -116,7 +124,7 @@ $ mcpp build --target aarch64-linux-musl **在 Windows 上,想要微软那套 ABI 时写 `msvc`。** `gnu` 是默认填充, 而 `msvc` 是不同的对象 ABI 而非不同的 C 库,因此那一段在两种体系下都有意义。 -## 构建报告了什么 +## 构建的报告内容 报告以写下的目标为标题,并把它解析为编译器自己的拼写: @@ -152,7 +160,7 @@ Target x86_64-windows-gnu → x86_64-w64-windows-gnu (gnu selects the Itanium 横切事项。把它读成 `c++-abi libc++` 是第二个错误答案,因为 libstdc++ 坐在同一套 ABI 上。 -## 三套词表,以及它们为何不同 +## 三套词表,以及它们不同的原因 一个三元组由三方书写,而三方并不共用一套约定;mcpp 在它们之间翻译。 知道手上这个字符串属于哪一套,第三段带来的困惑就消掉大半。 @@ -196,7 +204,7 @@ clang++ → x86_64-unknown-linux-gnu,而 --target= 可改 MinGW 自己的三元组是 `x86_64-w64-mingw32`: -| 段 | 值 | 为什么 | +| 段 | 值 | 原因 | |---|---|---| | arch | `x86_64` | | | vendor | `w64` | 项目名 `mingw-w64`,用以区别于已停滞的原 `mingw32` 项目 | @@ -231,7 +239,7 @@ LLVM x86_64 - unknown - windows - gnu ### mcpp 保留的那一套 -| 词表 | 例 | 谁读 | +| 词表 | 例 | 读者 | |---|---|---| | GCC / autoconf | `x86_64-w64-mingw32` | 预构建载荷的编译器,以文件名的形式 | | LLVM | `x86_64-w64-windows-gnu` | `clang --target=` | @@ -323,7 +331,7 @@ default = "llvm@22.1.8" 拒绝里为什么点名 openkal。 **裸机行与 `x86_64-windows-musl` 行的工具链不是约定**,根本不可被推翻—— -见[第 03 章](03-toolchains.md)。 +见[第 03 章](20-toolchains.md)。 ### 而依赖图会整个替换这一轴 @@ -365,7 +373,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— `linux-x86_64` 够得着,从 `linux-aarch64` 够不着;`aarch64-linux-gnu` 是镜像的 情形,两台上都是 `planned`。把它们并成 `linux`,一台会把另一台的行覆盖掉。 -### 哪台构建机服务哪个目标 +### 构建机与它服务的目标 | target | tier | pin | linux-x86_64 | linux-aarch64 | macos-arm64 | windows-x86_64 | |---|---|---|---|---|---|---| @@ -383,6 +391,13 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— | `riscv32-none-elf` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | | `aarch64-none-elf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | | `x86_64-none-elf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv6m-none-eabi` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv7m-none-eabi` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv7em-none-eabi` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv7em-none-eabihf` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | +| `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | 载荷 | 载荷 | 载荷 | 载荷 | `载荷` 这里有工具链载荷产出它 · `图` 没有载荷,但依赖可以供给系统 · `系统` 在机器上被找到,不是 mcpp 装的 · `SDK` 平台自己的 · @@ -390,7 +405,7 @@ CRT;图供给时是 `musl`。一个目标字符串,两个不同的 C 库 —— ### 列背后的规则 -| 目标类别 | 哪些构建机服务它 | 为什么 | +| 目标类别 | 服务它的构建机 | 原因 | |---|---|---| | `*-linux-musl` | Linux(任意架构)、Windows(仅同架构) | musl 载荷是自足的 | | `*-linux-gnu` | Linux,且仅同架构 | 还要本机架构的 `xim:glibc` / `xim:linux-headers` | @@ -443,10 +458,18 @@ runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic", `sysroot = ""` 选定零 libc 档:编译行上没有 C 库,链接上也没有。 **缺席 `sysroot` 键是另一个答案** —— 它继承该目标行自己的默认值。 -见[第 13 章](13-baremetal.md)。 +见[第 13 章](40-baremetal.md)。 ## 参考 -[第 14 章](14-target-side.md)讲五层以及每一层由谁供给。 -[第 15 章](15-openkal-cross.md)完整讲构建期体系。 -[第 03 章](03-toolchains.md)讲工具链轴,它是分开的:目标不决定编译器。 +[第 14 章](22-target-side.md)讲五层以及每一层由谁供给。 +[第 15 章](24-openkal-cross.md)完整讲构建期体系。 +[第 03 章](20-toolchains.md)讲工具链轴,它是分开的:目标不决定编译器。 + +## 当前边界 + +- 处于 `planned` 档位的行,含义是**词汇表里已登记,而还没有任何东西接线**。点名这样 + 的目标会被解析器接受、被构建拒绝。 +- `host_can_serve` 判断的是「这台机器能不能产出这个目标」,与档位是两个不同的问题。 + mcpp 支持的目标仍可能从这台宿主够不着;出口是显式的 + `[target.X] toolchain = "…"`。 diff --git a/docs/zh/14-target-side.md b/docs/zh/22-target-side.md similarity index 60% rename from docs/zh/14-target-side.md rename to docs/zh/22-target-side.md index 2edb4304..0bf0a674 100644 --- a/docs/zh/14-target-side.md +++ b/docs/zh/22-target-side.md @@ -1,4 +1,13 @@ -# 目标侧 +# 22 —— 目标侧 + +**读者:**要用同一份 manifest 服务多个目标的作者。 + +**本章回答的那一个问题:**manifest 怎样说「只在那里」,以及哪些东西可以这样被 +条件化。 + +**不在这里:**目标名的词汇表,那是 [21 —— 目标三元组](21-the-target-triple.md); +以及加速器这条轴 —— 它在图之后才解析,属于 +[42 —— 异构硬件构建](42-heterogeneous-builds.md)。 一次构建在发出任何命令行之前必须回答一个问题:目标的编译器运行时、平台接口、 C 库与 C++ 运行时从哪里来。mcpp 在依赖图解析完成之后解析该问题一次, @@ -73,7 +82,7 @@ C 库、平台接口与 C++ 运行时是互斥的选择,而非可叠加的贡献 引擎仅在两层来自不同来源时为它们接线。 -| 组合 | 关系由谁表达 | 引擎 | +| 组合 | 关系的表达方 | 引擎 | |---|---|---| | 两层均来自 `graph` | 包之间的普通依赖 | 不介入 | | 两层均来自 `payload` | 载荷自身一致 | 不介入 | @@ -281,3 +290,124 @@ mcpp 不认识的键 —— 打错的字,或来自更新版本 mcpp 的谓词 该规定只约束此后的引擎。一个包若声明某个层名, 其使用者仍须运行不早于该层名被引入的那个版本。 + +## `[target.*]` —— 平台条件依赖与 flag + +用 `[target.]` 表把依赖与构建 flag 限定到某个平台。选择器 `` 有三种形式: + +| 选择器 | 含义 | 示例 | +|---|---|---| +| **裸 OS 别名** | 单个 OS / 族 —— 简洁且常用的形式 | `[target.windows]`、`[target.unix]` | +| **`cfg(...)` 谓词** | 复合条件(arch / env / 组合子) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | +| **精确三元组** | 某个具体目标(同时承载 `toolchain` / `linkage` / `sysroot` / `runner`,见 [04 §2.7.3](04-mcpp-toml.md)) | `[target.x86_64-linux-musl]` | + +一个选择器可以承载平台条件的**依赖**与**构建 flag**: + +```toml +# 简洁的裸别名形式 —— 仅在 Windows 上拉取并链接 OpenBLAS。 +[target.windows.dependencies.compat] +openblas = "0.3.33" +[target.windows.build] +ldflags = ["-Llib", "-llibopenblas"] + +# cfg(...) 用于复合谓词(文法:all/any/not 作用于 os/arch/family/env, +# 以及裸别名 windows/unix/linux/macos)。 +[target.'cfg(all(linux, not(arch = "aarch64")))'.build] +cxxflags = ["-march=x86-64-v2"] +``` + +`[target.windows]` 与 `[target.'cfg(windows)']` 完全等价 —— 裸别名 +`windows` / `linux` / `macos` / `unix` 都不是合法的目标三元组,因此不存在歧义。 +单个 OS/族用裸形式,arch/env 条件与组合子用 `cfg(...)`。 + +- **可用键**:`dependencies` / `dev-dependencies` / `build-dependencies` / + `feature-deps.`(mcpp 2026.8.6.2+ —— 见 [30 —— build.mcpp](30-build-mcpp.md);feature 本身无条件注册, + 只有它的依赖集合受限定),以及带 `cflags` / `cxxflags` / `ldflags` / `sources` + 的 `build`(mcpp 0.0.95+ —— 条件源码 glob,例如把 `src/x86/**/*.asm` 收在 + `cfg(arch = "x86_64")` 之后;`!` 排除 glob 在此同样有效),再加 `flags` 与 + `include_dirs` / `include_dirs_after`(mcpp 0.0.102+), + 以及 `private_include_dirs` 与 `std-module-flags`(mcpp 2026.9.1.1+)。 +- **`build` 接受的恰好是*可叠加的构建输入*集合** —— 那些以追加方式合并、 + 并在谓词求值之后被消费的东西,也就是 `BuildInputs` 的成员表。`linkage`、`target` + 与档案开关刻意不在其中:它们是**目标选择的输入**(用一个针对 `target` 求值的谓词 + 去条件化 `target` 是循环的),或者需要覆盖而非追加的语义。 + 集合之外的键会被报出并忽略;消息里列出的正是它比对用的那份集合,因此不会与检查漂移。 +- **按解析后的目标求值** —— 交叉构建取 `--target` 三元组,否则取宿主。因此原生 + Linux 构建**根本不会下载** `[target.windows]` 依赖。 +- **谓词的键**:`os`、`arch`、`family`、`env` —— 三元组的坐标 —— 以及自 mcpp + 2026.9.1.1 起的五个目标侧层名 `compiler`、`compiler-runtime`、`kernel-abi`、 + `c-abi`、`c++-abi`(见[22 —— 目标侧](22-target-side.md))。`accelerator` 同样 + 是这里的键,由本次构建自己的 `accel`(`--accel` 或 `[build] accel` 里的后端名) + 回答,因此它是对一个集合的成员判定;`accelerator = "none"` 则是一段用来说 + 「本次构建没有命名任何后端」的写法,而不必枚举它不是的那些后端。裸词 + `linux` / `macos` / `windows` / `unix` 是对应 `os` / `family` 判定的糖。 + 集合之外的键会被报成一条 schema 警告,且该段不生效 —— 它过去静默地求值为假, + 而那与「这一段本就不该匹配」读数完全相同。 +- **被解析的层的谓词不能选择依赖。** 层是**从**依赖图解析出来的,因此由它选出的 + 依赖会决定它正在询问的那个答案。`[target.'cfg(c-abi = "musl")'.dependencies]` + 会被报出并忽略;同一谓词下的 `build` 输入照常生效。`accelerator` 不在此列 + (mcpp 2026.9.6.5):它是构建的输入而不是图给出的答案,所以 + `[target.'cfg(accelerator = "cuda")'.dependencies]` 生效。 +- **优先级**:精确三元组表胜过 `cfg`/别名表;多个命中的谓词表,其 flag 按序拼接。 + 条件项追加在无条件 `[build]` 项**之后**,因此在 GNU「最后一个 flag 生效」的 + 规则下,条件规则会覆盖更宽的无条件规则。这正是让按 OS **移除**成为可表达的原因: + + ```toml + [build] + flags = [{ glob = "third_party/zlib/**", defines = ["HAVE_UNISTD_H=1"] }] + + # clang-MSVC 没有 :撤销基础 define,加上 windows 的那个。 + [target.'cfg(windows)'.build] + flags = [{ glob = "third_party/zlib/**", + defines = ["NO_FSEEKO"], cflags = ["-UHAVE_UNISTD_H"] }] + ``` + +- **未命中当前目标的条件 `flags` 条目根本不存在**,因此它不会产生 + 「glob 未匹配到任何源文件」的警告。于是一份 manifest 可以同时携带三个 OS 的 + flag 表,而不会在另外两个上制造噪声 —— 与未启用 feature 的条目根本不存在是 + 同一个道理。**无条件**表里的零命中 glob 仍然告警,因为那里它是真实缺陷。 +- **`toolchain` / `linkage` / `sysroot` 仅限精确三元组** —— 它们描述某一个具体的交叉目标, + 因此写在 `[target.]` 下(见上),而不是裸别名或 `cfg(...)` 下。 + +### `sysroot` —— 目标的 C 库 + +`sysroot`(mcpp 2026.8.20.2+)覆盖目标表为某个三元组绑定的 C 库,与 `toolchain` +覆盖编译器 pin 同轴:一个指名目标所解析的编译器,另一个指名它的 C 库,两者在工程 +有理由与之分歧之前都只由引擎决定。 + +```toml +[target.riscv64-none-elf] +sysroot = "xim:newlib-riscv@4.4" # a different C library +``` + +```toml +[target.riscv64-none-elf] +sysroot = "" # no C library at all +``` + +**键缺席与键为空是两个不同的答案。** 缺席继承目标表的 C 库。存在且为空是 +**零 libc 档**:不解析任何 C 库,不加入头文件与库目录,链接行上只有工程与其依赖 +提供的内容,`#include ` 不再解析。内核与 bootloader 要的正是这一档,而把 +两种情形合并会让这类工程静默地把目标的 C 库拿回去。 + +取值是 xpkg 引用或空字符串;裸名在解析清单时即被拒绝,因为接受它会导致什么都不安装, +然后在很晚的时候以「缺少 libc」失败。 + +构建程序可以询问供给 sysroot 的是哪个 C 库**载荷**:`mcpp::target_libc()` 返回该包的 +名字,`mcpp::target_libc_profile()` 返回目标 ISA 档位对应的子目录。零 libc 档上两者均 +为空。 + +**这与「目标侧解析出的 C 库是哪一个」不是同一个问题。** `target_libc()` 命名的是 +mcpp 装上的那个载荷,而这个值是目标侧解析的一项**输入** —— 依赖图里的包可以改为供给 +C 库,那时解析出的 `c-abi` 就不是这里返回的东西。要按已解析的层分支,请用层谓词: +`[target.'cfg(c-abi = "musl")'.build]`(见[22 —— 目标侧](22-target-side.md))。 +这一段在 2026.9.1.1 之前写的是「解析到的是哪份 C 库」,那是两者里错的那一个。 +参见[40 —— 裸机与 freestanding 目标](40-baremetal.md)。 + +## 当前边界 + +- **依赖不能以加速器为条件。** 加速器这一层是从依赖图解析出来的,因此由它选择的依赖 + 会决定它自己在问的那个答案。mcpp 会报告该谓词并忽略它;包要么无条件、要么以平台为 + 条件,而由加速器选择的是 `[build] sources`。 +- 一般而言,层也不能选择依赖,理由相同:任何早于图的推导,都是在推断一个尚不存在的 + 事实。 diff --git a/docs/zh/23-the-project-environment.md b/docs/zh/23-the-project-environment.md new file mode 100644 index 00000000..264e2b85 --- /dev/null +++ b/docs/zh/23-the-project-environment.md @@ -0,0 +1,445 @@ +# 23 —— 项目环境 + +**读者:**构建需要「编译器之外的工具」的作者。 + +**本章回答的那一个问题:**工程怎样声明它的构建所处的环境,以及这条声明决定什么。 + +**不在这里:**构建程序拿这些工具做什么,那是 [30 —— 构建程序](30-build-mcpp.md); +以及选择编译器,那是 [20 —— 工具链管理](20-toolchains.md)。 + +项目可以声明自己在哪个环境里构建。这一条声明决定项目链接哪个 C 库、以及它的 +构建程序找到哪些工具——于是同一份 `mcpp.toml` 在开发机和 CI 上是同一个构建, +不论这两台机器上还装了别的什么。 + +```toml +[xlings] +subos = "tools" + +[xlings.workspace] +"xim:qemu-riscv" = "9.2.4-1" +``` + +可运行的工程:`examples/07-project-subos/`。 + +## 1. SubOS 的定义 + +SubOS 是一个目录,里面是一份用户态:它自己的 `bin`、自己的库视图、自己那套已 +装包版本,以及一个自述用的 `subos_info` 块。mcpp 把它当作「这个项目对着什么 +构建」的答案,而且是回答这个问题的唯一机制——不是编译器所在路径,不是 +`XLINGS_ACTIVE_SUBOS`,也不是当前 shell。 + +存在两种,区别在于目录落在哪里: + +| 声明 | 目录 | 共享范围 | +|---|---|---| +| 未声明 | mcpp 初始化的 `subos/default` | 机器上的每个项目 | +| `subos = "default"` | 同一个目录,只是被显式点名 | 机器上的每个项目 | +| `subos = ""` | `/.mcpp/.xlings/subos//` | 不共享 | + +第三行是隔离的那种。它属于该项目,就放在清单旁边,删掉项目它也随之消失。 + +## 2. 这条声明的决定范围 + +**C 库。** payload-first 的构建链接的是某一个确定的 glibc,而「哪一个」是项目 +的性质而非机器的性质。第 8 章讲绑定本身、降级规则,以及一个不自述的 SubOS 会 +让这些规则变成什么。 + +**构建程序看见哪些工具**(mcpp 2026.8.25.1+)。被声明环境的 `bin` 放在 +`build.mcpp` 运行时 `PATH` 的最前面: + +``` +PATH=<被声明环境的 bin>: +``` + +因此构建程序里把 `qemu-system-riscv64` 写成裸名,拿到的就是那个环境里的副本。 +这条通道的契约见第 7 章。 + +**只对声明了的项目生效。** 没有 `[xlings].subos` 的项目拿到的是 mcpp 启动时 +的 `PATH`,逐字节不变。把一个共享目录放到每个项目前面,会让「构建看见什么」 +取决于这台机器上还装过什么——同一台机器上的两个项目彼此一致,而同一个项目在 +两台机器上不一致。是声明本身把它放到了前面。 + +**前置而非替换。** 构建程序理应会调 `git`、`python3` 或 shell,这些都不在 +SubOS 里。前置让被声明的环境成为默认答案;其余的仍在它后面可达。 + +### 2.1 生效的版本钉(2026.9.3+) + +指名一个环境,同时改变了工具的版本从哪来。工程自己 `[xlings.workspace]` 里的条目 +总是胜出;不同的是它们叠在什么之上: + +| 工程声明了 | 它没点名的工具,版本来自 | +|---|---| +| `[xlings.workspace]`,无 `subos` | 机器的环境 | +| `[xlings.workspace]` 与 `subos = "<名>"` | 那个环境自己的 workspace;机器的不适用 | + +第二行就是隔离的含义。指名的环境有自己的已安装集合,把机器的版本带进去会指向那里 +不存在的版本 —— 所以原本依赖「机器上装了就能用」的工程,一旦指名环境,就必须把用到 +的都声明出来。 + +在工程内执行的 `xlings use` 压过这两者,直到 mcpp 重写环境为止:它是最后合并的那 +一层,而人做出的动作应当压过一份文件。 + +## 3. 这条声明不决定的部分 + +`[xlings.workspace]` 声明的是「环境里要有哪些包」,而每个包的载荷目录另有通道交付, +即 `MCPP_XPKG__DIR`。这与 `PATH` 是两个问题,答案也保持分开:需要某个包 +的数据文件(比如 protoc 自带的 well-known `.proto`)的构建程序问目录,需要 +**运行**某个程序的构建程序问 `PATH`。 + +**工作区成员的声明不是工作区的声明。** 工作区构建中由工作区根持有这个选择;成员的 +`[xlings]` 只在该成员作为独立根被构建时生效。 + +**依赖的声明是另一回事,而且它被采纳**(`[xlings] deps` 自 2026.9.5.4,下面那条版本 +规则自 2026.9.6.6)。板级支持包知道哪个模拟器够得到它那台机器,规则包知道它驱动哪个 +工具包;要消费者把这些再写一遍,正是这类包存在的意义所反对的重复。依赖声明的东西会被 +装上,而 `MCPP_XPKG__DIR` 在那个依赖自己的构建程序里为它作答。 + +工程与依赖命名**同一个包**时,只装它的一个版本:身份是 `(namespace, name)`,版本是这个 +包上的约束。离产物更近的声明赢,并且覆盖会被报出来;不满足对方所陈述之要求的钉会被拒绝 +并点出两侧。见 [23 — The Project Environment](23-the-project-environment.md) 的「一个包一个版本」。 + +## 4. 只读取环境,从不创建环境 + +mcpp 解析被声明的名字,并读取它找到的东西。解析不到的名字是硬失败: + +``` +error: selected SubOS 'tools' does not exist at …/.mcpp/.xlings/subos/tools; +create/bootstrap that environment instead of falling back to active/default +``` + +回退到 default 或回退到当前活跃的那个,等于用另一个环境顶替清单点名的那个,而 +这恰恰会让一份 `mcpp.toml` 意味着两个不同的构建。创建并填充 SubOS 属于 xlings +这一层——`xlings subos new`——mcpp 去管理 SubOS 状态则是把分层倒置。 + +一个存在但**不携带 `subos_info` 块**的环境是**降级而非失败**:运行时绑定报 +`inconclusive`,没有 payload-first 绑定可用,打印一条提示,构建继续。完整规则见 +第 8 章。 + +## 5. 采用私有环境的条件 + +- **产物取决于版本的代码生成器。** `protoc`、`flatc`、着色器编译器:它的输出是 + 下游一切的输入,所以项目钉住生产者,而不是指望机器上那个恰好兼容。 +- **构建程序要运行的模拟器。** 若干裸机包把「在 QEMU 里启动产物」作为验证的一 + 部分;是哪个 QEMU 属于「验证了什么」的一部分。 +- **CI 与开发机不一致的项目**,两边都没错,而构建不该察觉到差异。 +- **同一台机器上两个项目需要同一工具的不同版本。** 共享目录意味着必有一方落 + 败;私有环境让这个问题不成立。 + +代价一侧:隔离环境是一个必须被创建并填充的目录,而这笔账由首次构建来付。 +2026.8.29 起 mcpp 会做这件事 —— 声明在 `[xlings.workspace]` 里的包在首次使用时被供给, +一个尚不存在的具名 `[xlings] subos` 会被创建而不是被拒绝 —— 但代价是实打实的: +干净机器上的第一次构建会先下载安装,然后才编译。工具很普通、版本也无所谓的项目, +不声明、直接继承机器的那份更划算。 + +在 `--offline` / `MCPP_OFFLINE` 或 `MCPP_NO_AUTO_INSTALL` 下,mcpp 转为拒绝而不是 +安装,并列出包名以便手动供给 —— 与 `[toolchain]` 遵守的是同样两个开关,理由也相同: +一次没被要求的下载,不该由构建替工程决定。 + +这份声明在每台构建本工程的宿主上都会供给,宿主装不了的包是错误,不是被跳过的条目。 +只存在于某一个宿主平台的工具因此按平台声明(2026.9.2.1): +`deps = [{ linux = "qemu-user-aarch64" }]` 在 Linux 上声明这个模拟器,在别处什么都不声明。 +键与解析规则见本章。 + +**哪些命令会安装它。** 一条条目可以带档位 —— `{ version = "0.24.0", when = "run" }` —— +`[feature-xlings.]` 则把工具挂在某个 feature 上。用不到的工具因此不会被下载: +见本章。不写档位就是从前的行为。 + +**runner。** `[xlings.workspace]` 下的程序也是 `[target.].runner` 查找其第一个元素 +的首选位置,在 `PATH` 之前([04 §2.7.3](04-mcpp-toml.md))。两个键合起来,在 CI 宿主上供给用户态模拟器, +并通过它执行交叉构建的产物,而清单不必写出载荷的路径。 + +## 6. 应当写在别处的声明 + +| 需求 | 写入位置 | +|---|---| +| 程序链接的库 | `[dependencies]` | +| 编译器 | `[toolchain]`,第 3 章 | +| 依赖产出的宿主工具 | `tools = [...]`,第 7 章 | +| 环境里要有的工具 | `[xlings.workspace]` | +| 只有某个命令或某个 feature 需要的工具 | `when = "run"`、`[feature-xlings.]` | +| 用哪个环境 | `[xlings] subos` | + +## 7. `[xlings]` —— manifest 键 + +```toml +[xlings.workspace] # 这个工程的环境里有什么 +cmake = "3.28" +"xim:picolibc-riscv" = "1.8.12" # 带命名空间的包 —— 必须带引号 +code = "" # 存在即可,版本不限 +llvm = { macosx = "20", default = "22" } +``` + +```toml +[xlings] +subos = "dev" # 指名的隔离环境 +``` + +`[xlings]` 是 mcpp 对 **xlings local project 机制**的书写面:让一个目录拥有自己 +环境的那份项目 `.xlings.json`。子段名与含义都是那份文件的,mcpp 原样物化进 +`/.mcpp/.xlings.json`,没有翻译层。 + +**`[xlings.workspace]` 是唯一的表。** 一条条目写出工程用哪个包、用哪个版本。 +mcpp 既供给它——机器上没有就装,有就映射——也把它物化成解析用的钉,于是工程写下 +的版本就是它的工具解析到的版本。 + +### 条目的形式 + +| 形式 | 含义 | +|---|---| +| `cmake = "3.28"` | 该版本 | +| `llvm = "22"` | 已装的最高 `22.*`;版本前缀会被解析 | +| `code = ""` | 存在即可,版本不限 | +| `"xim:picolibc-riscv" = "1.8.12"` | 来自 `xim` 索引的包 | +| `llvm = { macosx = "20", default = "22" }` | 按宿主平台 | + +**带命名空间的包写成 `"<命名空间>:<名字>" = "<版本>"`,引号必需** —— TOML 的裸键 +不能含冒号。这是**推荐形态,也是所有官方包使用的形态**:一条条目先点名一个包, +再说用它的哪个版本,所以命名空间属于名字。 + +命名空间写在版本上(`picolibc-riscv = "xim:1.8.12"`)同样接受,因为物化出来的 +`.xlings.json` 里正是那种形态 —— 那里的键是 xvm target,scope 限定的是版本。 +两套词汇,同一条条目。两半都写且不一致是错误;同一个包用两种拼法出现两次也是错误。 + +平台键是 xlings 自己的 —— `linux`、`macosx`、`windows`,外加 `default`。`macos` +与 `macosx` 是同一个平台的两套词汇(mcpp 的三元组说前者,描述符与 xlings 的项目 +文件说后者),**凡是点名平台的地方两者都接受**。表里既没有本机这一项也没有 +`default`,就表示在这里什么都不声明。 + +### 两条解析轴 —— 宿主与目标(mcpp 2026.9.6.4+) + +一条工具条目回答的是两个不同问题中的一个,写在哪张表里决定了是哪一个: + +| 写法 | 轴 | 解析依据 | +|---|---|---| +| `[xlings.workspace]`,平台键写在值里 | 宿主 | 跑这次构建的机器 | +| `[target..xlings.workspace]` | 目标 | 解析后的目标(`--target`,否则是宿主) | + +两种写法都是正确的,谁也不取代谁。在构建机上执行的工具属于宿主轴;产物编译或链接 +时对着的载荷属于目标轴。 + +```toml +[xlings.workspace] +"xim:dpcpp" = "7.1.0" # 一个编译器,它在本机上跑 + +[target.'cfg(os = "linux")'.xlings.workspace] +"xim:glibc" = "" # 设备单元编译时对着的东西 +"xim:linux-headers" = "" +``` + +非交叉构建时两条轴指向同一个平台,所以把目标事实写在宿主轴上的工程是碰巧正确的, +而且照常工作。它在第一次被交叉构建时不再正确。**凡是产物编译或链接时对着的东西, +推荐写在目标轴上。** + +`[target..feature-xlings.]` 把条件与门组合起来,与 +`[target..feature-deps.]` 同形:selector 说的是哪些目标, +feature 说的是要不要。 + +```toml +[target.'cfg(os = "linux")'.feature-xlings.backend-vulkan] +"xim:shaderc" = "2026.3" +``` + +**这里的 selector 禁止命名被解析的层。** `c-abi`、`c++-abi`、`compiler`、 +`compiler-runtime`、`kernel-abi` 由依赖解析回答,而依赖解析发生在工具安装之后、 +构建程序运行之后。按这些层条件化的工具会被声明却永远装不上——构建照常成功,工具 +就是不在——所以这样的 manifest 会被拒绝,并把工具与谓词都点出来。改成按目标条件化, +或者用 feature 做门:`[feature-xlings.]` 在任何东西被供给之前就已知。 + +**`accelerator` 是例外,它被接受**(mcpp 2026.9.6.5)。它不由任何东西解析而来: +它是 `--accel`,或 `[build] accel`,在查找第一个包之前就已读入。以它为谓词的载荷 +与三元组谓词在同一趟合并,并像其它载荷一样被安装。 + +```toml +[target.'cfg(accelerator = "cuda")'.xlings.workspace] +"xim:cuda-nvcc" = "12.9.86" +"xim:cuda-cudart" = "12.9.79" +``` + +带设备孤岛的工程应当用这种写法。没有它,厂商工具包只能无条件声明或者干脆不声明, +于是不带加速器的 `mcpp build`——最便宜的那次构建,也是 CI 通常跑的那次——会为一个 +它根本没在编译的设备下载数 GB。 + +依赖同理:`[target.'cfg(accelerator = "cuda")'.dependencies]` 生效,而以被解析的层 +为条件的依赖不生效,因为后者会决定它正在询问的那个答案。加速器这条路径上没有任何 +循环。 + +条件只写在 selector 一处。selector 之下的值如果又带平台键,就是同一件事说了两遍, +会被拒绝,并把两半都指出来: + +``` +[target.cfg(os = "linux").xlings.workspace] xim:tool: the value carries platform +keys (linux, macosx), but [target.cfg(os = "linux")] already says which targets +this applies to. +``` + +`subos` 不按目标条件化:一个工程只有一个环境,所以 `[target..xlings]` +拒绝这个键,而不是把它丢掉。 + +**已发布的描述符不为目标轴条目携带边**,`mcpp publish` 会说明这一点。描述符按平台 +分块,而 selector 不是平台 —— `cfg(target_arch = "aarch64")` 不对应那份文件里的任何一块。 +**使用者**装到的东西来自顶层 `[xlings.workspace]`;目标轴对"本包自己的构建对着什么" +仍然是正确的。 + +这两条轴所属的一般规则见 [SPEC-004](../specs/manifest-semantics.md)。 + +### `when` —— 需要这个工具的命令(mcpp 2026.9.4.2+) + +```toml +[xlings.workspace] +"xim:qemu-arm" = "9.2.4-1" # 每次构建都装,与从前一样 +"xim:codegen" = { version = "1.0", when = "build" } +"xim:probe-rs" = { version = "0.24.0", when = "run" } +"xim:clang-tidy" = { version = "20", when = "dev" } +``` + +包依赖从一开始就有这条轴 —— `[dependencies]`、`[build-dependencies]`、 +`[dev-dependencies]`。工具只有一张表,于是一个同时点名模拟器与调试探针的板级支持包 +会把两个都装给每一位消费者,包括只想把库编出来的那一位。 + +| `when` | 安装方 | 传播到消费者 | +|---|---|---| +| *(不写)* | 每个构建命令 | 是 | +| `build` | 每个构建命令 | 是 | +| `run` | `mcpp run`、`mcpp test` | 是 | +| `dev` | 只有声明它的那个包作为根时 | **否** | + +**不写 `when` 就是 2026.9.4.2 之前的行为**,所以没有任何清单需要改。收窄是可选动作, +不是作者必须回答的新问题。 + +`dev` 是唯一不传播的一档。它的含义是「声明它的那个包自己在被开发时」,所以依赖的 +`dev` 条目永远不会为消费者安装。其余各档都会到达消费者 —— 这正是板级包知道自己机器 +的意义:它声明一次模拟器,每一位消费者都拿得到。 + +档位写在**条目**上而不是另开一张表,理由与 `[dependencies]` 同时接受 `dep = "1.0"` +和 `dep = { version = "1.0", features = [...] }` 是同一条。带档位的条目**必须**写出 +`version`,哪怕留空(`version = ""` 表示「存在即可,版本不限」)—— 否则 +`{ when = "run" }` 与写错的 `version` 键无法区分。 + +### `[feature-xlings.]` —— 某个 feature 才需要的工具 + +```toml +[features] +default = ["emulator"] +emulator = {} +hardware = {} + +[feature-xlings.hardware] +"xim:probe-rs" = "0.24.0" +``` + +同一张表,按 feature 门控,拼法沿用 `[feature-deps.]`。**不要 `hardware` +的消费者永远不会下载探针驱动。** 这里的条目同样接受 `when`。 + +`[features]` 里没有声明过的 feature 名会作为 schema 警告报出:它对谁都不激活、什么 +都不装,而这种工具的缺席只表现为「设备就是连不上」,是最难诊断的一种。 + +### 规则包自带它的环境(2026.9.6.6+) + +上面那张表是工程**有主张**时写的。多数工程没有主张,写下的也就是空无一物: + +```toml +[build-dependencies.mcpp] +plugins = { version = "0.3.0", features = ["rules-cuda"], host-module = true } +``` + +这一条边就是全部声明。规则包在选中它的那个 feature、它所服务的加速器之下,声明自己 +需要哪些包、最低到哪一版: + +```toml +# 写在规则包里,不写在你的工程里 +[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] +"xim:cuda-nvcc" = ">=12.9.86" +"xim:cuda-cudart" = ">=12.9.79" +``` + +**两重门,都要开。** feature 说「要不要这个规则」,selector 说「哪些构建真的下载」。 +同一个工程的 CPU-only 构建两道门都不过,一个字节都不装。 + +「需要哪些包、最低到哪一版」是规则作者的知识。在每个用它的工程里重写一遍,是一份会 +悄悄过期的副本——规则动了,而那些工程不会跟着动。 + +### 一个包一个版本(2026.9.6.6+) + +工具地址是 `[:][@<版本>]`,它的**身份是 `(namespace, name)` 二元组**。版本是 +这个包上的约束,不是它名字的一部分,所以 `xim:glibc`、`xim:glibc@2.40` 与 +`xim:glibc@>=2.38` 指的是同一个包。**一次构建只装它的一个版本。** + +装哪一个,分两步决定。 + +**裁决——离产物更近的声明赢。** 工程压过它依赖的包,于是一个钉覆盖规则的要求: + +```toml +# 工程侧,当它确实有主张时 +[target.'cfg(accelerator = "cuda")'.xlings.workspace] +"xim:cuda-nvcc" = "13.3.33" +``` + +**不带版本的声明弃权:** 它陈述了「要这个包」而没有陈述「要哪一版」,因此不会仅仅 +因为更近就压过一条下界。两条声明都带版本且不一致时,mcpp 会报出用了哪一条——一个 +只能表现为「声明了两个版本而目录里有一个」的覆盖,是要读者自己去文件系统里重建的 +事实。 + +**校验——赢家必须满足每一条落败的要求。** `>=`、`^`、`~` 以及逗号组合是**要求**。 +不满足的钉被拒绝,并同时点出两侧: + +``` +error: `xim:cuda-nvcc` is pinned to 12.0.0 by this project, and mcpp:plugins + requires >=12.9.86. + One version of a package is installed, so the two cannot both hold. + fix: pin a version satisfying >=12.9.86, or drop the pin and let the + requirement decide. +``` + +裸版本是**选择**而不是要求:两条互不相同的精确钉走裁决并被报告,不被拒绝。只有被 +陈述出来的要求才谈得上违反。 + +**这是一次比较,不是一次搜索。** 版本由裁决选定、再被检查,所以 mcpp 从不需要问索引 +「有哪些版本」,也就不带约束求解器。代价被写出来而不是藏起来:一个求解器本可满足的 +组合——工程写 `>=8.0`、规则写 `8.5.0`、而索引里最新是 8.3——会被拒绝,而拒绝消息里 +写着怎么往下走。 + +范围在两个方向上都被求解。`>=2026.1` 装到满足它的最高已发布版本,`>=2099.1` 作为 +不可满足被拒绝,而 `mcpp::xpkg_dir` 回答满足该范围的最高**已安装**版本——声明了下界 +的规则找得到下界带进来的东西。 + +### 工程没点名的工具,其版本的来源 + +| 工程声明了 | 版本来自 | +|---|---| +| `[xlings.workspace]`,无 `subos` | 机器的环境,工程自己的条目叠在上面 | +| `[xlings.workspace]` 与 `subos = "<名>"` | 那个环境自己的 workspace;机器的不适用 | +| 两者都没有 | 机器的环境 | + +中间那行不是遗漏。指名的环境有自己的已安装集合,把机器的版本带进去会指向那里不 +存在的版本。**写 subos 就是要隔离,不写就是要机器的环境加上自己的条目。** + +在工程内执行的 `xlings use` 压过这张表,直到 mcpp 重写环境为止——它是最后合并的 +那一层。 + +### `deps`,已被取代 + +`deps = ["xim:qemu-riscv@9.2.4-1"]` 是同一句话在 2026.9.3 之前的拼法。它仍然生效, +并且会被报告一次,同时给出该写的 `[xlings.workspace]` 那一行。**不拒绝**——拒绝会 +落到**依赖**的 manifest 上,而钉了那个包精确版本的工程改不了它。 + +### `envs`,已移除 + +`[xlings.envs]` 曾被物化进 `.xlings.json`,而没有任何东西读它:程序的环境由它自己 +的包声明,环境的环境由那个环境声明。现在这个键是错误,并同时点名这两者。索引里没有 +任何包用过它。 + +## 8. 相关章节 + +- [30 - build.mcpp](30-build-mcpp.md) —— 构建程序收到的契约,含它运行时的 + `PATH`。 +- [91 - 工具链内部](91-toolchain-internals.md) —— 运行时选择、`RuntimeBinding` + 快照与降级规则。 +- [04 - mcpp.toml](04-mcpp-toml.md) —— manifest 的其余部分。 + +## 当前边界 + +- **工具不能以加速器为条件。** 加速器在依赖图之后才解析,因此这样的工具会被声明而 + 永远装不上 —— 一次「工具干脆不在」却仍然成功的构建。写了这种条件的 manifest 会被 + 拒绝。 +- 离产物更近的那条声明胜出,且覆盖会被报告。不满足另一方所述要求的钉子会被拒绝并 + 点名两边,而不是与之并存安装。 diff --git a/docs/zh/15-openkal-cross.md b/docs/zh/24-openkal-cross.md similarity index 94% rename from docs/zh/15-openkal-cross.md rename to docs/zh/24-openkal-cross.md index 71a603dc..632291dc 100644 --- a/docs/zh/15-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -1,4 +1,12 @@ -# 基于 openkal 的交叉构建 +# 24 —— 基于 openkal 的交叉构建 + +**读者:**从本机为另一个操作系统做交叉构建的人。 + +**本章回答的那一个问题:**一份源码树怎样为多个 hosted 目标构建,而本机并没有它们 +的工具链。 + +**不在这里:**没有操作系统的目标,那是 [40 —— 裸机](40-baremetal.md);以及目标 +怎样命名,那是 [21 —— 目标三元组](21-the-target-triple.md)。 传统的交叉构建由载荷承担。一份工具链为一个目标而构建,它的驱动只有一个答案, 到达第二个目标意味着获取第二份工具链。因此一个发行方必须发布的载荷数, @@ -58,7 +66,7 @@ mcpp build --target x86_64-windows-gnu 一个工程命名其中最后一个。其余由它的依赖推出。 -## 编译器为何必须是 LLVM +## 编译器必须是 LLVM 的原因 `openkal-llvm-runtime` 把这项要求声明出来,而不是留待被发现: @@ -77,7 +85,7 @@ fatal error: __config: No such file or directory 有了这项声明,构建在编译任何东西之前拒绝该组合, 并指出选择一个满足它的编译器的那条命令。 -## 目标如何被选定 +## 目标的选定 mcpp 自身词表的目标行可以携带一条工具链约定。该约定命名的是 **供给该目标 C 库的那份载荷**,并且仅在两个条件同时成立时生效: @@ -178,7 +186,7 @@ runner = ["qemu-aarch64-static"] 没有这个键时,`mcpp run` 报告内核的拒绝(`Exec format error`)与应当写的键,`mcpp test` 把每个测试报告为未运行并以 2 退出。能原生执行该产物的宿主传 `--no-runner`。规则见 -[5 —— mcpp.toml](05-mcpp-toml.md) §2.7.3。 +[04 —— mcpp.toml](04-mcpp-toml.md) §2.7.3。 ### 源码是同一份,程序不是 @@ -225,7 +233,7 @@ UEFI 应用是 PE/COFF,经微软 x64 调用约定进入。两者都是 LLVM 工 作为一个跨若干指令集的接口呈现,每个指令集一个后端包。 一个内核依赖它,并供给自己的平台层,或者不供给。 -### x86_64 裸机为何需要引擎侧的工作 +### x86_64 裸机需要引擎侧工作的原因 `riscv64-none-elf` 与 `aarch64-none-elf` 是表中的行,除此之外别无他物: Clang 对两者都有 BareMetal 工具链,自行驱动它们的链接并到达 `ld.lld`。 @@ -257,5 +265,6 @@ g++: error: unrecognized command-line option '-fuse-ld=…/ld.lld' ## 参考 -[docs/14 — 目标侧](14-target-side.md) 给出五个层、四种来源与规则。 +[docs/14 — 目标侧](22-target-side.md) 给出五个层、四种来源与规则。 [SPEC-002](../specs/target-side.md) 给出能力语法的规范性陈述。 + diff --git a/docs/zh/07-build-mcpp.md b/docs/zh/30-build-mcpp.md similarity index 77% rename from docs/zh/07-build-mcpp.md rename to docs/zh/30-build-mcpp.md index 6b41e926..e4587085 100644 --- a/docs/zh/07-build-mcpp.md +++ b/docs/zh/30-build-mcpp.md @@ -1,6 +1,16 @@ -# `build.mcpp` —— 原生构建程序 +# 30 —— 构建程序:`build.mcpp` -[English](../07-build-mcpp.md) | **简体中文** +[English](../30-build-mcpp.md) | **简体中文** + +**读者:**构建里需要一步 mcpp 没有规则的工作的作者 —— 代码生成、嵌入资源、 +一项检查,或者第二个编译器。 + +**本章回答的那一个问题:**怎样把这份工作加进构建图,使它像其余部分一样被定序、 +进指纹、可增量。 + +**不在这里:**把这一步打包给别的工程用,那是 +[31 —— 编写规则包](31-authoring-a-rule-package.md);以及这一步要跑的工具,那是 +[23 —— 项目环境](23-the-project-environment.md)。 绝大多数工程只需要 `mcpp.toml`。需要构建期逻辑时 —— 探测主机、生成源码、依据环境 决定某个编译开关——就在工程根目录放一个 `build.mcpp`。它是 mcpp 版的 Zig `build.zig` @@ -241,7 +251,7 @@ crt/host_config.h:218: fatal error: features.h: No such file or directory **不是 `sysroot_dir()`。** 那个回答的是目标**档位**的问题,在宿主目标上为空, 而宿主目标恰恰是这一对存在的场合。mcpp 不传某个开关时,对应的那个为空串。 -### 解析出的是哪个 C++ 标准库:`cxx_stdlib`(2026.9.6.3+) +### 解析出的 C++ 标准库:`cxx_stdlib`(2026.9.6.3+) ```cpp const char* impl = mcpp::cxx_stdlib(); // "libstdc++" | "libc++" | "msvc-stl" | "" @@ -295,7 +305,7 @@ store 内部结构 —— 与 `dep_dir` 存在的理由相同。 **依赖声明的包同样被作答**(2026.9.6.6+),而且答的是这次构建**真正装上**的版本,不是 本地 manifest 写下的那个。一个包只有一个版本:工程与规则都命名它时,离产物更近的声明赢, -而两侧被告知同一个答案。见 [05 — mcpp.toml](05-mcpp-toml.md) 的「一个包一个版本」。 +而两侧被告知同一个答案。见 [23 — The Project Environment](23-the-project-environment.md) 的「一个包一个版本」。 **`[feature-xlings.]` 在 `` 生效时同样被作答**(2026.9.6.2+)。这张表从诞生 起就参与供给 —— 在那里写下一个包,它就会被下载并安装 —— 但构建程序的环境只由 @@ -324,7 +334,7 @@ mcpp 会**为构建机器**构建那个 `kind = "bin"` target(即使在 `--targe 全局缓存,并把路径交回。这个请求写在 `mcpp.toml` 而不是这里,理由和依赖本身 一样:向依赖图索取一个额外产物是**图级别**的请求,而图必须保持可静态分析。 完整契约(含 `[tools.overrides]` 与 `reexport = true` —— 库据此把整条工具链交给 -调用方,因此只需写**一条**依赖而不是四条)见 [05 §2.14](05-mcpp-toml.md)。 +调用方,因此只需写**一条**依赖而不是四条)见本章*依赖产出的 host 工具*。 ### 用通配符声明输入:`rerun_if_changed_glob`(2026.8.6.2+) @@ -443,7 +453,7 @@ mcpp 为那条边写出 `depfile =` 与 `deps = gcc`,ninja 读取该文件并把 > 把预编译对象写进 `[build].ldflags` 同样能到达链接器,但**不要**用它承载构建产物: > ldflags 是链接命令里的一串字符、不是图里的文件,没有任何东西跟踪它,改了它得到的是 -> `ninja: no work to do`。Windows 资源请用 [`[resources]`](05-mcpp-toml.md); +> `ninja: no work to do`。Windows 资源请用 [`[resources]`](04-mcpp-toml.md); > `object` 是其余一切的出口。 **必须写出输出文件名。** mcpp 在 prepare 期就定死源码集、fingerprint 与模块图, @@ -600,16 +610,16 @@ SubOS 里。前置让被声明的环境成为默认答案;宿主仍在其后可 `qemu-system-riscv64`:答案是一个执行时报「is not installed in this subos」的 shim,而可用的那份就在项目自己的环境里,根本不在 `PATH` 上。 -这个选择就是[第 8 章](08-toolchain-internals.md)已经描述的那一个——决定项目链接 +这个选择就是[第 8 章](91-toolchain-internals.md)已经描述的那一个——决定项目链接 哪个 C 库的同一条声明,多交付给了一个消费者。被声明的环境是什么、什么时候需要 -它,见[第 17 章](17-the-project-environment.md);`examples/07-project-subos/` 是 +它,见[第 17 章](23-the-project-environment.md);`examples/07-project-subos/` 是 一个可运行的工程。 ## 写一个规则包 一条规则 ——「对这些 `.proto` 跑 protoc」「对这些源码跑 clang-tidy」—— 属于一个包, 而不该被复制到每个消费者的 `build.mcpp` 里。机制是 -[`host-module = true`](../05-mcpp-toml.md);本节讲的是它里面应该长什么样。 +[`host-module = true`](../04-mcpp-toml.md);本节讲的是它里面应该长什么样。 下面这些从第一个规则包 `mcpplibs.grpcgen` 归纳而来,每一条特征都单独判过是必然还是偶然。 它们是指引而非规则,因为其中没有一条能给出引擎可以检查的判据。 @@ -649,7 +659,7 @@ shim,而可用的那份就在项目自己的环境里,根本不在 `PATH` 上。 而不是以包名注册,所以 `export module mcpp.rules.spirv;` 就是消费者 import 的那个名字。 官方插件集中在一个包里,`mcpp:plugins`(仓库 `mcpp-community/mcpp-plugins`):规则包命名为 `mcpp.rules.`,构建期工具命名为 `mcpp.tools.`,每个成员由该包的一个 feature 选择 -(见 [`host-module = true`](../05-mcpp-toml.md))。`mcpp.build.*` 是引擎自己的模块族, +(见 [`host-module = true`](../04-mcpp-toml.md))。`mcpp.build.*` 是引擎自己的模块族, 不用于插件。引擎判定不了谁是官方,所以检查以包的**命名空间**为键,两者不一致时告警 —— warning: build rule 'mcpplibs.plugins' declares the module @@ -680,7 +690,7 @@ action,由引擎调度。工具说明的是构建程序需要、而没有任何 **失败与提示走不同的通道。** mcpp 只在构建程序非零退出时打印抓到的输出,所以失败写 stderr 并返回非零。而必须在**成功**构建上被看见的消息要走 -[`mcpp::warning`](#warning--成功了而且仍然被听见2026821-2);成功时的 stderr 被丢弃, +[`mcpp::warning`](#warning--成功了而且仍然被听见20268212);成功时的 stderr 被丢弃, 也就是说选错通道恰好在需要它的那些构建上一言不发。 **一个 `(名字, 版本)` 只对应一份载荷。** mcpp 用这个二元组标识已安装的包,所以一个重新 @@ -745,12 +755,260 @@ mcpp **不会**每次构建都重跑 `build.mcpp`。它会缓存程序产出的 无变化时输出 `build.mcpp up to date (cached)`;否则是 `build.mcpp compiling` / `running`。 -## 说明与限制 +## 依赖产出的 host 工具(mcpp 2026.8.5.1+) + +一个包能构建出消费者在**构建期**需要的二进制 —— `protoc`、`grpc_cpp_plugin`、 +`flatc`、`moc`、转译器。在依赖上声明: + +```toml +[dependencies] +protobuf = { version = "35.1", tools = ["protoc"] } +grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] } +``` + +每个名字必须是该包的一个 `kind = "bin"` target。mcpp 会**为构建机器**构建它, +并把绝对路径以 `MCPP_DEP__BIN_` 交给 `build.mcpp` —— 用 +`mcpp::dep_bin("protobuf", "protoc")` 读取(见 [30 — build.mcpp](30-build-mcpp.md))。 + +四条值得知道的性质: + +- **永远是 host 二进制。** 即使 `mcpp build --target `,工具依然为**本机** + 构建 —— 代码生成器必须在这里跑。它是一次独立的、面向 host 的子构建:工具包 + 自己的 `[toolchain]`、自己的依赖解析生效,不需要与当前构建一致。安全的原因是 + 可执行文件与工程代码**零 ABI 接触**。 +- **单一版本轴。** 工具的版本**就是**依赖的版本,所以「protoc 与其运行时不匹配」 + 这种情况**不可表达**。(把工具单独打包正是会出这个问题,而且它在**运行期**才咬人, + 不是编译期。) +- **默认关闭。** 没人要就什么都不构建,成本由消费者付。包用 `[features]` + + `required_features` 给昂贵的部分加门(protobuf 的 `protoc` 需要 libprotoc 的 + ~157 个额外 TU,只用运行时的人绝不该编译它)。 +- **全局缓存**,按 包版本 × host 工具链 × feature × 自身依赖闭包 键控 —— 每台机器 + 构建一次,而不是每个工程一次。 + +**这个键里没有源码内容,而对 `path` 依赖这一点是看得见的。** 已发布的版本不可变, +所以对来自索引的工具,这个键是精确的。而正在旁边被编辑的工具,两次构建之间版本相同, +缓存里的二进制就留在原地:在 +[`examples/12-a-new-device-language`](../../examples/12-a-new-device-language/) +上实测,改动工具的 emitter 之后 `mcpp run` 打印的是上一次的答案,而抬高工具包的版本 +之后它被重建、产物随之改变。抬版本,或用 `mcpp cache clean` 清空构建缓存 —— +tool store 就住在里面,路径是 `/tool//@/`。 + +**缺口在重建,不在跟踪。** 把工具列进 action 输入的规则,确实会在那个文件的字节变化 +时重跑 —— 实测直接覆盖 store 里的二进制,产物随之改变。不发生的是「让这些字节变化」 +的那次重建。 + +### `[tools.overrides]` —— 使用已有的二进制 + +```toml +[tools.overrides] +"compat.protobuf:protoc" = "/usr/bin/protoc" +``` + +或者不改 manifest(CI、发行版打包): + +```bash +MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build +``` + +命中 override 会**完全跳过构建**。每个同类系统都提供这条逃生舱(vcpkg 的 +`VCPKG_HOST_TRIPLET`、CMake 的 `LLVM_NATIVE_TOOL_DIR`、Qt 的 `QT_HOST_PATH`), +理由一样:一个在本机构建不出来的工具**不能是死路**。它**刻意不进** cache key —— +逃生舱不是可复现输入。 + +### `host-module = true` —— 可复用的构建规则以包分发 + +一条规则(比如「对这些 `.proto` 跑 protoc」)应该**写一次**,而不是复制进每个 +消费者的 `build.mcpp`。把它做成普通的 mcpp 库包再 import: + +```toml +[dependencies] +protobufgen = { version = "0.1.0", host-module = true } +``` + +```cpp +// build.mcpp +import mcpp; +import protobufgen; +int main() { return protobufgen::generate({"schema"}) ? 0 : 1; } +``` + +mcpp 会把该包的 lib 根模块**为 host 编译,且与 `build.mcpp` 在同一条命令里** —— +这正是 BMI 能用的前提:一个模块接口只对「在 standard / dialect / 编译器身份上与 +它一致」的编译可导入。 + +于是规则**有版本、能测试、能通过既有的包管理器分发**,而且是用 **C++** 写的 +—— 不引入第二门语言,这正是 `build.mcpp` 存在的理由。 + +**模块名是规则源码自己声明的那个**(mcpp 2026.8.29.1+)。`export module +acme.rules.protobuf;` 就以 `acme.rules.protobuf` 被 import,与包叫什么无关。 +模块名是作者定义的 API,不镜像包身份 —— 普通库包一直遵循的就是这条规则。 + +2026.8.29.1 之前 host 模块这条路径注册的是裸 `package.name`,于是声明名与包名 +分叉的规则包在 GCC 上能构建、在 Clang 与 MSVC 上失败:GCC 的 BMI 隐式落在 +`gcm.cache` 且按**声明名**索引,而另外两者拿到的是显式的 `=` 映射。 +因此包名不再承担任何 C++ 命名约束,`grpc-rules` 重新是合法包名。 + +**两个规则不得声明同一个模块名。** `import` 寻址的是模块,所以两个这样的包对编译器 +不可区分,而它们的 BMI 与对象文件同名 —— 后者覆盖前者,存活的那个对象被送进链接两次。 +mcpp 拒绝这种情形,并点名两个包与各自的 interface 路径。检查的范围是一次 `build.mcpp` +能看见的那些规则,不是索引级的全局唯一性 —— `path` 依赖与私有 registry 本来就绕得开。 + +**`mcpp.` 前缀保留给由 mcpp 项目维护的规则。** 不在 `mcpp` 命名空间下的包声明该前缀 +的模块名时给出一条同时点名两者的警告,构建继续。之所以是警告:引擎判定不了谁是官方, +`path` 依赖、私有镜像与内部 fork 都合法,而且从这里看都一样。 + +lib 根必须在 `src/.cppm`(或 `[lib] path` 指向的位置);缺失时报 +*"host module 'x': no interface unit at …"*。 + +**一个包可以提供多条规则,由 feature 选择**(mcpp 2026.9.5.3+)。包解析后的 +`[build] sources` 里 —— 含 feature 加入的源文件 —— 每一个模块接口单元都以它自己声明的 +名字编成一个 host 模块,lib 根排在最前。feature 单元可以 import lib 根;除此之外每个 +单元单独编译,因此只 import `std` 与 `mcpp`。只有写在清单里的源文件参与:未声明 +`sources` 的包所推断出的 `src/**` 不被读取,所以此前发布的规则包暴露的仍是它当时暴露 +的那一个模块。 + +```toml +# 集合包的 manifest +[build] +sources = ["src/plugins.cppm"] # export module mcpp.plugins; + +[features] +rules-cuda = { sources = ["rules/cuda.cppm"] } # export module mcpp.rules.cuda; +rules-spirv = { sources = ["rules/spirv.cppm"] } # export module mcpp.rules.spirv; +``` + +```toml +# 消费者 +[build-dependencies.mcpp] +plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } +``` + +**用 `[build-dependencies]` 而不是 `[dependencies]`** —— 规则包正是 [04 §2.6.1](04-mcpp-toml.md) 描述的那种 +情形:它的库绝不该到达目标,而它的规则仍然被需要。两条轴是分开的: +`host-module = true` 说的是**要哪一种构建期产物**,而 section 说的是**这个包是否到达 +目标**;规则包在第二条轴上的答案是"否",而 section 就是说这件事的地方。写在 +`[dependencies]` 里同样能工作 —— 这恰恰是为什么这条区分必须被**陈述**,而不能指望由 +一次失败来教会。 + +模块集合就是 feature 集合:feature 未激活的单元不编译,import 它会以未知模块失败。 +`mcpp:plugins` 是 mcpp 项目维护的集合(仓库 `mcpp-community/mcpp-plugins`);其成员 +命名为 `mcpp.rules.`(规则包)与 `mcpp.tools.`(构建期工具)。 + +*仅构建期:* `host-module = true` 的依赖**不会**被编进、也不会被链进本工程的 target, +它所依赖的东西也不会。它只在 `build.mcpp` 期间运行,别处都不出现。(2026.8.5.2 之前 +它还会被当作普通库再编一遍,这正是规则里 `import mcpp;` 失败的原因:在那第二次编译里 +内置模块并不存在。2026.8.29.1 之前被排除的只有规则本身,它自己的 `[dependencies]` +仍会被编译并链进消费者的二进制,而规则却 import 不到它们。) + +### 依赖另一个规则的规则(mcpp 2026.8.29.1+) + +规则在自己的 `[build-dependencies]` 里声明所需之物,并可以 import 其中标了 +`host-module = true` 的条目: + +```toml +# 写在规则包自己的清单里 +[build-dependencies] +globbing = { path = "../globbing", host-module = true } +``` + +```cpp +// 规则自己的接口 +export module tidyrule; +import std; +import mcpp; +import globbing; +``` + +mcpp 先编译内层规则,同一条命令、同一套 flag,因此 BMI 的一致性仍是结构性事实而不是 +需要事后校验的性质。 + +消费者**不可以** import `globbing`:构建期的 provision 只在 `reexport = true` 的边上 +再跨一跳,而 mcpp 自己执行这条规则,不交给编译器 —— 在 GCC 上那个 import 会成功, +然后在别人的机器上失败。 + +*限制:* 每个 host 模块只有一个接口单元。带实现单元或多个模块的库还不能作为规则的 +构建期依赖。 + +### `reexport = true` —— 由库替用户拉起整条工具链(2026.8.6.2+) + +上面这些都由**使用工具的人**声明。当知识本来属于库时,这个位置就错了:gRPC +的代码生成需要 protobuf 的 `protoc`,而 gRPC 包的任何使用者都不应该知道这件事。 + +`reexport = true` 把一条边上的构建期提供物 —— 它的 `tools`、它的 +`host-module`、以及该依赖的目录 —— 交给**本包自己的消费者**: + +```toml +# 写在 grpc 包自己的 manifest 里 +[feature-deps.codegen] +"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } +grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"], reexport = true } +grpcgen = { version = "1.83.0", host-module = true, reexport = true } +``` + +于是使用者只写一行,再 import 那个规则: + +```toml +[dependencies] +grpc = { version = "1.83.0", features = ["codegen"] } +``` + +```cpp +// build.mcpp +import mcpp; +import grpcgen; +int main() { return grpcgen::generate_all() ? 0 : 1; } +``` + +- **默认关闭,并且刻意不复用边上的 `visibility`。** `visibility` 本身默认就是 + `"public"`,复用该可见性意味着任意深度的依赖都能静默地向构建程序的工具 + 命名空间里塞东西。「把某样东西交给消费者」是一条供应链主张,必须写下来。 +- **一次声明只走一跳。** 被再导出的提供物到达声明它的那个包的消费者;要继续 + 往上走,下一个包必须自己也写 `reexport`。每个包只决定**它**交出什么。 +- **feature 可以往一条已经声明过的依赖上追加请求。** gRPC 无条件依赖 protobuf, + 而它的 `codegen` feature 往同一条边加 `tools = ["protoc"], reexport = true`。 + `tools` 与 `features` 取并集,`host-module` 与 `reexport` 取或;`version` / + `path` / `git` 不合并 —— feature 仍然无法静默覆盖无条件条目的身份。 +- **传播的是可见性,不是执行。** `dep_bin()` 只返回路径,跑不跑仍由消费者的 + `build.mcpp` 决定;谁构建了这个工具、tool store 怎么做键,都不改变。 +- **裸名由阶梯决定,而不是靠运气。** 一旦两个库都能再导出,它们可能同时提供 + 尾名 `protobuf`。全限定的 `MCPP_DEP___BIN_` 总是发布;裸名 + 依次绑定到 `mcpplibs.`、`compat.`、无命名空间的 ``,最后才是「剩下 + 的唯一候选」——存在争用时 mcpp 会说出来,而不是默默选一个。 + +#### 旧版 mcpp 读到用了这些键的 manifest + +不认识的依赖键会被**记为降级**并忽略(mcpp 2026.8.6.2+),因此一份为更新的 +mcpp 写的包仍然能加载,这个读取器认识的部分照常生效。在那之前它是**整份加载 +失败**且报错误导,这正是「已发布的包永远无法采用新键」的原因——与索引下限确立 +的是同一条性质:**数据不得决定程序是否可用**。 + +因此,一个**依赖** `reexport` 才有那套人机工程的包,仍然需要足够新的客户端; +变化在于该包的其余部分在旧客户端上不再一起失效。 + +#### 按平台裁剪提供物 + +一个包可能只在部分平台声明 `bin` 目标。既然现在是**库**决定请求什么,无条件的 +请求就会把「不支持的平台」变成用户改不掉的硬错。用条件段裁剪: + +```toml +[target.'cfg(not(windows))'.feature-deps.codegen] +"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true } +``` + +`[target..feature-deps.]`(2026.8.6.2+)与 `[target.]` 下的 +其余依赖表(`dependencies` / `dev-dependencies` / `build-dependencies`)遵循同 +一套谓词规则,针对**解析后的 target** 求值。**feature 本身在所有平台都注册** +—— 只有它拉进来的东西是条件性的 —— 因此在没有任何谓词匹配的平台上请求它,不是 +「未知 feature」错误。 + + + +## 当前边界 - **在主机上运行——交叉构建下也是**(mcpp 0.0.95+)。`mcpp build --target ` 下,程序用宿主解析的工具链编译、在宿主运行,并看到 `MCPP_TARGET` = 交叉三元组。 纯声明式的目标门控仍首选 `[target.'cfg(...)']` 表——参见 - [05 - mcpp.toml 工程文件指南](05-mcpp-toml.md)。 + [04 - mcpp.toml 工程文件指南](04-mcpp-toml.md)。 - **当前工作目录是工程根目录**,因此相对路径(`src/generated.cpp`)会落在预期位置。 - `build.mcpp` 非零退出会中止构建并打印其输出。 - **运行有时间上限**(mcpp 2026.8.5.1+):构建程序默认有 **600 秒**,超时后 mcpp diff --git a/docs/zh/31-authoring-a-rule-package.md b/docs/zh/31-authoring-a-rule-package.md new file mode 100644 index 00000000..a291575e --- /dev/null +++ b/docs/zh/31-authoring-a-rule-package.md @@ -0,0 +1,298 @@ +# 31 —— 编写规则包 + +**读者:**要把一步构建工作打包给别的工程使用的生态作者 —— 一种设备语言、一个 +着色器编译器、一个生成的接口,或者一项检查。 + +**本章回答的那一个问题:**一个包怎样供给一条规则,以及消费者要写什么才能用上它。 + +**不在这里:**给单个工程自己的构建加一步,那是 +[30 —— 构建程序](30-build-mcpp.md) —— 同一批原语,规模更小;feature 相关键本身, +那是 [06](06-features-and-capabilities.md);以及已发布规则各自的拼法,那属于 +`mcpp:plugins`。示例:[`08-build-rules`](../../examples/08-build-rules/) 做检查与 +嵌入,[`12-a-new-device-language`](../../examples/12-a-new-device-language/) 新增 +一门语言。 + +## 扩展模型 + +mcpp 的构建表面由包扩展,而不是由发布扩展。做扩展的有五个点,而经由它们进来的任何 +名字,引擎都不持有。 + +| 扩展点 | 效果 | 声明位置 | +|---|---|---| +| `mcpp::action` | 构建图里的一条边:一条命令,带声明的输入与输出 | 构建程序,或它 import 的规则模块 | +| `device_extensions` | 一个扩展名被引擎归类为**设备源**,而不是被拒绝 | 包的某个 feature | +| `rule_module` | 消费者的构建程序 import 哪个模块来够到这条规则 | 同一个 feature | +| `tools = [...]` | 一个**从源码为构建机构建**的生成器或编译器,用 `mcpp::dep_bin` 取到 | 一条依赖边 | +| `[xlings]`、`[feature-xlings]` | 规则要运行的预建工具,按需安装 | 包本身,或它的某个 feature | + +生态用它们建出来的东西: + +| 扩展面 | 所在包 | 使用的扩展点 | +|---|---|---| +| CUDA、HIP、SYCL、Ascend C | `mcpp:plugins`,各一个 feature([42](42-heterogeneous-builds.md)) | 驱动厂商编译器的 action,加上按加速器设闸的载荷 | +| Slang | `mcpp:plugins` 的 `rules-slang` | `device_extensions = [".slang"]` —— 第一门无需引擎点名即被支持的语言 | +| GLSL 与 HLSL 到 SPIR-V,以及其上的模块 | `mcpp:plugins` 的 `rules-spirv` | 每个着色器一条 action,加上一个生成的模块 | +| 设备与 C++ 之间的岛边界 | `mcpp.tools.island` | 一个生成器,加上 `mcpp::generated` | +| 作为可链接对象的资源文件 | [`08-build-rules`](../../examples/08-build-rules/) | `role = "object"` | +| 能让构建失败的检查 | [`08-build-rules`](../../examples/08-build-rules/) | `role = "check"` | +| 引擎从未听说过的语言 | [`12-a-new-device-language`](../../examples/12-a-new-device-language/) | `device_extensions`,加上经 `tools = [...]` 构建出来的编译器 | + +### 这个模型能表达的形态 + +**一门新语言,不论由什么编译它。** 规则声明它认领的扩展名、为每个源提交一条 +action,并把编译器列进这条 action 的输入。这个编译器可以是厂商工具包、一个 LLVM +前端、一个发出设备二进制的解释器,也可以是规则包自己从源码构建出来的程序。它产出 +的是设备二进制、目标文件还是 C++,由 action 的 `role` 决定,别无其他。引擎始终不 +学习这门语言:它学到的是「某个扩展名是设备源」以及「某条 action 认领它」。 + +**预处理与代码生成。** `role = "source"` 的 action 产出 C++,由声明它的包随后编译, +而该包的每一条编译边都等它。输入可以是模板、接口定义、一张表,或另一条 action 的 +输出 —— 串联是常规做法,因为 action 之间由文件定序并计入指纹。 + +**一个一半是 C++、一半是另一种语言的文件。** 归类发生在任何规则运行之前,所以带着 +引擎自有扩展名的文件按 C++ 编译,永远到不了规则那里。因此一个携带外来代码块的源要 +用规则认领的扩展名,再由规则把它拆开:抽出来的 C++ 走 `role = "source"`,外来的那 +一半走规则自己的编译器,两者之间的缝就是 +[42 —— 异构构建](42-heterogeneous-builds.md) 里的 `extern "C"` 边界。本仓库今天没有 +任何包是这个形态。 + +### 边界 + +**声明不能重新归类引擎已经拥有的东西。** 依赖的 `device_extensions` 在内建角色 +**之后**才被查询,所以规则包认领不了 `.cpp`、`.cppm`、`.c` 或 `.S`。这些是引擎自己 +的词汇,一个包不得把文件从中挪走。认领它们不会被诊断,也不产生任何效果:实测把 +`".cpp"` 加进某条规则的 `device_extensions`,消费者的 `main.cpp` 仍按 C++ 编译, +构建成功。 + +**模块接口的扩展名是工程的轴,不是规则的轴。** 把接口写成 `.ixx` 的工程声明 +`[build] module_extensions`([04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md))。没有 +任何规则包的键能新增一个,因为模块接口要被扫描 import、要产出 BMI、还要进链接 —— +这是三项引擎行为,而不是一条要跑的命令。 + +**两张表都不包含的扩展名会被点名拒绝**,所以写错的 `device_extensions` 会立刻显形, +而不是把一个源默默丢掉: + +``` +error: scanner errors: + .../orphan.zzz: 'orphan.zzz' is listed in [build] sources, and mcpp has no role + for the extension '.zzz'. + Its object would be compiled and then linked by nothing, so this is refused rather + than built. +``` + +## 规则包的定义 + +三部分,没有一部分是 mcpp 特有的: + +| 部分 | 内容 | +|---|---| +| 一个包 | 普通的 `mcpp.toml`,有版本与许可证 | +| 一个模块 | 一个 `.cppm`,导出 `options` 与一个提交构建边的函数 | +| 一个 feature | 选中它的开关,也是它自己的依赖挂靠的地方 | + +引擎里没有规则名,也没有厂商名。消费者命名这个包、激活一个 feature,构建程序 +import 那个模块。 + +## manifest 的三个键 + +```toml +[features] +default = [] + +[features.rules-toy] +sources = ["src/rules-toy.cppm"] +rule_module = "example.rules.toy" +device_extensions = [".toy"] +``` + +| 键 | 对消费者的作用 | +|---|---| +| `sources` | 只有 feature 激活时才编译该模块 | +| `rule_module`(2026.9.7.1+) | 消费者构建程序 import 的模块。它隐含 `host-module = true` | +| `device_extensions`(2026.9.7.1+) | 这些扩展名被归类为**设备源**:不做 import 扫描、不产出 BMI,且没有 action 认领时被拒绝 | + +只做生成或检查、不编译某种语言的规则不声明 `device_extensions`; +`08-build-rules` 是这个形状,它的消费者在依赖边上自己写 `host-module = true`。 + +设备扩展名不在默认 source glob 里。消费者通过点名这些文件来选入: + +```toml +[build] +sources = ["src/*.cpp", "src/kernels/*.toy"] +``` + +## 消费者 import 的那个模块 + +每条已发布规则遵循的写法是:一个带默认值的 `options` 结构,加一个提交边的 +`compile`(或 `generate`)函数: + +```cpp +export module example.rules.toy; +import std; +import mcpp; + +export namespace example::rules::toy { + +struct options { + std::string out_dir = std::string(mcpp::out_dir()); + std::string rule_dir = std::string(mcpp::dep_dir("rules-toy")); +}; + +bool compile(options opt = {}); + +} +``` + +把「规划边」与「提交边」拆成两个函数的规则,给了消费者一条越过它最后一个旋钮 +而不必手写 action 的路;`08-build-rules` 的 `plan` / `submit` 就是这个形状。 + +**规则只取自己认领的扩展名,其余留给别人。** `mcpp::device_sources()` 是一个串、 +每行一个包根相对路径,而且它是这个包**全部**的设备集合。一个有两个后端的构建把 +两边的源都放进同一个列表,而构建程序里的每条规则读到的是同一个值。 + +## 声明工作:`mcpp::action` + +```cpp +mcpp::action a; +a.id = id.c_str(); // 包内稳定且唯一 +a.role = "source"; +a.description = desc.c_str(); +a.arg("sh").arg(script.c_str()).arg(input.c_str()).arg(output.c_str()); +a.input(input.c_str()); +a.input(script.c_str()); +a.output(output.c_str()); +a.submit(); +``` + +这些字符串必须比 action 活得久。`a.id = ("toy:" + stem).c_str()` 交给它的是一个 +指向临时对象的指针,而该临时对象在 `submit()` 之前就已经消失。 + +### 四种 role + +`role` 决定这条边的输出去哪里,以及这条边什么时候跑。 + +| `role` | 输出 | 次序 | +|---|---|---| +| `source` | 可编译的那些进入编译集合 | 声明它的包的每条编译边都等它 | +| `check` | 一个 stamp 文件 | 与编译并行;`blocking = true` 让编译等它 | +| `object` | 进入**链接**集合 | 链接边消费它们 | +| `artifact` | 一个新文件 | 它的输入是链接产物,所以它在链接之后跑 | + +### 声明的输入,以及其中的编译器 + +声明的输入变化时 action 重跑。**命令调用的那个工具也是输入。** 少了它,改动规则 +自己的编译器会让每条边都是干净的,产物保留上一个编译器产生的字节 —— 一次覆盖在 +陈旧结果之上的绿色构建。 + +action 的命令跑在**构建目录**里,不是包根。`mcpp::device_sources()` 答的是包根 +相对路径,所以规则要先用 `mcpp::manifest_dir()` 拼成绝对路径再放上命令行。 + +### 命令自己发现输入时,声明 depfile + +一份 include 了别的文件的着色器或 kernel,有规则枚举不出来的输入。涉及的每个 +编译器都能产出 depfile ——`glslangValidator --depfile`、`glslc -MD -MF`、 +`slangc -depfile`,以及 clang 家族驱动的 `-MD -MF`: + +```cpp +a.depfile = dep.c_str(); // 命令写出的一个路径 +a.arg("--depfile").arg(dep.c_str()); +``` + +depfile 不得同时被声明为 `output()`。 + +### 串接 action + +一个 action 可以消费另一个的产物。引擎只负责给它们定序与做指纹,而这就是一次 +device link 在引擎侧的全部内容:N 个 `artifact` action,其输出不进链接;再加一个 +`object` action 读取它们,产出真正进链接的对象。 + +## 规则自带的环境 + +规则拥有它所驱动的那份包清单,因为运行编译器、读头文件、把库目录放上链接行的 +正是它的代码。 + +```toml +[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda] +"xim:cuda-nvcc" = "12.9.86" +"xim:cuda-cudart" = "12.9.79" +``` + +两道闸,下载一个字节之前两道都要开:feature 说这条规则要不要, +`cfg(accelerator = ...)` 选择器说这次构建要不要为设备编译。不带加速器的构建两道 +都不开。 + +裸版本是项目可以覆盖的**选择**;`>=` 是项目不得低于的**要求**。见 +[23 —— 项目环境](23-the-project-environment.md) 的*一个包一个版本*。 + +## 生成岛的边界 + +设备翻译单元不能 import 模块,所以它与 C++ 侧之间的边界是一个 `extern "C"` 头。 +`mcpp:plugins` 的 `mcpp.tools.island` 从两个实现里读出标了 `MCPP_EXPORT_C` 的 +入口点,写出那个头以及它之上的模块,于是每个签名只存在一份。 + +```cpp +mcpp::tools::island::options opt; +opt.module_name = "myapp.kernels"; +opt.out_dir = std::string(mcpp::out_dir()) + "/island"; + +const auto entries = mcpp::tools::island::scan(halves, opt); +const auto out = mcpp::tools::island::emit(*entries, opt); +mcpp::generated(out->interface_file.c_str()); +``` + +可用的有四级,每一级覆盖上一级: + +| 级 | 手写的部分 | 消费者写 | +|---|---|---| +| L0 | 只有被标记的入口点 | `import myapp.kernels` —— 岛自己的 C 形状接口 | +| L1 | 生成模块之上的一个接缝模块 | `import myapp.saxpy` —— 项目设计的接口 | +| L2 | 接缝,外加直接传给 `emit` 的入口点列表 | 同上,用于 scan 看不见的入口点 | +| L3 | 头文件与模块都手写 | 同上,签名写了两遍 | + +生成的头经由 `mcpp::tools::island::force_include_flags` 到达岛,而这些旗标交给 +驱动设备编译器的那条**规则**,不走 `mcpp::cxxflag` —— 把一个头强制灌进每个 C++ +翻译单元,会让声明出现在模块接口的 `export module` 之前,那是非良构的。 + +[`examples/09-heterogeneous/boundary`](../../examples/09-heterogeneous/boundary/) +是 L0,并写明了每一级的代价。 + +## 报告构建应当知道的事 + +### `warning` —— 成功了,并且仍然被听见 + +`mcpp::warning` 是规则「干完了活、但有话要说」的通道:它不得不选择的宿主编译器、 +它回落到的载荷。除此之外,构建程序的输出只在非零退出时才被打印。 + +### `fact` / `floor` —— 探针通道 + +规则测量,引擎在编译任何东西之前比较。 + +```cpp +mcpp::fact("cuda.driver", driver_version); +mcpp::floor("cuda.driver", runtime_needs); +``` + +引擎读到的是一个名字、一个关系和一个版本;名字是穿过它的数据。探不到答案的探针 +不陈述任何答案。 + +## 找到规则自己的文件 + +| 对象 | 获取方式 | +|---|---| +| 规则包自己的目录树 | `mcpp::dep_dir("")` —— 按**消费者**在 `[dependencies]` 里写的那个名字 | +| `[xlings.workspace]` 声明的载荷 | `mcpp::xpkg_dir("")` | +| 由依赖构建出的宿主工具 | `mcpp::dep_bin("", "")` | + +`dep_dir` 按消费者 manifest 里的拼法作答。随模块一起发文件的规则要把该目录作为 +一个 option 暴露出来,好让用别的键声明这条边的消费者能提供它;并且在找不到时以 +一条点名「找的是什么」的消息拒绝,而不是拿一个空路径去执行命令。 + +## 当前边界 + +- 规则包自己 `[features] default` 里的规则 feature 不隐含 `host-module`。 + `mcpp:plugins` 声明 `default = []`;规则默认开启的规则包会被拒绝,消息点名 + 该模块与需要补的键。 +- `mcpp emit xpkg` 把 `manifest = "mcpp.toml"` 写进 `mcpp` 段,而 + `mcpp xpkg parse` 把该键报为未知并以 1 退出。`mcpp-index` 里没有任何描述符使用 + 它(218 个里 0 个);自带 `mcpp.toml` 的包整个省略 `mcpp` 字段。见 + [09 —— 按场景选命令](09-commands-by-scenario.md)的*当前边界*。 diff --git a/docs/zh/32-authoring-a-payload.md b/docs/zh/32-authoring-a-payload.md new file mode 100644 index 00000000..15b1ef24 --- /dev/null +++ b/docs/zh/32-authoring-a-payload.md @@ -0,0 +1,126 @@ +# 32 —— 编写一个载荷 + +**读者:**要把一个工具或一份预编译库打包出来,好让 mcpp 工程声明它、由 mcpp 安装 +它的人。 + +**本章回答的那一个问题:**一个 `xim:` 载荷由什么构成,以及它的描述符必须说清什么, +消费者才能点名它并得到一个能用的程序。 + +**不在这里:**发布供他人 `import` 的**源码包**,那是 +[11 —— 发布一个库](11-publishing-a-library.md);让**宿主**的库能被产物够到,那是 +[33 —— 编写运行时适配包](33-authoring-an-adapter.md);以及消费一个载荷,那是 +[23 —— 项目环境](23-the-project-environment.md)。 + +在此之前:[31 —— 编写规则包](31-authoring-a-rule-package.md) —— 规则声明它所驱动的 +载荷。在此之后:[33 —— 编写运行时适配包](33-authoring-an-adapter.md)。 + +## 载荷的定义 + +一切由 mcpp 安装而不编译的东西:编译器、着色器编译器、设备工具包、模拟器、探针 +驱动、预编译的 C 库。工程在 `[xlings.workspace]` 里点名它,或者规则包在 +`[feature-xlings.]` 里点名它,mcpp 在构建之前把它供给到位。 + +一个载荷在 `xim-pkgindex` 里是一个 Lua 文件:一个描述它的 `package` 表,加两个把 +它放好并登记的函数。 + +能用的最短形态: + +```lua +package = { + spec = "2", + name = "glslang", + description = "Khronos reference GLSL/ESSL front end and validator", + licenses = {"BSD-3-Clause", "Apache-2.0", "MIT"}, + type = "package", + archs = {"x86_64"}, + + xpm = { + linux = { + ["latest"] = { ref = "15.1.0" }, + ["15.1.0"] = { + url = { + GLOBAL = "https://github.com/…/glslang-15.1.0-linux-x86_64.tar.gz", + CN = "https://gitcode.com/…/glslang-15.1.0-linux-x86_64.tar.gz", + }, + sha256 = "87167c9cb32f258addbedb607639b2c1f484c029ba91542a92f19ead21d65d13", + }, + }, + }, +} + +function install() + local dir = pkginfo.install_dir() + os.tryrm(dir) + os.mv("glslang-15.1.0", dir) + return true +end + +function config() + xvm.add(package.name) + return true +end +``` + +`install()` 把解开的目录树放到 mcpp 会去找的位置;`config()` 登记这个载荷提供什么。 +本章其余内容,都是这两个函数在多做一些事。 + +## 描述符必须做对的四件事 + +**一个版本,两个 URL。** 每个版本都带 `GLOBAL` 与 `CN` 两个 URL 和一个 `sha256`。 +两个镜像服务同一份字节;在任一镜像后面的消费者解析到同一个哈希,而只写一个 URL 的 +描述符对半个生态不可用。 + +**`latest` 是一个引用,不是一个版本。** `["latest"] = { ref = "15.1.0" }`。它是 +消费者不点名版本时拿到的东西,移动它是一个刻意的动作 —— 钉在 `15.1.0` 的消费者 +不受影响。 + +**`archs` 与平台表是解析要读的东西。** 只为 `linux` 与 `x86_64` 发布的载荷就这样 +声明,于是别的平台上的消费者会被**点名拒绝**,而不是拿到一个跑不起来的东西。 + +**依赖是带下界的 `xim:` 地址。** + +```lua +deps = { "xim:gcc-runtime@>=15", "xim:glibc@>=2.38" }, +``` + +## 让载荷可被够到 + +只会解包的载荷不可用。三条声明把一个目录变成构建能消费的东西。 + +**路径上的一个程序。** `xvm.add(package.name)` 登记该载荷的 `bin/`,于是 mcpp 能 +按裸名找到这个程序。规则包应当**写程序名,不写路径** —— mcpp 会搜索图中每个被声明 +载荷的 `bin/`,然后才是 `PATH`,并且能准确报出它搜过哪些目录。 + +**消费者要链接或加载的库。** + +```lua +exports = { + runtime = { libdirs = { "lib" } }, +}, +``` + +`elfpatch` 从每个依赖里读这一项,写进消费者的 `RPATH` —— 这正是一叠载荷不需要任何人 +设 `LD_LIBRARY_PATH` 就能解析的原因。 + +**头文件,好让这个环境里的编译器能对着它构建。** `sysroot.declare_libs(...)` 与 +头文件声明把载荷放进 SubOS 的 sysroot 视图。**是声明而不是复制** —— xlings 会随包 +一起移除它们,而一份复制会比它的主人活得更久。 + +## 档位:需要载荷的命令 + +```toml +"xim:qemu-arm" = { version = "9.2.4-1", when = "run" } +``` + +`when` 是选中该载荷的那个 feature 之外的**第二道独立闸门**。feature 说**谁**需要这个 +工具;档位说**什么时候**。模拟器是运行时需要、编译时不需要的,所以一个只构建固件、 +从不烧录的 CI 任务**一个字节都不下载**。 + +## 当前边界 + +- 载荷发布到 `xim-pkgindex` —— 那是一个独立仓库,有它自己的评审;mcpp 里没有任何 + 东西发布载荷。 +- `latest` 与「表里版本号最大的那个」是两个不同的问题,想要最新已发布版本的消费者 + 应当点名 `latest`。 +- 载荷自己的 CI 无法核验消费者能否解析到它:那是「对着已发布描述符做沙箱核验」的 + 用途,也是唯一核验**已发布字节**而不是工作树的东西。 diff --git a/docs/zh/33-authoring-an-adapter.md b/docs/zh/33-authoring-an-adapter.md new file mode 100644 index 00000000..b1dff02f --- /dev/null +++ b/docs/zh/33-authoring-an-adapter.md @@ -0,0 +1,77 @@ +# 33 —— 编写运行时适配包 + +**读者:**要让**宿主**提供的某个库能被 mcpp 产物在 Linux 上够到的人 —— 图形驱动、 +Vulkan ICD、某个专有运行时。 + +**本章回答的那一个问题:**为什么产物看不见一个明明装在机器上的库,以及一个包要 +声明什么才能修好它。 + +**不在这里:**打包一个由 mcpp 安装的库,那是 +[32 —— 编写一个载荷](32-authoring-a-payload.md) —— 只要那个库**可以**被再分发,它 +就是正确答案;以及运行时契约的字段,那是 +[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md) 的 `[runtime]`。 + +在此之前:[32 —— 编写一个载荷](32-authoring-a-payload.md)。在此之后: +[34 —— 编写板级支持包](34-authoring-a-bsp.md)。 + +## 它为之存在的那次失败 + +库装好了。加载器也找到了它的 manifest。`dlopen` 仍然失败: + +``` +DRIVER: Found the following files: /usr/share/vulkan/icd.d/lvp_icd.json … +ERROR: libvulkan_lvp.so: cannot open shared object file +``` + +库就在 `/usr/lib/x86_64-linux-gnu`。够不到它们的是**进程**:mcpp 构建出的二进制跑在 +mcpp 自己的 glibc 之下,带着它自己的搜索路径, + +``` +interp: …/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2 +rpath : …/xim-x-glibc/2.39/lib64:…/xim-x-gcc/…/lib64:$ORIGIN +``` + +于是从那个进程内部发起的、按裸 soname 的 `dlopen`,**根本不会去搜索宿主的库路径**。 +没有任何东西坏掉;产物只是不往那里看 —— 而这正是 mcpp 构建可复现的那条性质本身。 + +## 适配包的定义 + +**一个符号链接农场,加上让它可被够到的那点元数据。** 不内嵌任何东西、不再分发任何 +东西,包里没有上游的字节。`runtime.library_dirs` 把一个包自有的符号链接目录放上产物 +的运行期搜索路径,于是整条链解析得开。 + +工程把适配包当作一个普通依赖声明,除此之外什么都不做。 + +## 驱动本身不能成为一个包的原因 + +专有驱动的用户态与**内核模块处于 ABI 锁步**,而它的许可证禁止再分发。这两条都不是 +靠努力能解决的打包问题,所以这类驱动被建模为**宿主能力** —— 机器要么有、要么没有 +—— 而适配包是产物够到它的方式。 + +**开源**驱动是另一种情形,取另一个答案:它就是一个载荷(CPU 上是 +`xim:mesa-lavapipe`,AMD 硬件上是 `xim:mesa`),而用着它的机器完全不需要这个农场。 +自 2026.09.05 起,当某个已发布载荷提供了同名库、且它的符号集合覆盖宿主那份时,适配包 +优先取载荷 —— 于是农场实际记录下来的,是专有用户态与打包欠账。 + +## 适配包最容易做错的三件事 + +**模式列表必须覆盖传递依赖。** 整条链都要经由同一个目录解析。Mesa 的软件光栅器会 +拉进 LLVM;NVIDIA 驱动会拉进它自己那一族。只列 ICD 一个,结果是同一句 +`cannot open shared object file` 往下挪一层。 + +**`libstdc++` 应当在列表里,这不是疏忽。** mcpp **静态**链接 libstdc++ —— 它不出现在 +构建产物的 `NEEDED` 里 —— 所以一个被 `dlopen` 的 C++ 驱动,除非宿主那份在这里被提供, +否则没有任何东西可供它解析。 + +**不得有任何一项是必需的。** 一台完全没有这类驱动的机器是**合法配置**,而这个生态的 +每一台 CI runner 都是这样的机器。此时农场为空,程序报告它实际找到了什么。一个在宿主库 +缺失时报错的适配包,会把一种受支持的配置变成构建失败。 + +## 当前边界 + +- **按构造只适用于 Linux。** macOS 的 dyld 与 Windows 的 PE 加载器没有对应的这一层, + 因此面向它们的工程不声明适配包。 +- 适配包无法让一台机器上不存在的驱动工作起来。它只移除一个障碍 —— 可达性 —— 其余的 + 按「缺席」如实报告。 +- 农场的内容在适配包被安装时决定。之后才装上的驱动,要到适配包被重新安装时才会被 + 纳入。 diff --git a/docs/zh/34-authoring-a-bsp.md b/docs/zh/34-authoring-a-bsp.md new file mode 100644 index 00000000..5a76198d --- /dev/null +++ b/docs/zh/34-authoring-a-bsp.md @@ -0,0 +1,118 @@ +# 34 —— 编写板级支持包 + +**读者:**要把一块板子支起来,好让工程能用 `mcpp build` 面向它、用 `mcpp run` +抵达它的人。 + +**本章回答的那一个问题:**一个板级支持包供给什么,以及一个包怎样同时服务模拟器与 +真实板子。 + +**不在这里:**使用一个 BSP,那是 +[40 —— 裸机与 freestanding 目标](40-baremetal.md);消费者看到的 runner 机制,那是 +[41 —— 抵达一台设备](41-devices.md);以及把模拟器或探针驱动本身打包,那是 +[32 —— 编写一个载荷](32-authoring-a-payload.md)。 + +在此之前:[33 —— 编写运行时适配包](33-authoring-an-adapter.md)。 + +## BSP 的供给内容 + +freestanding 目标没有操作系统,所以 hosted 程序白得的一切都必须来自某处。BSP 就是 +那个某处,它供给**整个目标世界**: + +| | 内容 | +|---|---| +| 内存布局 | 一份链接脚本 —— 程序既推导不出、也猜不到的那一个事实 | +| 启动代码 | `main` 之前跑的东西,以及向量表 | +| 一个导出模块 | 程序 import 它来够到板子的控制台与外设 | +| **runner** | `mcpp run` 与 `mcpp test` 究竟怎样抵达这块板子 | + +最容易被漏掉的是 runner;少了它,每个消费者都要自己写一遍模拟器的调用。 + +## 一个包,两种环境 + +经模拟器抵达的板子,与经调试探针抵达的同一块板子,**差别只在 runner 的 argv,别处 +一个字都不差**。链接脚本、启动代码、内存布局与导出模块是同一块板子。为了改四个字符串 +而发两个包,会把这一切复制一遍,并让两份副本各自漂移。 + +所以环境是一个 **feature**: + +```toml +[features] +default = ["emulator"] +emulator = {} +hardware = {} + +[feature-xlings.emulator] +"xim:qemu-arm" = { version = "9.2.4-1", when = "run" } + +[feature-xlings.hardware] +"xim:probe-rs" = { version = "", when = "run" } +``` + +**`emulator` 是默认,而这是一个关于「谁在读」的决定。** 刚接触这个包的人手边没有 +板子;手边有板子的人有理由把它说出来。一个要求硬件的默认值,会让所有还没买东西的人 +的第一条命令就失败。 + +两张表刻意对称:哪种环境都不是引擎眼里的「正常」,而消费者只下载它所选 feature 需要 +的那一份。 + +**并且两者都在 `run` 档位上 —— 这是第二道独立的闸门。** feature 说**谁**需要这个工具; +档位说**什么时候**。编译固件两者都不需要,只有抵达板子才需要,所以一个只构建、从不 +烧录的 CI 任务**一个字节都不下载**。 + +## C 库也是一个 feature + +每一行 `thumb*-none-eabi*` 的 C 库列都是空的,所以面向它的工程一开始**没有 libc**, +除非它开口要。BSP 停在这一档:它不引用任何 C 库符号,它的控制台走 semihosting 而不是 +`stdio`。 + +```toml +libc = {} + +[feature-deps.libc] +picolibc.picolibc = "1.8.12.3" +``` + +C 库以**源码包**的形式到达,用程序自己的旗标编译,于是没有 multilib 要匹配、也没有 +ABI 约定会弄错。`mcpp run --features libc` 就是全部;不选它,零 libc 档原样保留。 + +## 构建程序 + +```cpp +int main() { + mcpp::link_script("cortex-m.ld"); + mcpp::rerun_if_changed("cortex-m.ld"); + + const std::string target = mcpp::target() ? mcpp::target() : ""; + if (mcpp::has_feature("hardware")) { + for (auto a : {"probe-rs", "run", "--chip", "STM32L475VG"}) + mcpp::runner(a); + mcpp::run_exclusive(); + } else { + /* 这个目标对应的模拟器 argv */ + } + return 0; +} +``` + +**哪个机器型号对应哪个目标,是一张表,不是一个默认值。** 为 `thumbv6m` 构建的镜像 +在实现 `thumbv7em` 的型号上跑不起来,而猜错的结果是它启动了、然后在毫不相干的地方 +faul。表里没有这一行时要**点名报错**,而不是静默缺席 —— 一个没有配置 runner 的 BSP +会让 `mcpp run` 报「缺少 runner」并建议加一个 `runner` 键,那句话一般而言是对的,而 +在这里不是原因。 + +**写程序名,不写路径。** mcpp 会搜索图中任何包声明过的每一个载荷的 `bin/`,然后才是 +`PATH`,并且能准确报出它搜过哪些目录。用安装目录拼出来的绝对路径引入了一种失败形态: +查找返回空,而没有任何东西说明为什么。 + +**引擎不认识上面任何一个 runner 名字。** `flash`、`serve`、`erase` 是这个包的词汇; +另一个包提供的 `serve` 可以是别的意思,而两者都不必被 mcpp 知道。 + +**一个探针,一个使用者。** `mcpp::run_exclusive()` 声明这个目标的运行不可重叠, +`mcpp test` 随后把它们串行化。工程方永远不需要记得加 `-j1`。 + +## 当前边界 + +- BSP 把它的模拟器与探针驱动声明为载荷,因此工具链未在某个平台发布的板子,就无法 + 从那个平台抵达。 +- 机器型号表是按三元组组织的。需要表里没有的型号的板子,要改 BSP,而不是在工程侧 + 覆盖。 diff --git a/docs/zh/13-baremetal.md b/docs/zh/40-baremetal.md similarity index 96% rename from docs/zh/13-baremetal.md rename to docs/zh/40-baremetal.md index 47166623..64d2c778 100644 --- a/docs/zh/13-baremetal.md +++ b/docs/zh/40-baremetal.md @@ -1,11 +1,19 @@ -# 13 — 裸机与 freestanding 目标 +# 40 —— 裸机与 freestanding 目标 + +**读者:**面向没有操作系统的机器的嵌入式开发者。 + +**本章回答的那一个问题:**一个工程怎样为 freestanding 目标构建、谁供给这个目标的 +整个世界,以及标准库还剩下什么。 + +**不在这里:**运行与测试这个镜像,那是 [41 —— 抵达一台设备](41-devices.md);以及 +hosted 的交叉构建,那是 [24 —— 基于 openkal 的交叉构建](24-openkal-cross.md)。 本文说明 mcpp 如何为没有操作系统的目标构建、运行与测试软件,以及板级支持包如何 提供引擎刻意不去掌握的那部分目标事实。 -相关文档:[05 — mcpp.toml 清单指南](05-mcpp-toml.md) §2.7.2 是本文使用的 -`[target.]` 各键的参考;[07 — build.mcpp 构建程序](07-build-mcpp.md) -是板级支持包所用指令协议的参考;[08 — 工具链内部机制](08-toolchain-internals.md) +相关文档:[04 — mcpp.toml 清单指南](04-mcpp-toml.md) §2.7.2 是本文使用的 +`[target.]` 各键的参考;[30 — build.mcpp 构建程序](30-build-mcpp.md) +是板级支持包所用指令协议的参考;[91 — 工具链内部机制](91-toolchain-internals.md) 描述了本文所偏离的宿主链接模型。 ## 概述 @@ -442,9 +450,12 @@ C 库经过优化的那份,且没有任何提示。实测(picolibc 在场):冷 ### 在目标上运行测试 -`mcpp test` 为每个 `tests/*.cpp` 构建一个独立镜像,在板级支持包提供的模拟器里运行, -并以退出码为判据。semihosting 把固件 `main` 的返回值传递到模拟器的退出码,因此其 -心智模型与宿主上的测试运行完全一致。 +测试在这里的运作方式与 [08 —— 测试](08-testing.md) 所述完全一致,本节只补充板子 +特有的部分:每个 `tests/*.cpp` 成为它自己的一个镜像,由板级支持包提供的 runner +执行它。 + +让判据成立的是 semihosting —— 它把固件 `main` 的返回值传递到模拟器的退出码。这正是 +这个模型与宿主上的运行**完全一致**而不只是相似的原因。 ```bash mcpp test @@ -558,7 +569,7 @@ error: no runner is configured for 'riscv64-none-elf' — a freestanding artifac 这个键不限于裸机。hosted 交叉目标 —— x86_64 宿主上的 `aarch64-linux-musl` 产物 —— 使用同一个 `[target.].runner`,以 `qemu-aarch64-static` 这类用户态模拟器代替 系统模拟器;在这类目标上,缺少 runner 在内核拒绝产物之前不是错误。hosted 目标的规则、 -`--no-runner` 出口与 `mcpp test` 的未运行报告见 [5 —— mcpp.toml](05-mcpp-toml.md) +`--no-runner` 出口与 `mcpp test` 的未运行报告见 [04 —— mcpp.toml](04-mcpp-toml.md) §2.7.3。 ## 编写板级支持包 @@ -591,7 +602,7 @@ if (const char* dir = mcpp::xpkg_dir("xim", "qemu-riscv"); dir && *dir) { ``` 没有那一行,构建会成功、不配置 runner,而 `mcpp run` 报告缺少 runner 并建议写一个 -`runner` 键 —— 这句话一般情况下对,在这里不对。见 [07 —— build.mcpp](07-build-mcpp.md) +`runner` 键 —— 这句话一般情况下对,在这里不对。见 [30 —— build.mcpp](30-build-mcpp.md) 的 `mcpp:warning=`。 ### 板级支持包发出的指令 @@ -711,3 +722,4 @@ int main() { | 替换 C 库 | 自 2026.8.20.2 起可经 `[target.].sysroot` 表达,而**仅空值一侧经过验证**(零 libc 档)。指向另一份 C 库同样被接受并经同一通道安装,但生态中没有第二份裸机 C 库,该路径未经测试。 | | `win32-arm64` 上的 `qemu-riscv` | 上游包未为该宿主发布资产,因此在其上安装会失败。该失败是正确的而非静默的,但该宿主无法运行裸机镜像。 | | 生态侧 CI 广度 | 两个生态包各自的 CI 只跑 `ubuntu-24.04`。mcpp-index 的 `tests/examples/` workspace 成员在三个平台上无条件运行且没有能力门,因此需要模拟器与目标 sysroot 的包无法加入其中。这是一个已知的覆盖缺口。 | + diff --git a/docs/zh/18-devices.md b/docs/zh/41-devices.md similarity index 82% rename from docs/zh/18-devices.md rename to docs/zh/41-devices.md index a4cff365..a56d48ab 100644 --- a/docs/zh/18-devices.md +++ b/docs/zh/41-devices.md @@ -1,10 +1,19 @@ -# 18 - 抵达一台设备 +# 41 —— 抵达一台设备 + +**读者:**产物不在构建它的那台机器上运行的人 —— 一块板子、一个模拟器,或者一台 +远程主机。 + +**本章回答的那一个问题:**这样的产物怎样被运行与测试,以及一个包供给什么,才能让 +一条命令抵达它。 + +**不在这里:**这个目标本身怎么构建,那是 [40 —— 裸机](40-baremetal.md);以及 mcpp +认为什么是一个测试,那是 [08 —— 测试](08-testing.md)。 本文规定 mcpp 如何执行一个运行在构建机器之外的产物、包如何提供抵达它的其他方式, 以及工程如何在模拟器与真实硬件之间选择。 -相关文档:[13 - 裸机与 freestanding 目标](13-baremetal.md) · [07 - build.mcpp](07-build-mcpp.md) -· [11 - 机器输出](11-machine-output.md)。 +相关文档:[40 - 裸机与 freestanding 目标](40-baremetal.md) · [30 - build.mcpp](30-build-mcpp.md) +· [50 - 机器输出](50-machine-output.md)。 ## 一条命令,加具名的例外 @@ -34,7 +43,7 @@ runner,于是开发者从模拟器换到真板时,敲的命令不变。 `logcat` 对它一样陌生:它只知道「包可以提供具名 runner」这件事,然后执行它找到的 argv。**引擎里若有一份固定的名字表,就等于由引擎决定哪些领域可被表达。** -## 包提供什么 +## 包的供给内容 ```cpp mcpp::runner("qemu-system-arm"); // 默认:一次一个 token @@ -54,7 +63,7 @@ mcpp::run_exclusive(); // 这个目标的运行不能重叠 失败模式 —— **声明不是安装**,查询可能返回空,于是没有配置任何 runner 而没有任何 话说明原因。写程序名则让 mcpp 报出它究竟搜过哪些目录。 -## 工程覆盖什么 +## 工程的覆盖项 ```toml [target.thumbv7em-none-eabihf] @@ -68,7 +77,7 @@ monitor = ["probe-rs", "attach", "--chip", "STM32L475VG"] 优先级是通常那个:工程作者写的胜过依赖提供的,且覆盖会被报告。一个名字只允许一个 依赖提供,第二个是点名两个包的错误。 -## 是否终止由声明决定,不由推断 +## 终止由声明决定,不由推断 | | 含义 | |---|---| @@ -132,3 +141,12 @@ cortex-m-rt = { version = "0.1.0", features = ["hardware"] } **这不需要任何引擎机制。** 引擎读 runner,对模拟器与探针一无所知; `mcpp::has_feature` 本来就在。**一个问题不必新增任何东西就能回答,是分层按规定在 起作用。** + +## 当前边界 + +- **同一个 runner 名字只允许一个依赖提供。** 出现第二个是错误,并会点名两个包; + 没有任何「按顺序取胜」的规则。 +- 所选环境没有提供的 runner **就是不存在**。在 emulator feature 下没有调试探针, + 于是 `mcpp run --runner debug` 会报告没有这个 runner,并列出确实存在的那些。 +- 终止方式是被声明的,不是被推断的。一个不声明自己长驻的 runner,会被一直等到 + 操作者结束它。 diff --git a/docs/zh/20-heterogeneous-builds.md b/docs/zh/42-heterogeneous-builds.md similarity index 95% rename from docs/zh/20-heterogeneous-builds.md rename to docs/zh/42-heterogeneous-builds.md index f5654d77..6ca6757c 100644 --- a/docs/zh/20-heterogeneous-builds.md +++ b/docs/zh/42-heterogeneous-builds.md @@ -1,9 +1,18 @@ -# 20 — 异构硬件构建 +# 42 —— 异构硬件构建 + +**读者:**把程序的一部分编译到 GPU 或 AI 加速器上的人。 + +**本章回答的那一个问题:**设备代码怎样被编译并链接进一个普通程序,以及一个预建 +产物怎样声明它能在哪些设备上运行。 + +**不在这里:**编写驱动设备编译器的那条规则,那是 +[31 —— 编写规则包](31-authoring-a-rule-package.md);以及抵达设备把它跑起来,那是 +[41 —— 抵达一台设备](41-devices.md)。 GPU 与 AI 加速器目标,以及宿主/设备混合编译:mcpp 如何构建设备代码,以及一个预建产物如何 声明它能在哪些设备上运行。 -## 支持什么 +## 支持范围 一个构建点名它面向哪些设备后端,而且**可以点多个**: @@ -26,7 +35,7 @@ accel = "cuda12.9+{sm_89}, vulkan1.2" 「各条 lane」那张表写明每个驱动哪个编译器、需要哪些载荷。引擎里不持有任何厂商名字, 所以第六个是**一个包**而不是一次引擎改动。 -## 两种形态,以及为什么一套机制够到两者 +## 两种形态,以及两者共用的机制 加速器工具链有两种形态。它们描述的是**一个工具链通常怎么被使用**,不是一个构建系统 需要几套机制。 @@ -87,7 +96,7 @@ C++20 modules 的编译器。 上表是 mcpp **不需要被告知**就知道的那些:在「包可以自己声明」之前就已经支持的语言。 规则包通过 `[features]..device_extensions`(见 -[05 — mcpp.toml](05-mcpp-toml.md) §2.8)向它增补,而这正是**一门新设备语言到达的方式** +[04 — mcpp.toml](04-mcpp-toml.md) §2.8)向它增补,而这正是**一门新设备语言到达的方式** —— 不动引擎,也不需要发一版引擎。Slang 是第一个:`.slang` 不在上表里,由 `mcpp:plugins` 的 `rules-slang` 声明。 @@ -171,7 +180,7 @@ mcpp 2026.9.7.1 之前,那条 lane 是唯一的例外:生成的头文件**就是 所以规则不是「接口要生成」也不是「接口要手写」。规则是:**机械的接口生成,设计出来的 接口手写,而两种情况下头文件都是没有任何消费者会写出其名字的中间产物。** -#### 一份载荷以什么名字到达 +#### 载荷到达时的名字 模块名与命名空间是同一条标识符路径,由工程已经写下的名字推导。 @@ -196,7 +205,7 @@ mcpp 2026.9.7.1 之前,那条 lane 是唯一的例外:生成的头文件**就是 调用设备编译器的那条命令不内置在 mcpp 里,而是由**构建规则包**提供 —— 以 `host-module = true` 消费,emit 输出汇入链接的构建边。机制见 -[07 — build.mcpp](07-build-mcpp.md),可用的 CUDA 规则见 `examples/09-heterogeneous/cuda`。 +[30 — build.mcpp](30-build-mcpp.md),可用的 CUDA 规则见 `examples/09-heterogeneous/cuda`。 这个划分是刻意的。mcpp 拥有构建图、产物身份与架构集合;厂商的 flag 拼法、 架构语法与宿主编译器要求属于规则包。 @@ -239,7 +248,7 @@ plugins = { version = "0.3.0", features = ["rules-cuda"], host-module = true } ``` 更近的声明赢,装一个版本,并且 mcpp 说出用了哪条。不满足规则下界的钉会被拒绝并点出 -两侧,而不是与它并排装下来。完整规则见 [05 — mcpp.toml](05-mcpp-toml.md) 的 +两侧,而不是与它并排装下来。完整规则见 [23 — The Project Environment](23-the-project-environment.md) 的 「一个包一个版本」;`examples/09-heterogeneous/multi-backend` 是本仓库里唯一走覆盖 路径的示例,其余每一个都只写那条边。 @@ -267,7 +276,7 @@ mcpp 里长出第二份拷贝)。 干净、链接干净,到第一次分配才以 *"CUDA driver version is insufficient for CUDA runtime version"* 失败。规则经驱动自己的库(经由 sentinel 包够到,绝不经 `/usr/lib`)读出驱动 版本并陈述为事实;陈述它的运行时需要的下界;引擎在编译任何东西之前比较两者 —— 见 -[07 — build.mcpp](07-build-mcpp.md) 的探针通道。引擎读到的是一个名字、一个关系、一个 +[30 — build.mcpp](30-build-mcpp.md) 的探针通道。引擎读到的是一个名字、一个关系、一个 版本;`cuda.driver` 是流过引擎的数据。 凡是错答比不答更贵的地方都只报告不强制:工程里没有规则包的机器没有任何厂商相关的话 @@ -295,7 +304,7 @@ accelerators = ["cuda", "rocm"] 它与产物的 `accel` 刻意是不同的字段 —— 声明由人手写、可以是期望值, 而产物的字段是从产生它的那次构建**测量**出来的。 -## 一个预建产物声明了什么 +## 预建产物的声明内容 携带设备代码的产物把它记在兼容性标签旁边: @@ -314,7 +323,7 @@ accel = "cuda12.8+{sm_80,sm_90f} ptx>=90" `accel` 缺席表示该产物不携带设备代码,因而不施加任何约束 —— 这就是为什么一个纯 CPU 的库可被任何构建使用。 -### 消费者如何被匹配 +### 消费者的匹配 当一次构建请求的每个后端,产物都声明了该后端、工具包主版本一致、 且覆盖了请求的每个架构时,该产物满足这次构建。一个架构被覆盖,当它被直接列出, @@ -475,7 +484,7 @@ sources = ["src/cpu/*.cpp"] 今天由发布者显式写这个字段,索引描述符就是这么做的; 等 `kind = "device"` 把设备编译放进 mcpp 之后,`mcpp pack` 才会发它。 -## 各条 lane,以及每条驱动什么 +## 各条 lane 及其驱动的工具链 一个规则包拥有一个编译器的拼写。引擎不认识其中任何一个名字: `tests/unit/test_core_vendor_probes.cpp` 断言剥掉注释后 `src/` 里不出现任何厂商工具名, @@ -507,7 +516,7 @@ CUDA 入口点的内联包装,所以目标文件链接的是 CUDA 运行时,机 重复符号检查会报出它们共有的那些 unwinder 符号。任何东西都不得穿过接缝:SYCL 异常在 设备编译单元里被捕获并转成返回码,因为抛出它的那个运行时不是调用方会用来展开的那个。 -## 每条 lane 到得了哪些平台 +## 每条 lane 到达的平台 一条 lane 在某个平台上成立,要三件事同时为真:设备编译器为它发布了、产物需要的运行时 在那里够得到、以及这条规则自己那段按宿主分岔的代码在那里编译得过。第三件是最容易被默认 @@ -516,7 +525,7 @@ CUDA 入口点的内联包装,所以目标文件链接的是 CUDA 运行时,机 下载,只问六个模块编不编得过);它把三处潜伏的宿主差异变成了对应 runner 上的编译错误, 而三处没有一处是 Linux 构建看得见的。 -| lane | Linux | macOS | Windows | 由什么决定 | +| lane | Linux | macOS | Windows | 决定因素 | |---|---|---|---|---| | `rules-spirv` | 是 | 是 | 是 | 着色器编译器三个平台都有发布:Linux 上 `xim:glslang`,macOS arm64 与 Windows x86_64 上 `xim:shaderc` | | `rules-cuda` | 是 | 否 | 是 | NVIDIA 为 Linux 与 Windows 发布可再分发组件,而自 CUDA 10.2 之后没有为 macOS 发布过工具包 | @@ -556,7 +565,7 @@ CUDA 工具包。生态能做的是在一次构建请求跨过那条边界的地 私有 loader 后面,于是驱动包装的厂商库必须被搬到产物自己的搜索路径上。macOS(dyld) 与 Windows(PE loader)按构造就没有这一层,面向它们的项目一条适配声明都不写。 -## 在此之上,一个框架是什么形状 +## 建立在此之上的框架形态 五条 lane 证明了规则包能驱动五个编译器,其中最新的一个来自 NVIDIA 与 Khronos 两个 谱系之外的厂商。框架是下一个问题 —— 这套机制能不能扛起一个 @@ -588,18 +597,15 @@ Mesa 的 lavapipe 仅因类型就被排除 —— 尽管它声明了后端要求 `ggml-org:llamacpp` 以 `backend-vulkan` feature 承载这一整套。 -## 尚未实现 +## 当前边界 -岛形态的 device target 及其隐含的 device link、OpenMP offload 与 stdpar、HIP 的 AMD -平台,以及 Metal。 +- 岛形态的 device target,及其隐含的 device link(RDC)。 +- OpenMP `target` offload,以及 stdpar。 +- HIP 的 AMD 平台。`rules-hip` 只到达 NVIDIA 平台。 +- Metal(`.metal`)与 OpenCL C(`.cl`)。两个扩展名都被归类为设备源,而没有任何 + 已发布的规则包认领它们,因此声明了其中之一的构建会被拒绝,并点名该文件。 +- CUDA 的 13.x 线在 Windows 上。Windows 承载的是 12.x 线。 +- `mcpp pack` 不产出 `accel` 字段。需要它的发布方写进描述符。 -另有两处缺口是按平台而不是按编程模型分的,而且两处都是打包工作不是引擎工作。 -**HIP 上 Windows** 需要给 NVIDIA 平台的头文件包与 `cuda-profiler-api` 补 Windows 段, -以及给那一步「把头文件声明进去」补一个 Windows 形态 —— 它今天写进的是一个 Linux 的 -SubOS 视图。**CUDA 的 13.x 线上 Windows** 需要给后端的重新拼合补一个 Windows 形态: -那条线上,上游把 `nvvm/` 与 `crt/` 从 `cuda_nvcc` 里拆成四个单独发布的组件,索引用符号 -链接把它们拼回去,而 `ln` 不是那个宿主上的命令。12.x 线自带后端,一样都不需要,所以 -Windows 承载的是那条线,并在每个配方里显式声明为有意的分歧,而不是留给人从文件里读。 +每条 lane 的按平台边界见*每条 lane 到达哪些平台*一节的表格。 -这些所依据的设计,以及每一项仍然开着的理由,见 -`.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md`。 diff --git a/docs/zh/11-machine-output.md b/docs/zh/50-machine-output.md similarity index 93% rename from docs/zh/11-machine-output.md rename to docs/zh/50-machine-output.md index 00920ab8..f0a2793a 100644 --- a/docs/zh/11-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -1,10 +1,16 @@ -# 11 — 机器可读输出 +# 50 —— 机器可读输出 + +**读者:**要解析 mcpp 输出的工具、编辑器扩展或 CI 任务的作者。 + +**本章回答的那一个问题:**一个程序可以依赖什么、它怎样被版本化、以及协议怎样被 +识别。 + +**不在这里:**面向人的输出 —— 它不带任何兼容性承诺;以及退出码契约,那是 +[SPEC-003](../specs/exit-codes.md)。 mcpp 面向两类读者。本章是对第二类 —— **程序** —— 的契约。编辑器扩展、CI 脚本,以及任何解析 mcpp 输出的程序, 可依赖的部分在此列出。 -设计与背后的实测:`.agents/docs/2026-08-08-machine-readable-output-protocol-design.md`。 - ## 1. 首要规则 > **靠解析 stdout 来识别协议。不要靠退出码,也不要靠「命令没失败」。** @@ -193,7 +199,7 @@ capability,其 `outputSchema` 全部只有 `{"exitCode": integer}`,而客户端 ## 8. 各 kind -### `mcpp.env` —— mcpp 把东西放在哪 +### `mcpp.env` —— mcpp 使用的路径 ``` mcpp self env --format json @@ -242,7 +248,7 @@ mcpp cache list --format json `data` 是 `{root, entries[]}`,与 `--json` 裸打印的一致。 -### `mcpp.toolchain.list` —— 装了什么,以及这台宿主服务哪些目标 +### `mcpp.toolchain.list` —— 已安装的工具链,以及这台宿主服务的目标 ``` mcpp toolchain list --format json @@ -259,7 +265,7 @@ mcpp toolchain list --format json 没有约定的行为空。`x86_64-linux-gnu` 装了 gcc 而根本没有约定,所以要挑「约定是 gcc 的行」必须读 `pin`。 -### `mcpp.why.toolchain` —— 一对 (目标, 工具链) 会解析成什么 +### `mcpp.why.toolchain` —— 一对 (目标, 工具链) 的解析结果 ``` mcpp why toolchain [--target ] [--toolchain ] --format json @@ -312,7 +318,7 @@ mcpp why toolchain [--target ] [--toolchain ] --format json 从 `gnu` 变为 `glibc`,Windows 上从 `gnu` 变为 `ucrt`。字段的**含义**没变(它仍然是 「哪个实现」),所以 §7 仍然成立;变的是它不再报三元组的 env 段 —— 那是一次请求而 不是一个实现,也不是任何一个 C 库的名字。现在的取值就是 -[14 —— 目标侧](14-target-side.md)一直列着的那些,并且包可以在 +[22 —— 目标侧](22-target-side.md)一直列着的那些,并且包可以在 `cfg(c-abi = …)` 谓词里与它们比较。按字面量 `gnu` 取值的客户端需要更新; `musl`、`picolibc`、`libSystem` 不受影响。 @@ -392,3 +398,12 @@ mcpp test [pattern] [--workspace] --message-format json `workspace_summary` 增加 `tests_not_run`(各成员之和)与 `unrunnable_members`(所有 测试都 `not_run` 的成员),与既有的 `not_run` 列表并列;后者仍然指 `--workspace-timeout` 到达时尚未开始的成员。 + +## 当前边界 + +- **退出码表的作用域只到它点名的那些命令。** 别的命令返回的码不在表里,而把它加进来 + 等于记录一件那些命令承诺不了的事。 +- 没有位置的诊断会**省略** `path` 与 `range`,而不是送 0:`line: 0` 会指向一个不存在 + 的位置。 +- stdout 上没有 JSON 就意味着「不支持」,无论原因是什么 —— 旧版引擎、未知选项,或者 + 这条命令根本没有机器格式。客户端仅凭这条流分辨不出这三者。 diff --git a/docs/zh/19-supported-versions.md b/docs/zh/51-supported-versions.md similarity index 76% rename from docs/zh/19-supported-versions.md rename to docs/zh/51-supported-versions.md index 0556fccb..f0a3588f 100644 --- a/docs/zh/19-supported-versions.md +++ b/docs/zh/51-supported-versions.md @@ -1,4 +1,11 @@ -# 19 - 受支持的版本与兼容性 +# 51 —— 受支持的版本与兼容性 + +**读者:**带着政策问题来的人 —— 一次评审流程、一次隔网构建、一个被钉住的版本。 + +**本章回答的那一个问题:**哪些发布受支持、版本之间什么可以变、什么不可以。 + +**不在这里:**退出码表,那是 [SPEC-003](../specs/exit-codes.md);以及机器输出的 +schema,那是 [50 —— 机器可读输出](50-machine-output.md)。 本文陈述哪些发布受支持、支持多久、以及版本之间什么可以变化。它之所以存在,是因为 一个采用 mcpp 的工程会被它自己的评审流程问到这些问题,而只活在维护者脑子里的答案 @@ -21,7 +28,7 @@ 一个发布是被取代,而不是被撤回。更旧版本的已发布资产与索引条目原样保留,因为某个 工程可能钉住了它,移除会弄坏一个本来正常的构建。 -## 版本之间什么可以变化 +## 版本之间允许的变化 引擎自身的各个接口稳定性并不相同,这个差别值得精确陈述。 @@ -29,7 +36,7 @@ |---|---| | `mcpp.toml` 的键 | 只增。既有键保持其含义;无法识别的键会被报告,绝不静默忽略 | | CLI 命令与旗标 | 只增。被取代的拼写保留为别名 | -| 机器输出(`--message-format json`) | 由 `schemaVersion` 版本化,见 [11](11-machine-output.md) | +| 机器输出(`--message-format json`) | 由 `schemaVersion` 版本化,见 [50](50-machine-output.md) | | `build.mcpp` 指令协议 | 版本化,见 `kProtocolVersion`。引擎遇到声明**更高**版本的程序会拒绝,而不是猜测 | | `mcpp.lock` 格式 | 由 `schemaVersion` 版本化;更旧的文件在读取时迁移 | | 目标表的行 | 只增。一行的档位可以上升;只要还有已发布的包以它为目标,该行不会被移除 | @@ -76,3 +83,12 @@ mcpp emit sbom -o sbom.json 缺陷与安全报告提交到拥有该组件的仓库的 issue tracker —— 引擎、包索引,或者那个包 自身。一份点明了版本、宿主、目标与命令的报告是可执行的;没有这四样的报告通常换来 一次索要它们的回复。 + +## 当前边界 + +- **`mcpp.lock` 目前还不约束解析。** 它记录一次构建解析出的结果,`--locked` 核验新的 + 解析与它一致;把锁作为解析的**输入**来钉住结果,是对解析器的另一项改动。 +- SBOM 描述的是**已记录的**那次解析,而不是重新解析一次,所以在改了 manifest 之后、 + `mcpp update` 之前写出的文档,描述的是更早的那张图。 +- 许可证 mcpp 不认识的组件会被写成 `NOASSERTION` 而不是省略,因为缺失的字段读起来 + 是「没有检查过」。 diff --git a/docs/zh/90-build-from-source.md b/docs/zh/90-build-from-source.md new file mode 100644 index 00000000..b9cc2a28 --- /dev/null +++ b/docs/zh/90-build-from-source.md @@ -0,0 +1,175 @@ +# 90 —— 从源码构建与参与贡献 + +**读者:**要构建并修改 mcpp 本身的贡献者。 + +**本章回答的那一个问题:**mcpp 怎样从源码构建、它自己的测试怎样组织,以及一次 +贡献要满足什么。 + +**不在这里:**用户怎样构建自己的工程,那是 [01 —— 快速开始](01-getting-started.md); +以及一次发布怎样切出来,那是 [92 —— 发布 mcpp](92-release.md)。 + +> mcpp 采用自托管模式 —— 通过 mcpp 自身从源码构建 mcpp。 +> 任何已具备可运行 mcpp 二进制的环境均可完成源码构建。 + +## 准备 + +参照 [01 — 快速开始](01-getting-started.md) 安装一份现成的 mcpp, +然后克隆仓库: + +```bash +git clone https://github.com/mcpp-community/mcpp +cd mcpp +``` + +## 构建与测试 + +```bash +mcpp build # 使用现成 mcpp 编译当前源码 → ./target/.../bin/mcpp +mcpp run -- --version # 运行刚构建出的产物 +mcpp test # 构建并运行 tests/**/*.cpp 中发现的 C++ 测试(包含 tests/unit) +``` + +`mcpp test` 不执行 `tests/e2e/` 下的 shell 端到端套件;应单独让它使用刚构建的二进制。 + +首次构建会自动拉取默认工具链,详见 +[20 — 工具链管理](20-toolchains.md)。 + +如需生成与 release 一致的全静态二进制(对应 `release.yml` 走的路径): + +```bash +mcpp build --target x86_64-linux-musl +# → target/x86_64-linux-musl/.../bin/mcpp 为全静态 ELF +``` + +## 源码结构 + +mcpp 是一个**工作空间**。`modules/` 里是引擎 import 的九个包;`src/` 是引擎本身。 + +``` +modules/ 工作空间成员,被 src/ import +├── manifest/ manifest 与描述符解析 +├── platform/ 操作系统抽象 +├── toolchain-model/ 三元组、方言、指纹、链接模型 +├── buildmcpp/ build.mcpp 契约:协议、指令表、provisions +├── source-kind/ 源文件角色分类 +├── versioning/ 本二进制的版本,以及 SemVer 约束解析 +├── dyndep/ ninja dyndep 产出 +├── libs/ 内嵌的文本格式解析器 +└── log/ 分级日志 + +src/ +├── main.cpp 入口 +├── cli.cppm cli/ 命令分发与各条命令 +├── build/ 构建编排与 ninja 后端 +├── modgraph/ P1689 模块扫描与依赖图 +├── pm/ 解析器与包管理命令 +├── toolchain/ 探测、指纹、std 模块 +├── pack/ publish/ mcpp pack、mcpp publish 与 xpkg 产出 +├── fetcher/ fallback/ 下载、安装、回退解析 +├── bmi_cache/ 跨工程 BMI 缓存 +├── runtime/ xlings/ 运行时契约与 xlings 桥 +├── freestanding/ 裸机目标、链接行与 runner +└── scaffold/ `mcpp new` 与模板 + +tests/ +├── unit/ 108 个 C++ 测试,由 `mcpp test` 发现 +└── e2e/ 370 个对真实二进制运行的 shell 脚本 +``` + +## 测试组织 + +两层,回答的是不同的问题。 + +**单元测试**(`tests/unit/`,108 个)是 `mcpp test` 发现的 C++ 程序。它们检验一个模块 +的契约,不需要二进制、也不需要文件系统状态。 + +**端到端测试**(`tests/e2e/NN_.sh`,370 个)拿真实的 `mcpp` 二进制跑真实工程。 +`run_all.sh` 是 CI 的入口。`mcpp test` **不**运行它们。 + +```bash +MCPP=<新构建的 mcpp 二进制> bash tests/e2e/02_new_build_run.sh +``` + +**由能力闸门决定哪些会跑。** e2e 脚本开头几行声明它需要什么,不具备该能力的 runner +会跳过它: + +```bash +#!/usr/bin/env bash +# requires: elf gcc +``` + +在用的有 `gcc`(80 个脚本)、`elf`、`unix-shell`、`llvm`、`jq` 与 `fresh-sandbox`。 +一个要求「任何 CI job 都不提供的能力」的脚本**在任何地方都不会跑**,它的绿色什么都不 +说明 —— 新增脚本时要核对确有 job 供给它所要的能力。 + +## 写一条真的在测东西的判据 + +本仓库最可迁移的一条规则,也是被跳过时会静默失效的那一条: + +> **写完判据之后,把修复拿掉,跑一次。** 从未被看见失败过的判据,不能说它测到了什么。 + +它能抓住三种形态,每一种在这里都至少发生过一次: + +| 形态 | 形式 | +|---|---| +| 判据从没跑到 | 测试被闸在一个没有任何 job 提供的能力上 | +| 判据不可能失败 | 子串搜索,任何措辞都能满足它 | +| 判据施加在错误的对象上 | 夹具目录跨次累积,于是搜索回答的是更早那次构建 | + +也要说出分母。「表里每一台宿主都被扫过」是一条判据;「扫描没发现问题」不是 —— 因为 +空的枚举同样什么都发现不了。 + +## 测试之外 CI 还跑的检查 + +`.github/tools/` 里有十八个脚本。第一个 PR 之前值得知道的有四个: + +| 脚本 | 拒绝的内容 | +|---|---| +| `check_docs_style.sh` | 疑问句标题、参考章节里的第二人称、标题结构落后于英文的中文页 | +| `check_docs_structure.sh` | 章节引用设计记录、解析不到的 `docs/NN-*.md` 路径、翻译里少掉的表格 | +| `check_version_pins.sh` | 版本号只写在一处而别处没跟上 | +| `check_modules_wiring.sh` | 只接进了「三处必须知道它的地方」中的一部分的工作空间成员 | + + +## 新增一个 manifest 字段:准入标准 + +> **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。 + +- mcpp 只定义**机制**(features 并集/闭包、capability require/provide/override、 + profile→编译器旗标、platform→triple),键与形状固定;feature 名、能力名、 + 后端名等**领域词汇只出现在值里**,不进 mcpp 代码。 +- **不支持包自定义 toml 键**:键合法性不得依赖"先解析目标包",否则 manifest + 失去静态可解析性(lockfile/LSP/审计的前提)。包的扩展点 = 固定机制内的开放值域。 +- 包级旋钮统一收敛进 features;糖键(如 `backend=`)进入核心语法须满足: + ① 领域中立(跨生态通用模式)② 1:1 脱糖、零新增解析语义。 + +## Issue 与 PR 提交规范 + + + +### Issue + +提交至 [github.com/mcpp-community/mcpp/issues](https://github.com/mcpp-community/mcpp/issues), +建议附带以下信息: + +- `mcpp self env` 的完整输出 +- 失败命令的完整输出(配合 `MCPP_LOG_LEVEL=debug` 可获得更详细信息) +- 操作系统、发行版、glibc 版本(可通过 `ldd --version` 查看) + +### Pull Request + +mcpp 处于早期迭代阶段,接口可能调整,提交 PR 前请注意: + +1. 涉及 CLI 或 `mcpp.toml` schema 的改动,建议先开 issue 对齐方向。 +2. 单个 PR 聚焦单一改动;commit 标题使用英文 imperative 形式 + (`fix: ...` / `feat: ...`)。 +3. 行为改动或测试文档改动在提交前运行 `mcpp test`,并让相关 E2E 脚本使用刚构建的 + 二进制通过;纯文档改动复核示例和链接,并用 `gh pr checks ` 确认 PR + 实际 required checks。 + +## 社区资源 + +- [社区论坛](https://forum.d2learn.org/category/20) +- 交流群 QQ: 1067245099 +- [mcpp-index](https://github.com/mcpplibs/mcpp-index) — 默认包索引 +- [mcpplibs](https://github.com/mcpplibs) — 配套的模块化 C++ 库集合 diff --git a/docs/zh/08-toolchain-internals.md b/docs/zh/91-toolchain-internals.md similarity index 92% rename from docs/zh/08-toolchain-internals.md rename to docs/zh/91-toolchain-internals.md index 1bd02391..e7caef4b 100644 --- a/docs/zh/08-toolchain-internals.md +++ b/docs/zh/91-toolchain-internals.md @@ -1,7 +1,15 @@ -# 08 — 工具链机制内幕 +# 91 —— 工具链机制内幕 + +**读者:**贡献者,或者正在排查「工具链为什么解析成这样」的用户。 + +**本章回答的那一个问题:**mcpp 实际上怎样解析、安装并拼装一条工具链,以及每个 +flag 由哪一层决定。 + +**不在这里:**怎样**选择**一条工具链,那是 [20 —— 工具链管理](20-toolchains.md)。 +本章是它底下的机制,而这里没有任何东西是稳定接口。 > 本文详细描述 mcpp 工具链机制的内部工作原理,以及如何扩充新工具链、新架构乃至 -> 嵌入式目标的支持。与面向用户的 [03 — 工具链管理](03-toolchains.md)(CLI 用法) +> 嵌入式目标的支持。与面向用户的 [20 — 工具链管理](20-toolchains.md)(CLI 用法) > 互补,本文面向贡献者与维护者。 ## 1. 一张图看全模型 @@ -62,7 +70,7 @@ xlings 后端解析/自动安装到沙箱 | `payloadPaths` | 由解析出的 runtime binding(§2.1)**精确指名** glibc payload;linux-headers 仍按兄弟 xpkg 发现。没有 binding 就不走 payload-first——这是设计,不是缺陷 | | 运行库目录 | 工具链私有 lib 目录,用于产物的 `-L`/`-rpath` | -### 2.1 runtime binding:绑哪个 libc,只决定一次 +### 2.1 runtime binding:绑定的 libc,只决定一次 payload-first 的构建会链接到某个具体的 glibc,而**是哪一个**是根项目本地开发 OS 的事实, 不该从编译器路径或 shell 状态推断。mcpp 只有两种选择: @@ -162,7 +170,7 @@ mcpp 在**编译与链接**两条线上都发 `--sysroot=`,所以 subos 闭包是一张有序表,每条带来源: -| origin | 例子 | 可变? | 可随产物分发? | +| origin | 例子 | 可变性 | 可随产物分发 | |---|---|---|---| | `payload` | `/xim-x-glibc/2.39/lib64` | 否 —— 装一次不再动 | 否 | | `package` | 依赖描述符的 `[runtime]` 目录 | 否 | 否 | @@ -374,7 +382,7 @@ verdict 以 `.mcpp-runtime-verdicts.json` 存在 `build.ninja` 旁,键包含产 `/xim-x-glibc/2.44/{lib64,lib}`。精确 payload 缺失/陈旧就是错误,其他已安装版本 永远不是回退项。 -### 6.2 一条运行时搜索路径可以住在哪里(`runtime_env_contract.cppm`) +### 6.2 运行时搜索路径的允许位置(`runtime_env_contract.cppm`) 告诉 loader「去哪找」有两条通道,差别不在便利性,而在**波及范围**: @@ -432,7 +440,7 @@ loader;`/bin/sh` 有事:它的 `PT_INTERP` 指向**宿主** loader,而且任何 ### 7.3 嵌入式与裸机工具链 `riscv64-none-elf` 与 `riscv32-none-elf` 已实现,面向用户的说明见 -[13 — 裸机与 freestanding 目标](13-baremetal.md)。本节记录由此得到的形态与 +[40 — 裸机与 freestanding 目标](40-baremetal.md)。本节记录由此得到的形态与 上文 hosted 模型之间的关系。 本节早先的三条预测成立: @@ -464,7 +472,31 @@ C 世界(`CLibMode::Sysroot`)并有自己的 libc++ 链接处理;Windows 没有 mcpp 把运行时 DLL 部署到产物 exe 旁,这正是该平台对 §3–§4 所做一切的原生 等价物。 -### 7.5 一个 flag 由哪根轴决定 +**工程自己产出的共享库**(`kind = "shared"`)确实按格式而不同,而这个差别不是 +flag 的拼法 —— 它是产物记录下的关于它自己的东西: + +| 格式 | 生产方的产出 | 消费方链接的对象 | +|---|---|---| +| ELF | 声明了 soname 时发 `-Wl,-soname,` | `-L` + `-l`、`-Wl,-rpath,$ORIGIN` | +| Mach-O | **总是**发 `-Wl,-install_name,@rpath/` | `-L` + `-l`、`-Wl,-rpath,@loader_path` | +| PE / MinGW | `-Wl,--out-implib,` | **导入库**,且 `-Wl,-Bdynamic` 在前 | +| PE / MSVC | 拒绝(不做自动导出;见 docs/12) | —— | + +其中三行是新增的:在此之前,除 ELF 之外一律被拒绝,原生与交叉都是。要让它们 +可用而不只是被允许,有两处细节必须改。Mach-O 的 install name 默认取该库被**链接** +时的路径,因此「仅在声明了 `soname` 时才发出」会让其余每个 `.dylib` 都记下一个构建 +目录 —— 在构建它的那台机器上没问题,换任何一台就是 `image not found`。而这个选择 +原先由宿主上的 `#if defined(__APPLE__)` 做出,那只在原生 macOS 构建上碰巧正确, +对任何交叉链接都是错的;它现在由目标决定,正如 `target_output` 早已如此。 + +**服务不了的目标会被拒绝**,而不是悄悄按宿主构建:Linux 上的 +`--target x86_64-windows-msvc` 从前会解析到原生 `g++`、写进 +`target/x86_64-linux-gnu/` 并报告成功。词汇表的档位说的是「mcpp 支持这个目标」; +`host_can_serve`(`registry.cppm`)回答的是另一个问题「这台机器能不能产出它」, +而 `prepare.cppm` 现在会问它 —— 显式的 `[target.X] toolchain = "…"` 是作者自备 +交叉工具链时的出口。 + +### 7.5 决定一个 flag 的轴 2026.8.18 那一轮改了四个 flag,每一个此前都挂在错误的轴上。而这类错误的表现 永远相同:**在恰好一个平台上莫名其妙地失败**,报错既不点名那个 flag, @@ -472,7 +504,7 @@ mcpp 把运行时 DLL 部署到产物 exe 旁,这正是该平台对 §3–§4 一共三根轴,而在它们之间做选择的问题是:**这个 flag 最终被谁读到。** -| 轴 | 问题 | 例子 | 怎么问 | +| 轴 | 问题 | 例子 | 提问方式 | |---|---|---|---| | **目标格式** | 产出的是哪种映像 | `-fPIC`(PE 代码本就位置无关;clang 直接拒绝这个 flag) | `triple::parse(...)->is_pe()`,宿主兜底 | | **目标 ABI** | 哪个链接器会消费它 | `--out-implib` vs `/IMPLIB:`、`/DEF:`、SONAME / install-name 的形式 | `is_msvc_target(tc)`、`triple->is_msvc_env()` | @@ -508,4 +540,3 @@ mcpp 把运行时 DLL 部署到产物 exe 旁,这正是该平台对 §3–§4 | hermetic 链接校验 | `src/build/hermetic.cppm` | | 回归fence | `tests/e2e/86_llvm_hermetic_link.sh`、单测 `test_linkmodel.cpp`、`test_post_install.cpp`;`ci-linux-e2e.yml` 的无宿主工具链 CI job | -设计沿革:`.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md`。 diff --git a/docs/zh/09-release.md b/docs/zh/92-release.md similarity index 95% rename from docs/zh/09-release.md rename to docs/zh/92-release.md index 947f5976..76736b5e 100644 --- a/docs/zh/09-release.md +++ b/docs/zh/92-release.md @@ -1,7 +1,15 @@ -# 09 — 发布 mcpp +# 92 —— 发布 mcpp + +**读者:**正在切一次 mcpp 自身发布的维护者。 + +**本章回答的那一个问题:**从一个提交到用户可安装的发布版本要走哪些步骤,以及每一步 +怎样被核验。 + +**不在这里:**把一个包发布到索引,那是 [11 —— 发布一个库](11-publishing-a-library.md); +以及版本之间什么可以变,那是 [51](51-supported-versions.md)。 **mcpp 自身**的发布如何到达用户手上。本章面向维护者;打包**自有项目**见 -[02 — 打包发布](02-pack-and-release.md)。 +[10 — 打包发布](10-pack-and-release.md)。 在此之前这套流程只活在 commit message 和 workflow 注释里,其中一条 commit message 里的诊断是错的,已在 §5 更正。 @@ -158,7 +166,7 @@ $(find "$XLINGS_HOME" -name mcpp -type f -path '*/bin/*' | head -1) --version 记录在案的上限约 40 分钟)。clean-room 里仍然报旧的 `latest` 不是失败,是**还没追上**。 `ci-fresh-install` 的 `wait-index` job 正是把这件事编码成了 15 分钟有界等待。 -## 4. 自举 pin:它是什么,什么时候该 bump +## 4. 自举 pin 的定义与更新条件 `.xlings.json` 的 `[workspace].mcpp` 是**自举的起点** —— 那个由 `xlings install mcpp` 装进 workspace、供 CI 从源码构建 mcpp 的已发布 mcpp。 @@ -189,7 +197,7 @@ curl -fsSL https://github.com/xlings-res/xim-index/releases/download/latest/xim- 推 pin 之前,`index_version` 必须等于 xim-pkgindex `main` 的短 SHA。没有任何东西强制 这一点,而在作业日志里,由此产生的失败与「版本名真的写错了」无法区分。 -## 5. `MCPP_PIN` 改为推导,以及这为什么重要 +## 5. `MCPP_PIN` 改为推导,以及由此产生的结果 `ci-fresh-install.yml` 过去带着 pin 的第二份手工副本。它们从来就不是一回事: `MCPP_PIN` 是**被测版本** —— 永远是最新的已发布版本;而 `.xlings.json` 是 @@ -232,3 +240,4 @@ curl -fsSL https://github.com/xlings-res/xim-index/releases/download/latest/xim- [ ] clean-room XLINGS_HOME:xlings install mcpp@ 成功 [ ] (可选)bump .xlings.json —— 只在此刻,绝不提前 ``` + diff --git a/docs/zh/README.md b/docs/zh/README.md index 67e71c89..e5a8257e 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -2,28 +2,145 @@ [English](../README.md) | **简体中文** -- [00 - 快速开始](00-getting-started.md) -- [01 - 示例项目](01-examples.md) -- [02 - 发布打包](02-pack-and-release.md) -- [03 - 工具链管理](03-toolchains.md) -- [04 - 从源码构建 & 参与贡献](04-build-from-source.md) -- [05 - mcpp.toml 工程文件指南](05-mcpp-toml.md) -- [06 - 工作空间](06-workspace.md) -- [07 - build.mcpp 构建程序](07-build-mcpp.md) -- [08 - 工具链机制内幕](08-toolchain-internals.md) -- [09 - 发布 mcpp](09-release.md) -- [10 - 发布一个库到 mcpp-index](10-publishing-a-library.md) -- [11 - 机器可读输出](11-machine-output.md) -- [12 - 分发预编译库](12-binary-distribution.md) -- [13 - 裸机与 freestanding 目标](13-baremetal.md) -- [14 - 目标侧](14-target-side.md) -- [15 - 基于 openkal 的交叉构建](15-openkal-cross.md) -- [16 - 目标三元组](16-the-target-triple.md) -- [17 - 项目环境](17-the-project-environment.md) -- [18 - 抵达一台设备](18-devices.md) -- [19 - 受支持的版本与兼容性](19-supported-versions.md) -- [20 - 异构硬件构建](20-heterogeneous-builds.md) -- [21 - 按场景选命令](21-commands-by-scenario.md) +这棵树是 **mcpp 已实现功能的使用手册**。每一章陈述一个能力做什么、怎么写、 +当前边界在哪里。设计理由、被否掉的替代方案、以及尚未发布的规划,**不写在这里** +—— 它们属于设计记录,而设计记录不是用户文档。 + +## 文档的归属 + +| 树 | 读者 | 内容 | +|---|---|---| +| `docs/**` | 手上有任务的人 | mcpp 已实现的东西怎么用 | +| [`docs/specs/**`](../specs/README.md) | 对着机制做实现的人:索引作者、下游工具、贡献者 | 语义、约束与匹配规则,每条标注实现状态 | +| `.agents/docs/**` | 做过某次改动的人,以及以后问「为什么是这样」的人 | 推理、实测,以及什么被推翻了 | +| `.agents/skills/**` | 照流程执行的贡献者或 agent | 带判据的步骤 | + +章节引用规范以取得精确语义,但**不引用设计记录**:记录描述的是一个时刻,不带 +稳定性承诺,因此读者需要的东西一律写在本树或规范里。 + +## 从这里开始 + +| 目标 | 阅读 | 运行 | +|---|---|---| +| 写一个程序 | [01](01-getting-started.md)、[04](04-mcpp-toml.md) §1 | [`01-hello`](../../examples/01-hello/)、[`02-with-deps`](../../examples/02-with-deps/) | +| 写一个供他人 import 的库 | [11](11-publishing-a-library.md)、[06](06-features-and-capabilities.md)、[04](04-mcpp-toml.md) §2.4 | [`04-workspace`](../../examples/04-workspace/)、[`11-features`](../../examples/11-features/) | +| 发布它 | [10](10-pack-and-release.md)、[11](11-publishing-a-library.md)、[12](12-binary-distribution.md) | [`03-pack-static`](../../examples/03-pack-static/)、[`05-lib-distribution`](../../examples/05-lib-distribution/) | +| 为另一台机器构建 | [21](21-the-target-triple.md)、[24](24-openkal-cross.md)、[40](40-baremetal.md) | [`06-openkal-cross`](../../examples/06-openkal-cross/)、`mcpp new … --template riscv-virt-rt` | +| 使用 GPU 或加速器 | [42](42-heterogeneous-builds.md)、[41](41-devices.md) | [`09-heterogeneous`](../../examples/09-heterogeneous/),从 [`boundary/`](../../examples/09-heterogeneous/boundary/) 开始 | +| 增加一条规则、一种语言或一个生成器 | [31](31-authoring-a-rule-package.md)、[30](30-build-mcpp.md) | [`08-build-rules`](../../examples/08-build-rules/)、[`12-a-new-device-language`](../../examples/12-a-new-device-language/) | +| 向索引添加一个包 | [11](11-publishing-a-library.md)、[SPEC-001](../specs/package-identity.md) | [09](09-commands-by-scenario.md) —— 发布相关场景 | +| 为别人打包一个工具、一个驱动或一块板子 | [32](32-authoring-a-payload.md)、[33](33-authoring-an-adapter.md)、[34](34-authoring-a-bsp.md) | `xim-pkgindex` 与 `mcpp-index` 里的描述符 | +| 修改 mcpp 本身 | [90](90-build-from-source.md)、[92](92-release.md)、[51](51-supported-versions.md) | — | + +课程也可以以**项目模板**的形式到达:模板由包提供,`mcpp new --template` 实例化 +它。今天有文档的两个是 `riscv-virt-rt`(裸机)与 `ocornut.imgui`(图形应用), +用到它的章节会点名。 + +## 章节 + +首位数字就是部分,所以编号本身说明一章属于哪里: + +| | | +|---|---| +| `0x` | 基础 | +| `1x` | 发布 | +| `2x` | 工具链与目标 | +| `3x` | 扩展 mcpp 与它的生态 | +| `4x` | 设备与加速器 | +| `5x` | 程序可以解析的契约 | +| `9x` | mcpp 自身 | + +同一部分内部的排列是阅读顺序,不是字母序。 + +### 0x —— 基础 + +- [00 —— mcpp 是什么](00-what-mcpp-is.md) —— 它是什么、它做什么,以及一段真的跑过的会话 +- [01 —— 快速开始](01-getting-started.md) —— 安装、创建、构建、运行 +- [02 —— 场景](02-scenarios.md) —— mcpp 被用来做什么,以及每一类工作会用到它的哪些功能 +- [03 —— 示例项目](03-examples.md) —— 哪个示例教什么 +- [04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md) —— manifest 可以说什么 +- [05 —— 依赖与解析](05-dependencies.md) —— 依赖从哪里来,以及哪个版本胜出 +- [06 —— Feature 与能力](06-features-and-capabilities.md) —— 让包的一部分成为可选 +- [07 —— 工作空间](07-workspace.md) —— 多个包,一次构建 +- [08 —— 测试](08-testing.md) —— 包括在本机跑不了的那些 +- [09 —— 按场景选命令](09-commands-by-scenario.md) —— 认识名词之后的查阅入口 + +### 1x —— 发布 + +- [10 —— 发布打包](10-pack-and-release.md) +- [11 —— 发布一个库到 mcpp-index](11-publishing-a-library.md) +- [12 —— 分发预编译库](12-binary-distribution.md) + +### 2x —— 工具链与目标 + +- [20 —— 工具链管理](20-toolchains.md) +- [21 —— 目标三元组](21-the-target-triple.md) +- [22 —— 目标侧](22-target-side.md) +- [23 —— 项目环境](23-the-project-environment.md) +- [24 —— 基于 openkal 的交叉构建](24-openkal-cross.md) + +### 3x —— 扩展 mcpp 与它的生态 + +- [30 —— 构建程序:`build.mcpp`](30-build-mcpp.md) —— 工程需要一步 mcpp 没有规则的工作 +- [31 —— 编写规则包](31-authoring-a-rule-package.md) —— 把那一步打包给别的工程用 +- [32 —— 编写一个载荷](32-authoring-a-payload.md) —— 由 mcpp 安装的工具或预编译库 +- [33 —— 编写运行时适配包](33-authoring-an-adapter.md) —— 够到宿主提供的库 +- [34 —— 编写板级支持包](34-authoring-a-bsp.md) —— 一块板子,以及抵达它的方式 + +### 4x —— 设备与加速器 + +- [40 —— 裸机与 freestanding 目标](40-baremetal.md) +- [41 —— 抵达一台设备](41-devices.md) +- [42 —— 异构硬件构建](42-heterogeneous-builds.md) + +### 5x —— 面向程序的契约 + +- [50 —— 机器可读输出](50-machine-output.md) +- [51 —— 受支持的版本与兼容性](51-supported-versions.md) + +### 9x —— mcpp 自身 + +- [90 —— 从源码构建与参与贡献](90-build-from-source.md) +- [91 —— 工具链机制内幕](91-toolchain-internals.md) +- [92 —— 发布 mcpp](92-release.md) + +## 反查 + +上面的章节表是**阅读顺序**。这里是另一种索引:从读者眼前的一个记号,查到拥有它的 +那一章。 + +**manifest 的表与键** + +| | 章节 | | 章节 | +|---|---|---|---| +| `[package]`、`[targets.]`、`[build]`、`[lib]` | [04](04-mcpp-toml.md) | `[profile.]`、`[resources]`、`[runtime]` | [04](04-mcpp-toml.md) | +| `[dependencies]`、`[dev-dependencies]`、`[build-dependencies]` | [05](05-dependencies.md) | `scan_overrides`、`module_extensions` | [04](04-mcpp-toml.md) | +| `[features]`、`[feature-deps.]`、`provides` / `requires` | [06](06-features-and-capabilities.md) | `[workspace]` | [07](07-workspace.md) | +| `[toolchain]`、`cxx_runtime` | [20](20-toolchains.md) | `[target.]`、`cfg(…)` | [22](22-target-side.md) | +| `[xlings]`、`[xlings.workspace]`、`[feature-xlings.]` | [23](23-the-project-environment.md) | `[pack]` | [10](10-pack-and-release.md) | +| `[build] accel`、`[package] accelerators`、`device_extensions` | [42](42-heterogeneous-builds.md) | `[hooks]` | [09](09-commands-by-scenario.md) | +| `[package] platforms`、`[build] cache` | [04](04-mcpp-toml.md) | `[targets.]`、`[profile.]` | [04](04-mcpp-toml.md) | +| `runner`、`[target..runners]` | [41](41-devices.md) | `rule_module` | [31](31-authoring-a-rule-package.md) | + +**命令** + +| | 章节 | | 章节 | +|---|---|---|---| +| `build`、`run` | [01](01-getting-started.md) | `test` | [08](08-testing.md) | +| `new`、`new --template` | [01](01-getting-started.md) | `add`、`update`、`why` | [05](05-dependencies.md) | +| `pack` | [10](10-pack-and-release.md) | `publish`、`emit xpkg`、`xpkg parse` | [11](11-publishing-a-library.md) | +| `toolchain` | [20](20-toolchains.md) | `clean`、`cache`、`index`、`self …` | [09](09-commands-by-scenario.md) | + +**概念** + +| | 章节 | | 章节 | +|---|---|---|---| +| mcpp 是什么,以及那条保证 | [00](00-what-mcpp-is.md) | `import std`、模块接口、BMI | [20](20-toolchains.md)、[30](30-build-mcpp.md) | +| `mcpp::action`、构建程序 | [30](30-build-mcpp.md) | 规则包、`MCPP_EXPORT_C` | [31](31-authoring-a-rule-package.md) | +| 目标三元组、支持矩阵 | [21](21-the-target-triple.md) | runner、具名 runner | [41](41-devices.md) | +| 岛、接缝、`accel` | [42](42-heterogeneous-builds.md) | 描述符、索引 | [11](11-publishing-a-library.md) | +| ABI tag、预建产物 | [12](12-binary-distribution.md) | 退出码、JSON 输出 | [50](50-machine-output.md) | +| `xim:` 载荷、`[xlings.workspace]` | [32](32-authoring-a-payload.md)、[23](23-the-project-environment.md) | `compat:` 适配包、`runtime.library_dirs` | [33](33-authoring-an-adapter.md) | ## 规范文档 @@ -32,4 +149,6 @@ - [specs/](../specs/README.md) —— 全部规范的索引 - [SPEC-001 —— 包身份、`[dependencies]` 选择器与匹配](../specs/package-identity.md) + - [SPEC-002 —— 目标侧模型:保留命名空间、五层、三条规则](../specs/target-side.md) + - [SPEC-003 —— 退出码契约](../specs/exit-codes.md) - [SPEC-004 —— `mcpp.toml` 的平面划分、条件化形状、解析轴与命名规约](../specs/manifest-semantics.md) diff --git a/examples/06-openkal-cross/README.md b/examples/06-openkal-cross/README.md index fbf1616f..dc433e0f 100644 --- a/examples/06-openkal-cross/README.md +++ b/examples/06-openkal-cross/README.md @@ -150,10 +150,10 @@ There is no platform interface beneath it to depend on; what such a program builds on is `mcpplibs/openarch`, the architecture-mechanism layer. Both routes are described in -[docs/15 — Cross-Compilation Over openkal](../../docs/15-openkal-cross.md). +[docs/15 — Cross-Compilation Over openkal](../../docs/24-openkal-cross.md). ## Reference -[docs/15 — Cross-Compilation Over openkal](../../docs/15-openkal-cross.md) for -the model, and [docs/14 — The Target Side](../../docs/14-target-side.md) for the +[docs/15 — Cross-Compilation Over openkal](../../docs/24-openkal-cross.md) for +the model, and [docs/14 — The Target Side](../../docs/22-target-side.md) for the five layers and the rules that govern them. diff --git a/examples/09-heterogeneous/README.md b/examples/09-heterogeneous/README.md index 88b9609f..93eda6d1 100644 --- a/examples/09-heterogeneous/README.md +++ b/examples/09-heterogeneous/README.md @@ -8,6 +8,7 @@ belong to the structure and which to a vendor. | directory | model | device compiler | what it adds to the structure | |---|---|---|---| +| [`boundary/`](boundary/) | none — a C island | none | the boundary alone: a generated module the consumer imports, with no seam and no header anywhere in the project | | [`cuda/`](cuda/) | CUDA | the project's own clang (`-x cuda`), or nvcc | the driver relation stated as a fact and a floor, and two pairings nvcc cannot have | | [`vulkan/`](vulkan/) | Vulkan compute | glslang or shaderc, to SPIR-V | a device output that is a header rather than an object, and one artifact that runs on three devices | | [`sycl/`](sycl/) | SYCL | the dpcpp payload's clang | a second compiler with its own standard library, and a chained action for the device link | @@ -15,14 +16,16 @@ belong to the structure and which to a vendor. | [`cann/`](cann/) | Ascend C | the toolkit's BiSheng (`-x asc`) | a device object for hardware nobody in this repository has, and a host half that declines cleanly | | [`multi-backend/`](multi-backend/) | CUDA **and** Vulkan | both of the above | the other shape: backends that are additive rather than a seam, chosen at run time | -Start with `cuda/`. The four beside it assume it, and `multi-backend/` assumes -two of them. +Start with `boundary/`, which needs no device and isolates the interface +between an island and the C++ side. Then `cuda/`, which adds the device compiler +and a seam over that boundary. The four beside `cuda/` assume it, and +`multi-backend/` assumes two of them. -The first five are one shape — a **seam**: exactly one implementation exists in -the artifact and the choice was made at build time. `multi-backend/` is the -other — several implementations in one artifact, chosen when the program runs. -A program can take either; a library that is compiled once and consumed by -people whose machines differ can only take the second. +`boundary/` and the five model directories are one shape — a **seam**: exactly +one implementation exists in the artifact and the choice was made at build time. +`multi-backend/` is the other — several implementations in one artifact, chosen +when the program runs. A program can take either; a library that is compiled +once and consumed by people whose machines differ can only take the second. ## The structure @@ -34,14 +37,16 @@ is which compiler consumes the file, not which dialect the file is written in. **A seam.** The island's interface is `extern "C"` and free of standard-library types, because the two sides do not share a C++ ABI. A module — `app.saxpy` in -all four — turns that C interface back into a C++ one. The seam is the single +each of the five model directories — turns that C interface back into a C++ one. +`boundary/` is the one that stops before this step, so the two can be read side +by side. The seam is the single place where the implementation underneath becomes a different model or a CPU loop, and the single place a `cfg(accelerator = ...)` section has to apply. Without it, every importer would be backend-specific. The seam carries one more entry point, `saxpy_device_name()`, for a reason -worth stating: all four islands and all four CPU fallbacks produce the same -four numbers. Without a name in the output, a run that silently fell back to +worth stating: every island and every CPU fallback here produces the same four +numbers. Without a name in the output, a run that silently fell back to the CPU is indistinguishable from a run on a device -- in a set of examples whose whole subject is which device ran the computation. Each backend fills it in with the device it used, the CPU file fills it in with `cpu`, and both do so @@ -71,6 +76,16 @@ anything is compiled. The two variants land in different artifact directories because the device axis is part of the build's identity, so alternating between them does not rebuild from scratch. +**A boundary, generated or written.** The `extern "C"` declarations under the +seam are each entry point's signature stated a second time, at the one place a +disagreement is invisible: C language linkage does not mangle, and an island and +its host fallback are never in one link. `mcpp.tools.island` reads the marked +declarations out of both implementations and writes that header and a module +over it, so the signatures exist once. `boundary/`, `cuda/` and `sycl/` take +that route; `hip/`, `vulkan/` and `cann/` keep the header written by hand, so +the two can be read side by side. [`boundary/`](boundary/) states what each +rung costs. + **A rule package, which brings its own environment.** Every vendor spelling — `--cuda-gpu-arch`, `-gencode`, `--target-env`, `-fsycl-targets`, `-fsycl-link`, `--cce-aicore-arch` — lives in `mcpp:plugins`, a package the project depends on @@ -84,18 +99,18 @@ selects it and the accelerator it serves, so a project writes one edge and no ```toml [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-cuda", "tools-island"], host-module = true } ``` `multi-backend/` is the one example here that also pins a version, and it does so to demonstrate the override: the rule owns "which package, and no older than what", the project owns "and exactly this one". One version is installed either -way — see *One package, one version* in `docs/05-mcpp-toml.md`. +way — see *One package, one version* in `docs/04-mcpp-toml.md`. ## The layers underneath -A device build reaches hardware through four layers, and each of the four -examples uses all of them. Confusing two of them is the most common way a +A device build reaches hardware through four layers, and each of the model +directories uses all of them. Confusing two of them is the most common way a working build stops working on another machine. | layer | owns | example | @@ -137,6 +152,6 @@ The examples show the shape mcpp implements: device code in separate translation units, compiled by a separate compiler, linked into an ordinary program. Models that compile a whole target with an offloading compiler — OpenMP `target`, C++ parallel algorithms with a device backend — have no -separable island and are not reached this way. `docs/20-heterogeneous-builds.md` +separable island and are not reached this way. `docs/42-heterogeneous-builds.md` states the distinction and why it is a property of the model rather than a gap in the tool. diff --git a/examples/09-heterogeneous/boundary/README.md b/examples/09-heterogeneous/boundary/README.md new file mode 100644 index 00000000..d8079da6 --- /dev/null +++ b/examples/09-heterogeneous/boundary/README.md @@ -0,0 +1,84 @@ +# The boundary, on its own — the ladder's lowest rung + +The consumer imports a module that this build generated, and the project +contains no seam, no header, and no hand-written `.cppm`. + +``` +cd examples/09-heterogeneous/boundary +mcpp run # 12 24 36 48 +``` + +Read this before [`../cuda`](../cuda/). It isolates the boundary between an +island and the C++ side; `cuda/` then adds the device compiler and the seam over +it, and the difference between the two is what each rung buys. + +## The whole project + +``` +mcpp.toml one dependency edge +build.mcpp scan the island, emit the boundary +src/kernels/saxpy.c the island +src/main.cpp import boundary.kernels; +``` + +`mcpp.tools.island` reads the entry points marked `MCPP_EXPORT_C` and writes two +files into the build directory: the `extern "C"` header the island reads, and a +module over it whose whole content is a re-export. + +```cpp +// generated +module; +#include "boundary.kernels.h" +export module boundary.kernels; + +export using ::saxpy_device; +export using ::saxpy_device_name; +``` + +Re-exporting names rather than restating signatures is what lets the generator +work without a C parser: it needs only the identifier before the `(`. It is also +why no second copy of a signature exists — at a C-linkage boundary that is the +copy that can disagree without anything noticing. + +## The island is C here, and that is the only simplification + +An ordinary `.c` file, so this example runs on any machine with no device +toolkit. What makes it an island is the property the boundary exists for: it is +compiled separately, it cannot import a module, and its interface is +`extern "C"`. Substituting a `.cu` and a device rule is what `../cuda` shows, +and nothing about the boundary changes when that happens. + +## Four rungs, and what each one costs + +| rung | written by hand | the consumer writes | the consumer gets | +|---|---|---|---| +| **L0 — this example** | nothing but the marked entry points | `import boundary.kernels` | the island's own interface: pointers and a count | +| L1 — `../cuda`, `../sycl` | a seam module over the generated one | `import app.saxpy` | the interface the project designed | +| L2 | a seam, and the entry list passed to `emit` directly | `import app.saxpy` | the same, for entry points a scan cannot see | +| L3 — `../hip`, `../vulkan`, `../cann` | the header and the module | `import app.saxpy` | the same, with the signature written twice | + +**What L0 does not have.** The interface is C-shaped: `saxpy_device(2.0f, x, y, +out, 4)` rather than a span. And there is no place for a +`cfg(accelerator = ...)` section to apply, because a seam is the single point at +which one implementation is exchanged for another. A project with one island and +one backend can stop here; a project that will swap backends needs L1. + +**What the generator will not do.** Which functions, which types, and what +happens on failure are design decisions, so the C++ interface stays +hand-written. The generator produces what is mechanical around the entry +points — the include guard, the `extern "C"` block, the `__cplusplus` dance and +the module wrapper. + +## Criteria + +| criterion | measured | +|---|---| +| the consumer imports the generated module and names no header | `grep -rn '#include' src/` is empty | +| the project has no hand-written `.cppm` | the file list above | +| the result is the family's | `12 24 36 48` | + +The first two are why this example exists. `mcpp:plugins` records that a +consumer importing the generated module links against an implementation +compiled by a different driver, measured with GCC 16.1; until this example +nothing in either repository ran that arrangement — every fixture and every +example kept a seam on top. diff --git a/examples/09-heterogeneous/boundary/build.mcpp b/examples/09-heterogeneous/boundary/build.mcpp new file mode 100644 index 00000000..bddbf356 --- /dev/null +++ b/examples/09-heterogeneous/boundary/build.mcpp @@ -0,0 +1,41 @@ +import std; +import mcpp; +import mcpp.tools.island; + +// The lowest rung: the generated boundary module IS the interface, and this +// project contains no seam and no header. +// +// Both halves are handed to `scan` unconditionally. They are the only place the +// entry points' signatures are written, and the generator is the only point at +// which both texts exist at once -- C language linkage does not mangle, so two +// halves that declared one name differently would link cleanly and read the +// arguments by whichever signature each was compiled with. +int main() { + const std::string root = mcpp::manifest_dir(); + const std::vector halves{ + root + "/src/kernels/saxpy.c", + }; + for (auto const& f : halves) mcpp::rerun_if_changed(f.c_str()); + + mcpp::tools::island::options opt; + opt.module_name = "boundary.kernels"; + opt.out_dir = std::string(mcpp::out_dir()) + "/island"; + opt.produced_by = "the boundary example"; + + const auto entries = mcpp::tools::island::scan(halves, opt); + if (!entries) return 1; + const auto out = mcpp::tools::island::emit(*entries, opt); + if (!out) return 1; + + // The island reads the generated header through a forced-include flag, so + // no source in this project names a generated file. + // + // `cflag` and not `cxxflag`: the island is C, and forcing a header into + // every C++ translation unit would put declarations ahead of the + // `export module` line of a module interface, which is ill-formed. + for (auto const& f : mcpp::tools::island::force_include_flags( + out->header_file, mcpp::compiler())) + mcpp::cflag(f.c_str()); + mcpp::generated(out->interface_file.c_str()); + return 0; +} diff --git a/examples/09-heterogeneous/boundary/mcpp.toml b/examples/09-heterogeneous/boundary/mcpp.toml new file mode 100644 index 00000000..5decbd75 --- /dev/null +++ b/examples/09-heterogeneous/boundary/mcpp.toml @@ -0,0 +1,14 @@ +[package] +name = "boundary" +version = "0.1.0" + +[language] +standard = "c++23" + +# `tools-island` is not a device rule: it claims no extension and names no rule +# module, so the edge states `host-module = true` itself. +[build-dependencies.mcpp] +plugins = { version = "0.4.0", features = ["tools-island"], host-module = true } + +[build] +sources = ["src/*.cpp", "src/kernels/*.c"] diff --git a/examples/09-heterogeneous/boundary/src/kernels/saxpy.c b/examples/09-heterogeneous/boundary/src/kernels/saxpy.c new file mode 100644 index 00000000..491ab608 --- /dev/null +++ b/examples/09-heterogeneous/boundary/src/kernels/saxpy.c @@ -0,0 +1,17 @@ +/* The island. An ordinary C file here, so this example runs on any machine; + substituting a `.cu` and a device rule is what `../cuda` shows. What makes + this an island is the property the boundary exists for: it is compiled + separately, it cannot import a module, and its interface is `extern "C"`. */ + +static const char* g_ran_on = ""; + +MCPP_EXPORT_C +int saxpy_device(float a, const float* x, const float* y, + float* out, unsigned n) { + for (unsigned i = 0; i < n; ++i) out[i] = a * x[i] + y[i]; + g_ran_on = "cpu (this example's island is an ordinary C file)"; + return 0; +} + +MCPP_EXPORT_C +const char* saxpy_device_name(void) { return g_ran_on; } diff --git a/examples/09-heterogeneous/boundary/src/main.cpp b/examples/09-heterogeneous/boundary/src/main.cpp new file mode 100644 index 00000000..dd4e03fc --- /dev/null +++ b/examples/09-heterogeneous/boundary/src/main.cpp @@ -0,0 +1,24 @@ +import std; + +// THE CONSUMER IMPORTS THE GENERATED MODULE. There is no seam in this project +// and no header in its source tree: `boundary.kernels` is written into the +// build directory by `mcpp.tools.island` during this build. +// +// What arrives is the island's own interface -- pointers and a count, an +// `extern "C"` signature. `../cuda` adds a hand-written seam over exactly this +// module and turns it into spans; the README states what each rung costs. +import boundary.kernels; + +int main() { + const float x[]{ 1, 2, 3, 4 }; + const float y[]{ 10, 20, 30, 40 }; + float out[4]{}; + + if (saxpy_device(2.0f, x, y, out, 4) != 0) { + std::println(std::cerr, "saxpy_device failed"); + return 1; + } + for (int i = 0; i < 4; ++i) std::print("{}{}", out[i], i == 3 ? "\n" : " "); + std::println("ran on: {}", saxpy_device_name()); + return 0; +} diff --git a/examples/09-heterogeneous/cann/app/mcpp.toml b/examples/09-heterogeneous/cann/app/mcpp.toml index 37375fcc..27625bc0 100644 --- a/examples/09-heterogeneous/cann/app/mcpp.toml +++ b/examples/09-heterogeneous/cann/app/mcpp.toml @@ -14,7 +14,7 @@ import_std = true # imports it as `mcpp.rules.ascendc`. `[build-dependencies]`, because a rule # package's library must never reach the target while its rule is wanted. [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-ascendc"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-ascendc"], host-module = true } # NO [xlings.workspace]. `mcpp.rules.ascendc` declares `xim:cann-toolkit` # itself, gated on this accelerator and on the feature that selects the rule -- diff --git a/examples/09-heterogeneous/cuda/README.md b/examples/09-heterogeneous/cuda/README.md index b8af616f..68e5b0cc 100644 --- a/examples/09-heterogeneous/cuda/README.md +++ b/examples/09-heterogeneous/cuda/README.md @@ -91,7 +91,7 @@ The rule names it. This project writes one edge and no payload list at all: ```toml [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-cuda", "tools-island"], host-module = true } ``` `mcpp.rules.cuda` declares nvcc, cudart, cuRAND's headers, CCCL and the driver diff --git a/examples/09-heterogeneous/cuda/app/build.mcpp b/examples/09-heterogeneous/cuda/app/build.mcpp index 1ff04deb..8ce24a18 100644 --- a/examples/09-heterogeneous/cuda/app/build.mcpp +++ b/examples/09-heterogeneous/cuda/app/build.mcpp @@ -16,9 +16,10 @@ import mcpp.rules.cuda; // arguments by whichever signature it was compiled with. `scan` is the only // point at which both texts exist at once, and it refuses there. // -// The island reads the generated header through the compiler's forced-include -// flag, so no source in this project names a generated file and the project has -// no header in its source tree at all. +// The ISLAND reads the generated header through the compiler's forced-include +// flag, so it names no generated file. The host half does include it by name -- +// `src/cpu/saxpy.cpp` says so at its first line and explains why the two sides +// differ. What this project has none of is a header in its SOURCE TREE. // WHERE THE FLAGS GO, AND WHY IT IS NOT `mcpp::cflag`. // // `force_include_flags` makes a compiler read the generated boundary header diff --git a/examples/09-heterogeneous/cuda/app/mcpp.toml b/examples/09-heterogeneous/cuda/app/mcpp.toml index 16279404..c9da8914 100644 --- a/examples/09-heterogeneous/cuda/app/mcpp.toml +++ b/examples/09-heterogeneous/cuda/app/mcpp.toml @@ -23,7 +23,7 @@ default = "llvm@22.1.8" # `extern "C"` boundary the island's compiler reads and the module the seam # imports, so neither signature is stated twice. [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda", "tools-island"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-cuda", "tools-island"], host-module = true } # The driver's userspace library, reached through an index package that owns # the one hop mcpp needs: a directory on the artifact's runtime search path. @@ -38,7 +38,7 @@ cuda-driver = "2026.09.05" # `cfg(accelerator = ...)` and the feature that selects it, so this project # names the rule and nothing else. To pin a different version, write the same # entry here and it wins -- see `One package, one version` in -# docs/05-mcpp-toml.md. Needs mcpp 2026.9.7.1 and mcpp:plugins 0.3.0. +# docs/04-mcpp-toml.md. Needs mcpp 2026.9.7.1 and mcpp:plugins 0.3.0. # # What the rule declares for this lane: nvcc, cudart, cuRAND's headers, CCCL and # the driver sentinel. The 12.9 line is an EXACT default rather than a floor, diff --git a/examples/09-heterogeneous/hip/app/mcpp.toml b/examples/09-heterogeneous/hip/app/mcpp.toml index e982626e..7cba9239 100644 --- a/examples/09-heterogeneous/hip/app/mcpp.toml +++ b/examples/09-heterogeneous/hip/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-hip"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-hip"], host-module = true } # The driver's userspace library. HIP reaches the device through the CUDA # runtime here, so this is the same one hop the CUDA consumer needs: mcpp's diff --git a/examples/09-heterogeneous/multi-backend/README.md b/examples/09-heterogeneous/multi-backend/README.md index 34ea8bc1..bd61e58d 100644 --- a/examples/09-heterogeneous/multi-backend/README.md +++ b/examples/09-heterogeneous/multi-backend/README.md @@ -93,7 +93,7 @@ saying it after the vocabulary grows. ```toml [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-cuda", "rules-spirv"], host-module = true } ``` Two rules, in one build program, which is what an additive-backend package diff --git a/examples/09-heterogeneous/multi-backend/mcpp.toml b/examples/09-heterogeneous/multi-backend/mcpp.toml index 401da1c3..42cd5ae1 100644 --- a/examples/09-heterogeneous/multi-backend/mcpp.toml +++ b/examples/09-heterogeneous/multi-backend/mcpp.toml @@ -40,7 +40,7 @@ default = "llvm@22.1.8" # The rules are declared unconditionally because `build.mcpp` imports them # unconditionally -- each returns immediately when its own backend is absent. [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-cuda", "rules-spirv"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-cuda", "rules-spirv"], host-module = true } # ── the payloads, gated on the device they are for ────────────────────────── # diff --git a/examples/09-heterogeneous/sycl/app/mcpp.toml b/examples/09-heterogeneous/sycl/app/mcpp.toml index b876124c..a997f2ff 100644 --- a/examples/09-heterogeneous/sycl/app/mcpp.toml +++ b/examples/09-heterogeneous/sycl/app/mcpp.toml @@ -20,7 +20,7 @@ import_std = true default = "llvm@22.1.8" [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-sycl", "tools-island"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-sycl", "tools-island"], host-module = true } # The SYCL runtime, on the artifact's runtime search path. mcpp's private # loader does not consult /usr/lib, so `libsycl.so.9` -- which the rule diff --git a/examples/09-heterogeneous/vulkan/app/mcpp.toml b/examples/09-heterogeneous/vulkan/app/mcpp.toml index 820df29b..6fdd6a11 100644 --- a/examples/09-heterogeneous/vulkan/app/mcpp.toml +++ b/examples/09-heterogeneous/vulkan/app/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # The rule that compiles the shaders lives in the official plugin collection, # selected by its feature; `build.mcpp` imports it as `mcpp.rules.spirv`. [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-spirv"], host-module = true } # The Khronos loader, built by the index rather than taken from the host, and # the adapter that makes the host's own ICDs reachable from a binary running diff --git a/examples/10-graphics/README.md b/examples/10-graphics/README.md index f3536f67..ed9500e5 100644 --- a/examples/10-graphics/README.md +++ b/examples/10-graphics/README.md @@ -53,7 +53,7 @@ tells the two apart, which is why it is printed. ```toml [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-spirv"], host-module = true } [build] accel = "vulkan1.2" diff --git a/examples/10-graphics/offscreen/mcpp.toml b/examples/10-graphics/offscreen/mcpp.toml index e8849d5b..f3ce5431 100644 --- a/examples/10-graphics/offscreen/mcpp.toml +++ b/examples/10-graphics/offscreen/mcpp.toml @@ -13,7 +13,7 @@ import_std = true # One edge. `mcpp.rules.spirv` declares the shader compiler it drives, so this # project names no payload for it. [build-dependencies.mcpp] -plugins = { version = "0.3.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.4.0", features = ["rules-spirv"], host-module = true } # The Khronos loader. Unconditional, and the reason is a rule of the engine # rather than a preference: A DEPENDENCY CANNOT BE CONDITIONED ON A LAYER. diff --git a/examples/11-features/README.md b/examples/11-features/README.md new file mode 100644 index 00000000..da8323dd --- /dev/null +++ b/examples/11-features/README.md @@ -0,0 +1,77 @@ +# 11 — Features: what a package declares, and what a consumer pays for + +Every heterogeneous example in this repository *consumes* a feature +(`features = ["rules-cuda"]` on a dependency edge). This one **declares** them. + +``` +cd examples/11-features/greeter +mcpp run # HELLO, WORLD! metrics: off +mcpp run --features metrics # HELLO, WORLD! metrics: on +mcpp test # the dev-dependency reaches the test +``` + +## Three shapes of feature, in one manifest + +```toml +[features] +default = ["shout"] + +shout = {} # a compile macro and nothing else + +[features.metrics] # ...and a source +sources = ["src/metrics.cpp"] + +[feature-deps.metrics] # ...and a dependency +counters = { path = "../counters" } +``` + +| shape | what it adds | reached in the source as | +|---|---|---| +| `shout = {}` | `MCPP_FEATURE_SHOUT` | `#ifdef MCPP_FEATURE_SHOUT` | +| `[features.metrics] sources` | one file in the source set | the file is compiled or it is not | +| `[feature-deps.metrics]` | a package in the graph | `import counters;` inside that file | + +Two more keys are here because a library has them and no other example does: + +```toml +[dev-dependencies] # reaches tests, never the artifact +counters = { path = "../counters" } + +[profile.release] +opt_level = 3 +``` + +## The criterion + +Not "the default build still works" — that passes when the optional dependency +is resolved and simply unused. The criterion is that **the default build's +resolution does not name the optional package**: + +``` +mcpp build counters appears 0 times in the build output +mcpp build --features metrics counters appears 2 times +``` + +Measured on 2026.9.8.1. That is the property `[feature-deps]` exists for: a +consumer that does not ask for the backend does not download it, does not +compile it, and does not link it. + +## A build program sees the resolved feature set + +```cpp +if (mcpp::has_feature("metrics")) + mcpp::warning("the metrics feature is active; `counters` is in the graph"); +``` + +A rule or a generator acts on the feature set without the project stating the +decision a second time. This is the mechanism the accelerator lanes use: a rule +package declares its toolkit under the feature that selects it, so a build that +does not name the feature installs nothing. + +## Current limitations + +**A default feature is turned off in the manifest, not on the command line.** +There is no `--no-default-features`. `mcpp run --features metrics` yields +`default ∪ {metrics}`; to build without `shout`, `[features] default` is +edited. With `default = []` this example prints `Hello, world.` instead of +`HELLO, WORLD!`. diff --git a/examples/11-features/counters/mcpp.toml b/examples/11-features/counters/mcpp.toml new file mode 100644 index 00000000..bc282786 --- /dev/null +++ b/examples/11-features/counters/mcpp.toml @@ -0,0 +1,6 @@ +[package] +name = "counters" +version = "0.1.0" + +[language] +standard = "c++23" diff --git a/examples/11-features/counters/src/counters.cppm b/examples/11-features/counters/src/counters.cppm new file mode 100644 index 00000000..fd85133b --- /dev/null +++ b/examples/11-features/counters/src/counters.cppm @@ -0,0 +1,10 @@ +// The optional backend. A consumer that does not name the `metrics` feature +// never resolves this package, which is the criterion the README states. +export module counters; +import std; + +export namespace counters { +inline int calls = 0; +inline void record() { ++calls; } +inline std::string report() { return std::format("{} call(s)", calls); } +} diff --git a/examples/11-features/greeter/build.mcpp b/examples/11-features/greeter/build.mcpp new file mode 100644 index 00000000..77c7b1bd --- /dev/null +++ b/examples/11-features/greeter/build.mcpp @@ -0,0 +1,11 @@ +import std; +import mcpp; + +// A build program sees the feature set that was resolved, so a rule or a +// generator can act on it without the project repeating the decision in a +// second place. +int main() { + if (mcpp::has_feature("metrics")) + mcpp::warning("the metrics feature is active; `counters` is in the graph"); + return 0; +} diff --git a/examples/11-features/greeter/mcpp.toml b/examples/11-features/greeter/mcpp.toml new file mode 100644 index 00000000..feae21de --- /dev/null +++ b/examples/11-features/greeter/mcpp.toml @@ -0,0 +1,38 @@ +[package] +name = "greeter" +version = "0.1.0" + +[language] +standard = "c++23" + +[targets.greet] +kind = "bin" +main = "src/main.cpp" + +# FEATURES ARE ADDITIVE AND DECLARED HERE. `default` is what a consumer that +# says nothing gets; every other feature is opted into by name. +[features] +default = ["shout"] + +# A feature that only turns a compile macro on. Every active feature defines +# `MCPP_FEATURE_`, so a source can branch without the manifest naming a +# file. +shout = {} + +# A feature that ADDS A SOURCE. The file is compiled only when the feature is +# active, so a consumer that does not ask for it does not pay for it. +[features.metrics] +sources = ["src/metrics.cpp"] + +# ...AND PULLS A DEPENDENCY. `[feature-deps.]` is what makes an optional +# backend optional: a build that does not name `metrics` resolves a graph in +# which `counters` does not appear at all. +[feature-deps.metrics] +counters = { path = "../counters" } + +# Used by `mcpp test` and by nothing the artifact contains. +[dev-dependencies] +counters = { path = "../counters" } + +[profile.release] +opt_level = 3 diff --git a/examples/11-features/greeter/src/greeter.cppm b/examples/11-features/greeter/src/greeter.cppm new file mode 100644 index 00000000..18a72a6d --- /dev/null +++ b/examples/11-features/greeter/src/greeter.cppm @@ -0,0 +1,19 @@ +export module greeter; +import std; + +export namespace greeter { + +// `MCPP_FEATURE_SHOUT` is defined because `shout` is in `[features] default`. +// A consumer that builds with `--no-default-features` gets the other branch, +// and neither branch is named in the manifest. +inline std::string greet(std::string_view who) { +#ifdef MCPP_FEATURE_SHOUT + std::string s = std::format("HELLO, {}", who); + for (auto& c : s) c = static_cast(std::toupper(static_cast(c))); + return s + "!"; +#else + return std::format("Hello, {}.", who); +#endif +} + +} // namespace greeter diff --git a/examples/11-features/greeter/src/main.cpp b/examples/11-features/greeter/src/main.cpp new file mode 100644 index 00000000..758e64f9 --- /dev/null +++ b/examples/11-features/greeter/src/main.cpp @@ -0,0 +1,17 @@ +import std; +import greeter; + +#ifdef MCPP_FEATURE_METRICS +extern "C" void greeter_record_call(); +#endif + +int main() { + std::println("{}", greeter::greet("world")); +#ifdef MCPP_FEATURE_METRICS + greeter_record_call(); + std::println("metrics: on"); +#else + std::println("metrics: off"); +#endif + return 0; +} diff --git a/examples/11-features/greeter/src/metrics.cpp b/examples/11-features/greeter/src/metrics.cpp new file mode 100644 index 00000000..510139bf --- /dev/null +++ b/examples/11-features/greeter/src/metrics.cpp @@ -0,0 +1,6 @@ +// Compiled only when the `metrics` feature is active: `[features.metrics]` +// names this file. Under any other feature set it is not in the source set, +// so `counters` is not linked and not resolved. +import counters; + +extern "C" void greeter_record_call() { counters::record(); } diff --git a/examples/11-features/greeter/tests/test_greet.cpp b/examples/11-features/greeter/tests/test_greet.cpp new file mode 100644 index 00000000..9b1d864f --- /dev/null +++ b/examples/11-features/greeter/tests/test_greet.cpp @@ -0,0 +1,15 @@ +// `mcpp test` compiles every `tests/**/*.cpp` as its own program. This one uses +// `counters`, which reaches it through `[dev-dependencies]` -- a dependency the +// artifact never links. +import std; +import greeter; +import counters; + +int main() { + const auto s = greeter::greet("x"); + if (s.empty()) { std::println(std::cerr, "greet returned nothing"); return 1; } + counters::record(); + if (counters::calls != 1) { std::println(std::cerr, "counters did not record"); return 1; } + std::println("ok: {} / {}", s, counters::report()); + return 0; +} diff --git a/examples/12-a-new-device-language/README.md b/examples/12-a-new-device-language/README.md new file mode 100644 index 00000000..bcaba486 --- /dev/null +++ b/examples/12-a-new-device-language/README.md @@ -0,0 +1,247 @@ +# 12 — A device language the engine has never heard of + +A rule package teaches mcpp to compile `.toy`, a small language with its own +compiler, and a project uses it. No mcpp release is involved: `.toy` is not in +the engine's built-in device-extension table and will never be. + +``` +cd examples/12-a-new-device-language/app +mcpp run +``` + +``` + Rules example.rules.toy (example:rules-toy) + Building host tool toyc:toyc from toyc v0.1.0 (once per package version × host toolchain) + Compiling toyapp v0.1.0 (.) + Finished dev [unoptimized + debuginfo] in 0.65s + +gcd(1071, 462) = 21 +scale(21, 2) = 42 +answer() = 42 +``` + +## The three packages + +| directory | what it is | who builds it | +|---|---|---| +| `toyc/` | the compiler for `.toy`: a lexer, a recursive-descent parser, semantic checks and a C++ emitter | mcpp, **for the build machine**, as a host tool | +| `rules-toy/` | the rule: it declares the extension, the module a consumer imports, and one action per `.toy` | the consumer's build program | +| `app/` | the project | mcpp, for the target | + +## The language + +`.toy` has integers, `let`, assignment, `if` / `else`, `while`, calls between +kernels, and the arithmetic and comparison operators. Every kernel takes and +returns an integer. + +``` +program := { kernel } +kernel := 'kernel' ident '(' [ ident { ',' ident } ] ')' block +block := '{' { stmt } '}' +stmt := 'let' ident '=' expr ';' + | ident '=' expr ';' + | 'return' expr ';' + | 'if' '(' expr ')' block [ 'else' block ] + | 'while' '(' expr ')' block +expr := cmp +cmp := sum { ('<' | '>' | '<=' | '>=' | '==' | '!=') sum } +sum := term { ('+' | '-') term } +term := unary { ('*' | '/' | '%') unary } +unary := [ '-' ] primary +primary := number | ident | ident '(' [ expr { ',' expr } ] ')' | '(' expr ')' +``` + +`src/kernels/answer.toy` is Euclid's algorithm and a caller: + +``` +kernel gcd(a, b) { + while (b != 0) { + let t = b; + b = a % b; + a = t; + } + return a; +} + +kernel answer() { + return scale(gcd(1071, 462), 2); +} +``` + +`toyc` emits one `extern "C"` function per kernel, and forward-declares them +all first so kernels may call each other in any order: + +```cpp +extern "C" int toy_gcd(int v_a, int v_b) { + while ((v_b != 0)) { + int v_t = v_b; + v_b = (v_a % v_b); + v_a = v_t; + } + return v_a; +} +``` + +Three things in that output are decisions rather than accidents. **`extern "C"`**, +because the two sides are produced by different compilers and share no C++ ABI +— the same reason a device island's boundary is. **`v_` on every local**, +because a kernel that names a variable `class` must not become a C++ file that +fails to compile for a reason the toy source cannot express. **Parentheses +around every binary expression**, because the AST already holds the grouping +and the emitter does not reproduce C++'s precedence table. + +The compiler rejects what it cannot compile, and says where. Each of these was +produced by running `toyc` on a file with that one defect: + +```console +$ toyc bad.toy -o bad.cpp +bad.toy:1:1: error: kernel `scale` can reach its end without a `return` +bad2.toy:2:12: error: `b` is not a kernel in this file +bad3.toy:2:12: error: `x` is not declared +bad4.toy:3:5: error: expected `;`, found `return` +``` + +The first is the one worth the code it takes. A kernel returns an integer on +every path, and a block satisfies that if it ends in a `return` or in an +`if`/`else` whose branches both do — `while` never counts, because the language +cannot state that a loop runs at all. Emitting `return 0;` at the end instead +would have compiled everything and given a wrong answer for the kernel whose +author forgot a branch. + +## The two manifest keys + +`rules-toy/mcpp.toml` declares them on the feature that selects the rule: + +```toml +[features.rules-toy] +sources = ["src/rules-toy.cppm"] +rule_module = "example.rules.toy" +device_extensions = [".toy"] +``` + +| key | effect | +|---|---| +| `device_extensions` | a consumer that activates the feature gets `.toy` classified as a **device source**: never scanned for imports, never producing a BMI, and refused if no action claims it | +| `rule_module` | the module the consumer's build program imports. It implies `host-module = true`, so the consumer writes the feature and nothing else | + +Device extensions are not in the default source glob. A `.toy` is compiled +because the manifest names it: + +```toml +[dependencies] +rules-toy = { path = "../rules-toy", features = ["rules-toy"] } + +[build] +sources = ["src/*.cpp", "src/kernels/*.toy"] +``` + +## The compiler is a package, built for the build machine + +The rule contains no compiler. `toyc` is an ordinary mcpp package with a +`kind = "bin"` target, and one line brings it into the graph: + +```toml +[feature-deps.rules-toy] +toyc = { path = "../toyc", tools = ["toyc"], reexport = true } +``` + +| part | what it does | +|---|---| +| `tools = ["toyc"]` | mcpp builds that target **for the build machine**, even when the project around it is cross-compiling | +| `reexport = true` | the tool reaches whoever activated the feature. Without it the tool stays with this package, which is the supply-chain default: an arbitrary transitive dependency must not put entries in a build program's tool namespace | +| on `[feature-deps]` | a project that depends on this package **without** activating the rule builds no compiler | + +The rule reads the path back with `mcpp::dep_bin("toyc", "toyc")`, under the +name of the manifest entry that **declared** the tool rather than the +consumer's spelling of anything. + +That gating was measured. With `features = ["rules-toy"]` removed from the +consumer, the build stops before any tool is built: + +``` +error: build.mcpp imports 'example.rules.toy', and no dependency provides it as a host module. + declared without `host-module = true`: rules-toy (in [dependencies]) +``` + +and the tool store holds no `toyc` entry afterwards. + +## What this example is the first to demonstrate + +Every other rule in this repository is built into `mcpp:plugins` and compiles an +extension the engine already knows. This one adds a language from outside — the +property the accelerator design is built on — and it is the tree's only example +of a **dependency that produces a host tool**, which is how a rule package ships +a real compiler rather than a script. + +## Criteria + +| criterion | measured | +|---|---| +| a `.toy` is compiled and its output joins the link, on an engine that does not know the extension | `answer() = 42` | +| the language is executed rather than pattern-matched | `gcd(1071, 462) = 21`, computed by the emitted loop | +| editing the `.toy` reaches the artifact | `scale(…, 2)` → `scale(…, 3)`: `42` → `63` | +| the compiler is built for the build machine, on demand, and only when the rule is active | the `Building host tool` line above; no store entry without the feature | + +## The boundary this example measured: a host tool is cached by version + +Four changes were made one at a time, each from the same starting state: + +| what changed | the artifact | how it was changed | +|---|---|---| +| the `.toy` source | follows: `42` → `63` | `scale(…, 2)` → `scale(…, 3)` | +| the compiler's **bytes**, at the path the action names | follows: `42` → `168` | overwriting the binary in the tool store | +| the compiler's **sources**, its version unchanged | does not follow: the previous answer stands | editing the emitter | +| the compiler's **version** | follows: `42` → `168`, and the tool is rebuilt | `0.1.0` → `0.1.1` | + +Rows two and three are the whole finding, and they separate two things that are +easy to merge. **The action's input tracking works**: `rules-toy` declares the +compiler beside the source, and changing that file's bytes re-runs the edge. +**What does not happen is the rebuild that would change those bytes.** The tool +store's key is the tool package's identity, version, host triple, compiler +identity, profile, features and the versions of its transitive dependencies — +it holds no source content. For a package that arrives from an index the key is +exact, because a published version is immutable; for a `path` dependency being +edited it is not. + +`mcpp run` prints `Finished dev in 0.00s` in row three, and that line is mcpp's +own summary rather than evidence: row two prints it too, and the artifact +changed. + +**Row four does not test row two, which is why the difference is worth stating.** +The tool's path is on the action's command line, so a new version re-runs the +edge whether or not the compiler is also a declared input. Removing +`a.input(compiler)` from `rules-toy` and bumping the version left the artifact +following anyway. The isolating change is different bytes at the *same* path — +row two — and it takes both of its directions: with the input removed, the +artifact followed the overwrite and then stopped following the restore. CI runs +that pair. + +Two ways out: bump the tool package's version, or empty the build cache with +`mcpp cache clean` — the tool store lives inside it, at +`/tool//@/`. + +**One more trap sits behind them.** Going back from `0.1.1` to `0.1.0`, whose +clean tool was still in the store, left the artifact at `168`. The build program +did not re-run, so the plan still named the `0.1.1` binary, which had not +changed. `rm -rf target` cleared it. Iterating on a compiler means the version +goes forward only. + +## Three things the first version got wrong + +Recorded because each is a mistake a rule author will make once. + +**An action's command does not run from the package root.** +`mcpp::device_sources()` answers with package-root-relative paths, and the +command runs from the build directory. The relative path reached `toyc` +unchanged and the read failed there. The rule joins `mcpp::manifest_dir()` to +each path. + +**The compiler was a shell script, and its exit status lied.** The first `toyc` +summed with `sed … | grep … | paste -sd+ - | bc`. A pipeline's status is its +last command's: when an earlier stage produced nothing, `bc` still exited 0, so +`set -e` never fired and the script wrote a program that compiled, linked, ran +and printed `0`. Replacing the script with a compiled program removed the +class, not just the instance. + +**The strings must outlive the action.** `a.id = ("toy:" + stem).c_str()` hands +`submit()` a pointer into a temporary that is already gone. diff --git a/examples/12-a-new-device-language/app/build.mcpp b/examples/12-a-new-device-language/app/build.mcpp new file mode 100644 index 00000000..c5ecc72b --- /dev/null +++ b/examples/12-a-new-device-language/app/build.mcpp @@ -0,0 +1,11 @@ +import std; +import mcpp; +import example.rules.toy; + +int main() { + // The rule reads the device set from the graph, so this program names no + // file. What it must track is the SET of paths: a `.toy` appearing or + // disappearing changes what the graph should be. + mcpp::rerun_if_changed_glob("src/kernels/**/*.toy"); + return example::rules::toy::compile() ? 0 : 1; +} diff --git a/examples/12-a-new-device-language/app/mcpp.toml b/examples/12-a-new-device-language/app/mcpp.toml new file mode 100644 index 00000000..009366e6 --- /dev/null +++ b/examples/12-a-new-device-language/app/mcpp.toml @@ -0,0 +1,18 @@ +[package] +name = "toyapp" +version = "0.1.0" + +[language] +standard = "c++23" + +# ONE EDGE. `rule_module` on the feature implies `host-module = true`, and the +# feature's own `[feature-deps]` brings the compiler, so the consumer names the +# feature and nothing else. +[dependencies] +rules-toy = { path = "../rules-toy", features = ["rules-toy"] } + +# `.toy` IS NOT IN THE DEFAULT SOURCE GLOB. Device sources are opted into by +# naming them -- a package that vendors one it builds elsewhere must not start +# compiling it on an mcpp upgrade. +[build] +sources = ["src/*.cpp", "src/kernels/*.toy"] diff --git a/examples/12-a-new-device-language/app/src/kernels/answer.toy b/examples/12-a-new-device-language/app/src/kernels/answer.toy new file mode 100644 index 00000000..8e5cefd6 --- /dev/null +++ b/examples/12-a-new-device-language/app/src/kernels/answer.toy @@ -0,0 +1,27 @@ +# answer.toy -- a kernel source in a language the engine has never heard of. +# +# `.toy` has integers, `let`, assignment, `if` / `else`, `while`, calls +# between kernels, and the arithmetic and comparison operators. Every kernel +# takes and returns an integer, and the file's kernels are what the C++ side +# can call. + +kernel gcd(a, b) { + while (b != 0) { + let t = b; + b = a % b; + a = t; + } + return a; +} + +kernel scale(v, k) { + if (k < 0) { + return -v * -k; + } else { + return v * k; + } +} + +kernel answer() { + return scale(gcd(1071, 462), 2); +} diff --git a/examples/12-a-new-device-language/app/src/main.cpp b/examples/12-a-new-device-language/app/src/main.cpp new file mode 100644 index 00000000..628d7494 --- /dev/null +++ b/examples/12-a-new-device-language/app/src/main.cpp @@ -0,0 +1,16 @@ +import std; + +// Declared by hand here, and defined by C++ that `toyc` wrote from +// `src/kernels/answer.toy`. The declarations are `extern "C"` for the same +// reason a device island's boundary is: the two sides are produced by +// different compilers and do not share a C++ ABI. +extern "C" int toy_gcd(int a, int b); +extern "C" int toy_scale(int v, int k); +extern "C" int toy_answer(); + +int main() { + std::println("gcd(1071, 462) = {}", toy_gcd(1071, 462)); + std::println("scale(21, 2) = {}", toy_scale(21, 2)); + std::println("answer() = {}", toy_answer()); + return toy_answer() == 42 ? 0 : 1; +} diff --git a/examples/12-a-new-device-language/rules-toy/mcpp.toml b/examples/12-a-new-device-language/rules-toy/mcpp.toml new file mode 100644 index 00000000..d2a116bf --- /dev/null +++ b/examples/12-a-new-device-language/rules-toy/mcpp.toml @@ -0,0 +1,40 @@ +[package] +name = "rules-toy" +namespace = "example" +version = "0.1.0" +description = "A rule package that teaches mcpp a device language the engine has never heard of" +license = "Apache-2.0" + +# THE TWO KEYS THAT MAKE A NEW DEVICE LANGUAGE COST NO ENGINE RELEASE. +# +# `device_extensions` says which sources this rule compiles. A consumer that +# activates the feature gets `.toy` classified as a DEVICE source: never +# scanned for imports, never producing a BMI, and refused if no action claims +# it. +# +# `rule_module` says which module the consumer's build program imports to reach +# the rule. It also implies `host-module = true`, so a consumer writes the +# feature and nothing else. +# +# `.toy` is not in the engine's built-in table and never will be. The engine +# knows what a device source IS; it does not know that `.toy` is anything. +[features] +default = [] + +[features.rules-toy] +sources = ["src/rules-toy.cppm"] +rule_module = "example.rules.toy" +device_extensions = [".toy"] + +# THE COMPILER IS A PACKAGE, AND IT IS BUILT FOR THE BUILD MACHINE. +# +# `tools = ["toyc"]` asks the graph for that target as a host executable even +# when the project around it is cross-compiling; `reexport = true` hands it to +# whoever activated this feature. Without `reexport` the tool stays here, which +# is the supply-chain default: an arbitrary transitive dependency must not be +# able to put entries in a build program's tool namespace. +# +# The request hangs on the FEATURE, so a project that depends on this package +# without activating the rule builds no compiler. +[feature-deps.rules-toy] +toyc = { path = "../toyc", tools = ["toyc"], reexport = true } diff --git a/examples/12-a-new-device-language/rules-toy/src/rules-toy.cppm b/examples/12-a-new-device-language/rules-toy/src/rules-toy.cppm new file mode 100644 index 00000000..11fc85f6 --- /dev/null +++ b/examples/12-a-new-device-language/rules-toy/src/rules-toy.cppm @@ -0,0 +1,93 @@ +// The rule for `.toy`. +// +// It contains no compiler. `toyc` is an ordinary mcpp package next door, and +// this module's whole job is to turn "the consumer listed some `.toy` files" +// into edges in the build graph -- ordered, fingerprinted and incremental like +// every other edge. +export module example.rules.toy; + +import std; +import mcpp; + +export namespace example::rules::toy { + +struct options { + std::string out_dir = std::string(mcpp::out_dir()); + + // The compiler, as the consumer's build program sees it. `dep_bin` answers + // under the name in the manifest that DECLARED the tool -- this package's + // `[feature-deps]` entry -- and the answer travels to the consumer because + // that entry says `reexport = true`. + std::string compiler = std::string(mcpp::dep_bin("toyc", "toyc")); +}; + +// Compile every `.toy` the consumer listed. +inline bool compile(options opt = {}) { + const std::string root = mcpp::manifest_dir(); + if (root.empty()) { + std::println(std::cerr, + "example.rules.toy: no mcpp build context -- this runs from build.mcpp"); + return false; + } + + if (opt.compiler.empty()) { + std::println(std::cerr, + "example.rules.toy: the `toyc` compiler was not built. Activate this " + "package's `rules-toy` feature, which is what asks the graph for it: " + "rules-toy = {{ path = \"...\", features = [\"rules-toy\"] }}"); + return false; + } + + // `mcpp::device_sources()` is one string, one path per line, and it is the + // package's WHOLE device set rather than this rule's share of it. A rule + // selects by the extension it claims and leaves the rest: a project with a + // second backend puts that backend's sources in the same list, and handing + // them to `toyc` would fail on a file `toyc` never claimed. + std::vector mine; + std::string_view all(mcpp::device_sources()); + for (std::size_t i = 0; i <= all.size();) { + const auto sep = all.find('\n', i); + const auto one = all.substr(i, sep == std::string_view::npos ? all.size() - i : sep - i); + i = sep == std::string_view::npos ? all.size() + 1 : sep + 1; + if (one.ends_with(".toy")) mine.emplace_back(one); + } + + bool any = false; + for (auto const& src : mine) { + const std::string stem = std::filesystem::path(src).stem().string(); + const std::string gen = opt.out_dir + "/toy_" + stem + ".cpp"; + + // The strings outlive the action. `a.id = ("toy:" + stem).c_str()` + // would hand it a pointer into a temporary that is gone by `submit`. + const std::string id = "toy:" + stem; + const std::string desc = "toyc " + src; + // ABSOLUTE. `mcpp::device_sources()` answers package-root-relative, and + // an action's command does not run from the package root -- it runs + // from the build directory. Measured: the relative path reached `toyc` + // unchanged and the read failed there. + const std::string abs = root + "/" + src; + + mcpp::action a; + a.id = id.c_str(); + // `source`: what this produces is C++ that mcpp then compiles. A rule + // whose compiler emitted an object directly would use `object`. + a.role = "source"; + a.description = desc.c_str(); + a.arg(opt.compiler.c_str()).arg(abs.c_str()).arg("-o").arg(gen.c_str()); + a.input(abs.c_str()); + // THE COMPILER IS AN INPUT TOO. Without this line, a change to `toyc` + // leaves every edge clean and the artifact keeps the bytes the previous + // compiler produced -- a green build over a stale result, which is the + // failure that is hardest to notice. + a.input(opt.compiler.c_str()); + a.output(gen.c_str()); + a.submit(); + any = true; + } + + if (!any) + std::println(std::cerr, "example.rules.toy: no `.toy` source in this package"); + return any; +} + +} // namespace example::rules::toy diff --git a/examples/12-a-new-device-language/toyc/mcpp.toml b/examples/12-a-new-device-language/toyc/mcpp.toml new file mode 100644 index 00000000..b854767e --- /dev/null +++ b/examples/12-a-new-device-language/toyc/mcpp.toml @@ -0,0 +1,19 @@ +[package] +name = "toyc" +namespace = "example" +version = "0.1.0" +description = "The compiler for the toy device language" +license = "Apache-2.0" + +[language] +standard = "c++23" + +# The compiler is an ordinary mcpp package. A rule package depends on it with +# `tools = ["toyc"]`, and mcpp builds this target FOR THE BUILD MACHINE even +# when the project around it is cross-compiling. +[build] +sources = ["src/lexer.cppm", "src/compile.cppm"] + +[targets.toyc] +kind = "bin" +main = "src/main.cpp" diff --git a/examples/12-a-new-device-language/toyc/src/compile.cppm b/examples/12-a-new-device-language/toyc/src/compile.cppm new file mode 100644 index 00000000..8b4cf04f --- /dev/null +++ b/examples/12-a-new-device-language/toyc/src/compile.cppm @@ -0,0 +1,487 @@ +// The parser, the semantic checks and the C++ emitter for `.toy`. +// +// One module rather than three, and the AST is not exported: the emitter is +// the AST's only consumer, so exporting it would widen the interface without +// adding a reader. +// +// The grammar, which is the whole language: +// +// program := { kernel } +// kernel := 'kernel' ident '(' [ ident { ',' ident } ] ')' block +// block := '{' { stmt } '}' +// stmt := 'let' ident '=' expr ';' +// | ident '=' expr ';' +// | 'return' expr ';' +// | 'if' '(' expr ')' block [ 'else' block ] +// | 'while' '(' expr ')' block +// expr := cmp +// cmp := sum { ('<' | '>' | '<=' | '>=' | '==' | '!=') sum } +// sum := term { ('+' | '-') term } +// term := unary { ('*' | '/' | '%') unary } +// unary := [ '-' ] primary +// primary := number | ident | ident '(' [ expr { ',' expr } ] ')' | '(' expr ')' +export module example.toyc.compile; + +import std; +import example.toyc.lexer; + +namespace toyc { + +// ── the AST ────────────────────────────────────────────────────────────── +struct expr; +using expr_ptr = std::unique_ptr; + +struct e_number { long long value; }; +struct e_name { std::string id; int line, col; }; +struct e_unary { std::string op; expr_ptr operand; }; +struct e_binary { std::string op; expr_ptr lhs, rhs; }; +struct e_call { std::string callee; std::vector args; int line, col; }; + +struct expr { std::variant node; }; + +struct stmt; +using stmt_ptr = std::unique_ptr; +struct block { std::vector body; }; + +struct s_let { std::string name; expr_ptr init; int line, col; }; +struct s_assign { std::string name; expr_ptr value; int line, col; }; +struct s_return { expr_ptr value; }; +struct s_if { expr_ptr cond; block then_branch; std::optional else_branch; }; +struct s_while { expr_ptr cond; block body; }; + +struct stmt { std::variant node; }; + +struct kernel { + std::string name; + std::vector params; + block body; + int line, col; +}; + +// ── the parser ─────────────────────────────────────────────────────────── +class parser { +public: + explicit parser(std::vector toks) : toks_(std::move(toks)) {} + + std::expected, diag> parse_program() { + std::vector out; + while (peek().kind != tok::end) { + auto k = parse_kernel(); + if (!k) return std::unexpected(k.error()); + out.push_back(std::move(*k)); + } + if (out.empty()) return std::unexpected(err("a source file declares at least one kernel")); + return out; + } + +private: + std::vector toks_; + std::size_t i_ = 0; + + const token& peek(std::size_t ahead = 0) const { + return toks_[std::min(i_ + ahead, toks_.size() - 1)]; + } + const token& take() { return toks_[i_ < toks_.size() - 1 ? i_++ : i_]; } + bool accept(tok k) { if (peek().kind == k) { ++i_; return true; } return false; } + + diag err(std::string m) const { return diag{std::move(m), peek().line, peek().col}; } + std::unexpected expected(tok k) const { + return std::unexpected(err(std::format("expected {}, found {}", + spelling(k), spelling(peek().kind)))); + } + + std::expected parse_kernel() { + const token head = peek(); + if (!accept(tok::kw_kernel)) return expected(tok::kw_kernel); + if (peek().kind != tok::ident) return expected(tok::ident); + kernel k{take().text, {}, {}, head.line, head.col}; + if (!accept(tok::lparen)) return expected(tok::lparen); + if (!accept(tok::rparen)) { + for (;;) { + if (peek().kind != tok::ident) return expected(tok::ident); + k.params.push_back(take().text); + if (accept(tok::comma)) continue; + if (accept(tok::rparen)) break; + return expected(tok::rparen); + } + } + auto b = parse_block(); + if (!b) return std::unexpected(b.error()); + k.body = std::move(*b); + return k; + } + + std::expected parse_block() { + if (!accept(tok::lbrace)) return expected(tok::lbrace); + block b; + while (!accept(tok::rbrace)) { + if (peek().kind == tok::end) return expected(tok::rbrace); + auto s = parse_stmt(); + if (!s) return std::unexpected(s.error()); + b.body.push_back(std::move(*s)); + } + return b; + } + + std::expected parse_stmt() { + const token head = peek(); + + if (accept(tok::kw_let)) { + if (peek().kind != tok::ident) return expected(tok::ident); + std::string name = take().text; + if (!accept(tok::assign)) return expected(tok::assign); + auto e = parse_expr(); + if (!e) return std::unexpected(e.error()); + if (!accept(tok::semi)) return expected(tok::semi); + return std::make_unique(stmt{s_let{std::move(name), std::move(*e), + head.line, head.col}}); + } + if (accept(tok::kw_return)) { + auto e = parse_expr(); + if (!e) return std::unexpected(e.error()); + if (!accept(tok::semi)) return expected(tok::semi); + return std::make_unique(stmt{s_return{std::move(*e)}}); + } + if (accept(tok::kw_if)) { + if (!accept(tok::lparen)) return expected(tok::lparen); + auto c = parse_expr(); + if (!c) return std::unexpected(c.error()); + if (!accept(tok::rparen)) return expected(tok::rparen); + auto t = parse_block(); + if (!t) return std::unexpected(t.error()); + std::optional e; + if (accept(tok::kw_else)) { + auto eb = parse_block(); + if (!eb) return std::unexpected(eb.error()); + e = std::move(*eb); + } + return std::make_unique(stmt{s_if{std::move(*c), std::move(*t), std::move(e)}}); + } + if (accept(tok::kw_while)) { + if (!accept(tok::lparen)) return expected(tok::lparen); + auto c = parse_expr(); + if (!c) return std::unexpected(c.error()); + if (!accept(tok::rparen)) return expected(tok::rparen); + auto b = parse_block(); + if (!b) return std::unexpected(b.error()); + return std::make_unique(stmt{s_while{std::move(*c), std::move(*b)}}); + } + if (peek().kind == tok::ident && peek(1).kind == tok::assign) { + std::string name = take().text; + take(); // `=` + auto e = parse_expr(); + if (!e) return std::unexpected(e.error()); + if (!accept(tok::semi)) return expected(tok::semi); + return std::make_unique(stmt{s_assign{std::move(name), std::move(*e), + head.line, head.col}}); + } + return std::unexpected(err(std::format( + "expected a statement, found {}", spelling(peek().kind)))); + } + + // Precedence is expressed by the call chain: cmp calls sum calls term. + std::expected parse_expr() { return parse_cmp(); } + + std::expected parse_cmp() { + auto lhs = parse_sum(); + if (!lhs) return lhs; + for (;;) { + std::string op; + switch (peek().kind) { + case tok::lt: op = "<"; break; + case tok::gt: op = ">"; break; + case tok::le: op = "<="; break; + case tok::ge: op = ">="; break; + case tok::eq: op = "=="; break; + case tok::ne: op = "!="; break; + default: return lhs; + } + take(); + auto rhs = parse_sum(); + if (!rhs) return rhs; + lhs = std::make_unique(expr{e_binary{op, std::move(*lhs), std::move(*rhs)}}); + } + } + + std::expected parse_sum() { + auto lhs = parse_term(); + if (!lhs) return lhs; + for (;;) { + std::string op; + if (peek().kind == tok::plus) op = "+"; + else if (peek().kind == tok::minus) op = "-"; + else return lhs; + take(); + auto rhs = parse_term(); + if (!rhs) return rhs; + lhs = std::make_unique(expr{e_binary{op, std::move(*lhs), std::move(*rhs)}}); + } + } + + std::expected parse_term() { + auto lhs = parse_unary(); + if (!lhs) return lhs; + for (;;) { + std::string op; + if (peek().kind == tok::star) op = "*"; + else if (peek().kind == tok::slash) op = "/"; + else if (peek().kind == tok::percent) op = "%"; + else return lhs; + take(); + auto rhs = parse_unary(); + if (!rhs) return rhs; + lhs = std::make_unique(expr{e_binary{op, std::move(*lhs), std::move(*rhs)}}); + } + } + + std::expected parse_unary() { + if (accept(tok::minus)) { + auto e = parse_unary(); + if (!e) return e; + return std::make_unique(expr{e_unary{"-", std::move(*e)}}); + } + return parse_primary(); + } + + std::expected parse_primary() { + const token head = peek(); + if (head.kind == tok::number) { + take(); + return std::make_unique(expr{e_number{head.value}}); + } + if (head.kind == tok::ident) { + take(); + if (!accept(tok::lparen)) + return std::make_unique(expr{e_name{head.text, head.line, head.col}}); + e_call c{head.text, {}, head.line, head.col}; + if (!accept(tok::rparen)) { + for (;;) { + auto a = parse_expr(); + if (!a) return a; + c.args.push_back(std::move(*a)); + if (accept(tok::comma)) continue; + if (accept(tok::rparen)) break; + return expected(tok::rparen); + } + } + return std::make_unique(expr{std::move(c)}); + } + if (accept(tok::lparen)) { + auto e = parse_expr(); + if (!e) return e; + if (!accept(tok::rparen)) return expected(tok::rparen); + return e; + } + return std::unexpected(err(std::format( + "expected an expression, found {}", spelling(head.kind)))); + } +}; + +// ── the emitter, with the semantic checks it needs to emit at all ──────── +class emitter { +public: + explicit emitter(const std::vector& ks) { + for (const auto& k : ks) arity_[k.name] = k.params.size(); + } + + std::expected emit(const std::vector& ks, + std::string_view source_name) { + std::string out = std::format( + "// Generated by toyc from {}. Do not edit.\n" + "//\n" + "// Every kernel is `extern \"C\"`: the C++ side and this file are\n" + "// produced by different compilers and share no C++ ABI.\n\n", + source_name); + + for (const auto& k : ks) out += declaration(k) + ";\n"; + out += "\n"; + + for (const auto& k : ks) { + scopes_.clear(); + scopes_.emplace_back(); + for (const auto& p : k.params) scopes_.back().insert(p); + + if (!always_returns(k.body)) + return std::unexpected(diag{ + std::format("kernel `{}` can reach its end without a `return`", k.name), + k.line, k.col}); + + out += declaration(k) + " {\n"; + auto body = emit_block(k.body, 1); + if (!body) return std::unexpected(body.error()); + out += *body; + out += "}\n\n"; + } + return out; + } + +private: + std::map arity_; + std::vector> scopes_; + + // `v_` on every local, `toy_` on every kernel. The target language has + // keywords of its own, and a kernel that names a variable `class` must not + // become a C++ file that does not compile for a reason the toy source + // cannot express. + static std::string local(std::string_view n) { return std::format("v_{}", n); } + static std::string global(std::string_view n) { return std::format("toy_{}", n); } + + static std::string declaration(const kernel& k) { + std::string params; + for (std::size_t i = 0; i < k.params.size(); ++i) + params += std::format("{}int {}", i ? ", " : "", local(k.params[i])); + return std::format("extern \"C\" int {}({})", global(k.name), + params.empty() ? "void" : params); + } + + // Every kernel returns an integer, so every path out of one is a `return`. + // A block satisfies that if its last statement is a `return`, or an + // `if`/`else` whose branches both do -- `while` never counts, because the + // language cannot say that a loop runs at all. + // + // Emitting `return 0;` at the end instead would have compiled everything + // and given a wrong answer for the kernel whose author forgot a branch. + static bool always_returns(const block& b) { + if (b.body.empty()) return false; + const stmt& last = *b.body.back(); + if (std::holds_alternative(last.node)) return true; + if (const auto* i = std::get_if(&last.node)) + return i->else_branch && always_returns(i->then_branch) + && always_returns(*i->else_branch); + return false; + } + + bool visible(const std::string& n) const { + return std::ranges::any_of(scopes_, [&](const auto& s) { return s.contains(n); }); + } + + std::expected emit_block(const block& b, int depth) { + scopes_.emplace_back(); + std::string out; + for (const auto& s : b.body) { + auto one = emit_stmt(*s, depth); + if (!one) return one; + out += *one; + } + scopes_.pop_back(); + return out; + } + + std::expected emit_stmt(const stmt& s, int depth) { + const std::string pad(static_cast(depth) * 4, ' '); + return std::visit([&](const auto& n) -> std::expected { + using T = std::decay_t; + if constexpr (std::is_same_v) { + if (scopes_.back().contains(n.name)) + return std::unexpected(diag{ + std::format("`{}` is already declared in this block", n.name), + n.line, n.col}); + auto e = emit_expr(*n.init); + if (!e) return std::unexpected(e.error()); + scopes_.back().insert(n.name); + return std::format("{}int {} = {};\n", pad, local(n.name), *e); + } else if constexpr (std::is_same_v) { + if (!visible(n.name)) + return std::unexpected(diag{ + std::format("`{}` is assigned before it is declared", n.name), + n.line, n.col}); + auto e = emit_expr(*n.value); + if (!e) return std::unexpected(e.error()); + return std::format("{}{} = {};\n", pad, local(n.name), *e); + } else if constexpr (std::is_same_v) { + auto e = emit_expr(*n.value); + if (!e) return std::unexpected(e.error()); + return std::format("{}return {};\n", pad, *e); + } else if constexpr (std::is_same_v) { + auto c = emit_expr(*n.cond); + if (!c) return std::unexpected(c.error()); + auto t = emit_block(n.then_branch, depth + 1); + if (!t) return t; + std::string out = std::format("{}if ({}) {{\n{}{}}}", pad, *c, *t, pad); + if (n.else_branch) { + auto e = emit_block(*n.else_branch, depth + 1); + if (!e) return e; + out += std::format(" else {{\n{}{}}}", *e, pad); + } + return out + "\n"; + } else { + auto c = emit_expr(*n.cond); + if (!c) return std::unexpected(c.error()); + auto b = emit_block(n.body, depth + 1); + if (!b) return b; + return std::format("{}while ({}) {{\n{}{}}}\n", pad, *c, *b, pad); + } + }, s.node); + } + + std::expected emit_expr(const expr& e) { + return std::visit([&](const auto& n) -> std::expected { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::format("{}", n.value); + } else if constexpr (std::is_same_v) { + if (!visible(n.id)) + return std::unexpected(diag{ + std::format("`{}` is not declared", n.id), n.line, n.col}); + return local(n.id); + } else if constexpr (std::is_same_v) { + auto o = emit_expr(*n.operand); + if (!o) return o; + return std::format("(-{})", *o); + } else if constexpr (std::is_same_v) { + auto l = emit_expr(*n.lhs); + if (!l) return l; + auto r = emit_expr(*n.rhs); + if (!r) return r; + // Parenthesised because the emitter does not reproduce C++'s + // precedence table: the AST already holds the grouping, and + // parentheses are how it survives into the target language. + return std::format("({} {} {})", *l, n.op, *r); + } else { + const auto it = arity_.find(n.callee); + if (it == arity_.end()) + return std::unexpected(diag{ + std::format("`{}` is not a kernel in this file", n.callee), + n.line, n.col}); + if (it->second != n.args.size()) + return std::unexpected(diag{ + std::format("kernel `{}` takes {} argument(s), {} given", + n.callee, it->second, n.args.size()), + n.line, n.col}); + std::string args; + for (std::size_t i = 0; i < n.args.size(); ++i) { + auto a = emit_expr(*n.args[i]); + if (!a) return a; + args += std::format("{}{}", i ? ", " : "", *a); + } + return std::format("{}({})", global(n.callee), args); + } + }, e.node); + } +}; + +} // namespace toyc + +export namespace toyc { + +// The whole compiler as one function: text in, C++ out, or one diagnostic +// with a position. +std::expected compile(std::string_view source, + std::string_view source_name) { + auto tokens = scan(source); + if (!tokens) return std::unexpected(tokens.error()); + parser p(std::move(*tokens)); + auto kernels = p.parse_program(); + if (!kernels) return std::unexpected(kernels.error()); + + std::set seen; + for (const auto& k : *kernels) + if (!seen.insert(k.name).second) + return std::unexpected(diag{std::format("kernel `{}` is declared twice", k.name), + k.line, k.col}); + + emitter e(*kernels); + return e.emit(*kernels, source_name); +} + +} // namespace toyc diff --git a/examples/12-a-new-device-language/toyc/src/lexer.cppm b/examples/12-a-new-device-language/toyc/src/lexer.cppm new file mode 100644 index 00000000..25fb5ddc --- /dev/null +++ b/examples/12-a-new-device-language/toyc/src/lexer.cppm @@ -0,0 +1,148 @@ +// The lexer for `.toy`. +// +// The interface exports one function and two aggregates. The parser's own +// types -- the variant-shaped AST -- stay inside `example.toyc.compile`, +// because a module interface that exports nested standard containers is the +// shape that has truncated BMIs on this repository's own toolchain before. +export module example.toyc.lexer; + +import std; + +export namespace toyc { + +enum class tok { + end, number, ident, + kw_kernel, kw_let, kw_return, kw_if, kw_else, kw_while, + lparen, rparen, lbrace, rbrace, comma, semi, + assign, plus, minus, star, slash, percent, + lt, gt, le, ge, eq, ne, +}; + +struct token { + tok kind = tok::end; + std::string text; // for ident, and for diagnostics + long long value = 0; // for number + int line = 1; + int col = 1; +}; + +// A diagnostic carries a position because a compiler that cannot say WHERE is +// a compiler its users debug by bisecting the input. +struct diag { + std::string message; + int line = 0; + int col = 0; +}; + +std::string spelling(tok k) { + switch (k) { + case tok::end: return "end of file"; + case tok::number: return "a number"; + case tok::ident: return "an identifier"; + case tok::kw_kernel: return "`kernel`"; + case tok::kw_let: return "`let`"; + case tok::kw_return: return "`return`"; + case tok::kw_if: return "`if`"; + case tok::kw_else: return "`else`"; + case tok::kw_while: return "`while`"; + case tok::lparen: return "`(`"; + case tok::rparen: return "`)`"; + case tok::lbrace: return "`{`"; + case tok::rbrace: return "`}`"; + case tok::comma: return "`,`"; + case tok::semi: return "`;`"; + case tok::assign: return "`=`"; + default: return "an operator"; + } +} + +std::expected, diag> scan(std::string_view src) { + std::vector out; + int line = 1, col = 1; + std::size_t i = 0; + + auto advance = [&](std::size_t n) { + for (std::size_t k = 0; k < n; ++k) { + if (src[i + k] == '\n') { ++line; col = 1; } else { ++col; } + } + i += n; + }; + auto push = [&](tok k, std::size_t n, std::string text = {}) { + out.push_back(token{k, std::move(text), 0, line, col}); + advance(n); + }; + + while (i < src.size()) { + const char c = src[i]; + + if (c == '#') { // a comment runs to the newline + std::size_t n = 0; + while (i + n < src.size() && src[i + n] != '\n') ++n; + advance(n); + continue; + } + if (std::isspace(static_cast(c))) { advance(1); continue; } + + if (std::isdigit(static_cast(c))) { + std::size_t n = 0; + long long v = 0; + while (i + n < src.size() && std::isdigit(static_cast(src[i + n]))) { + v = v * 10 + (src[i + n] - '0'); + ++n; + } + token t{tok::number, {}, v, line, col}; + out.push_back(t); + advance(n); + continue; + } + + if (std::isalpha(static_cast(c)) || c == '_') { + std::size_t n = 0; + while (i + n < src.size() && + (std::isalnum(static_cast(src[i + n])) || src[i + n] == '_')) ++n; + const std::string word(src.substr(i, n)); + const tok k = word == "kernel" ? tok::kw_kernel + : word == "let" ? tok::kw_let + : word == "return" ? tok::kw_return + : word == "if" ? tok::kw_if + : word == "else" ? tok::kw_else + : word == "while" ? tok::kw_while + : tok::ident; + push(k, n, word); + continue; + } + + // Two-character operators are tested first: `<` is a prefix of `<=`, + // and testing the one-character form first would lex `<=` as `<` `=` + // and report the error at the `=`, one column past the cause. + const std::string_view two = src.substr(i, 2); + if (two == "<=") { push(tok::le, 2); continue; } + if (two == ">=") { push(tok::ge, 2); continue; } + if (two == "==") { push(tok::eq, 2); continue; } + if (two == "!=") { push(tok::ne, 2); continue; } + + switch (c) { + case '(': push(tok::lparen, 1); continue; + case ')': push(tok::rparen, 1); continue; + case '{': push(tok::lbrace, 1); continue; + case '}': push(tok::rbrace, 1); continue; + case ',': push(tok::comma, 1); continue; + case ';': push(tok::semi, 1); continue; + case '=': push(tok::assign, 1); continue; + case '+': push(tok::plus, 1); continue; + case '-': push(tok::minus, 1); continue; + case '*': push(tok::star, 1); continue; + case '/': push(tok::slash, 1); continue; + case '%': push(tok::percent, 1); continue; + case '<': push(tok::lt, 1); continue; + case '>': push(tok::gt, 1); continue; + default: break; + } + return std::unexpected(diag{std::format("unexpected character `{}`", c), line, col}); + } + + out.push_back(token{tok::end, {}, 0, line, col}); + return out; +} + +} // namespace toyc diff --git a/examples/12-a-new-device-language/toyc/src/main.cpp b/examples/12-a-new-device-language/toyc/src/main.cpp new file mode 100644 index 00000000..386a07db --- /dev/null +++ b/examples/12-a-new-device-language/toyc/src/main.cpp @@ -0,0 +1,49 @@ +// toyc -- the driver. +// +// toyc -o +// +// Exit 0 and write the output, or exit 1 and print one diagnostic in the +// `file:line:col: error: message` form every editor already parses. +import std; +import example.toyc.compile; + +int main(int argc, char** argv) { + std::string in, out; + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a == "-o" && i + 1 < argc) out = argv[++i]; + else if (a.starts_with("-o")) out = a.substr(2); + else if (a.starts_with("-")) { std::println(std::cerr, "toyc: unknown option `{}`", a); return 2; } + else if (in.empty()) in = a; + else { std::println(std::cerr, "toyc: more than one input file"); return 2; } + } + if (in.empty() || out.empty()) { + std::println(std::cerr, "usage: toyc -o "); + return 2; + } + + std::ifstream file(in, std::ios::binary); + if (!file) { + std::println(std::cerr, "toyc: cannot read {}", in); + return 1; + } + const std::string source((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + const auto cpp = toyc::compile(source, std::filesystem::path(in).filename().string()); + if (!cpp) { + std::println(std::cerr, "{}:{}:{}: error: {}", + in, cpp.error().line, cpp.error().col, cpp.error().message); + return 1; + } + + // The output directory is created by whoever declared the action; a + // compiler that creates it silently hides a rule that got out_dir wrong. + std::ofstream sink(out, std::ios::binary); + if (!sink) { + std::println(std::cerr, "toyc: cannot write {}", out); + return 1; + } + sink << *cpp; + return sink ? 0 : 1; +} diff --git a/mcpp.toml b/mcpp.toml index 272c8f67..fa4f69c8 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -13,7 +13,7 @@ repo = "https://github.com/mcpp-community/mcpp" # binary would be -O0. A `--profile`/`--dev`/`--release` flag still overrides. default-profile = "release" # Split the module edge: importers start when the BMI is published rather than -# when the compiler exits. See docs/05-mcpp-toml.md. +# when the compiler exits. See docs/04-mcpp-toml.md. # # ON HERE, `auto` (= off) EVERYWHERE ELSE. The key stays opt-in until it has been # through CI on every platform, and nothing was exercising it, so it could never diff --git a/modules/buildmcpp/src/directives.cppm b/modules/buildmcpp/src/directives.cppm index 349c837e..d0557d39 100644 --- a/modules/buildmcpp/src/directives.cppm +++ b/modules/buildmcpp/src/directives.cppm @@ -33,7 +33,7 @@ // Scope a field means the next directive cannot be added without someone // answering that question. // -// See .agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md §4 (S5). +// See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §4 (S5). export module mcpp.build.directives; diff --git a/modules/buildmcpp/src/program_protocol.cppm b/modules/buildmcpp/src/program_protocol.cppm index fab31c4d..7301dcfa 100644 --- a/modules/buildmcpp/src/program_protocol.cppm +++ b/modules/buildmcpp/src/program_protocol.cppm @@ -23,7 +23,7 @@ // the manifest, the toolchain or the filesystem to be stated would not be a // protocol term. // -// See .agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md §4 and +// See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §4 and // .agents/docs/2026-08-11-source-kind-table-and-build-program-timeout.md §4. export module mcpp.build.program_protocol; diff --git a/modules/manifest/src/glob.cppm b/modules/manifest/src/glob.cppm index ca50bae0..3cea1cdb 100644 --- a/modules/manifest/src/glob.cppm +++ b/modules/manifest/src/glob.cppm @@ -86,7 +86,7 @@ std::optional try_narrow(const std::filesystem::path& p) { // The stored spelling is GENERIC (`/`), because that is what try_narrow // produces and there is no second narrowing here to disagree with it. On // Windows the reported path therefore reads `C:/pkg/test/www`, not -// `C:\pkg\test\www`; docs/05-mcpp-toml.md shows it that way too. +// `C:\pkg\test\www`; docs/04-mcpp-toml.md shows it that way too. void note_unnarrowable_path(const std::filesystem::path& p); // Take and clear this run's records. @@ -96,7 +96,7 @@ void note_unnarrowable_path(const std::filesystem::path& p); // CLI reports. Drained in exactly one place (`cli::run`'s scope guard), which // is what keeps "recorded but never shown" from becoming the next silent // failure. The rule is written up in .agents/skills/mcpp-contributing/SKILL.md -// ("路径窄化不变式") and the user-facing behaviour in docs/05-mcpp-toml.md. +// ("路径窄化不变式") and the user-facing behaviour in docs/04-mcpp-toml.md. std::vector take_unnarrowable_paths(); // Does `candidate` match `glob`, interpreted relative to `root`? diff --git a/modules/manifest/src/toml.cppm b/modules/manifest/src/toml.cppm index 43c08cd5..f8a66c31 100644 --- a/modules/manifest/src/toml.cppm +++ b/modules/manifest/src/toml.cppm @@ -2207,7 +2207,7 @@ std::expected parse_string(std::string_view content, // [hooks] — project build lifecycle commands (#496). Parsed HERE rather // than by the module that runs them, for the reason Appendix A of - // docs/05-mcpp-toml.md states: mcpp.toml has one grammar and one parser. + // docs/04-mcpp-toml.md states: mcpp.toml has one grammar and one parser. // A second reader of the same file would report ITS syntax errors in its // own vocabulary — a typo in [package] arriving as "invalid hook // configuration" — and would sit outside the warning/--strict policy every @@ -2376,7 +2376,7 @@ std::expected parse_string(std::string_view content, m.lib.path = *v; } - // [pack] — `mcpp pack` configuration. See docs/35-pack-design.md. + // [pack] — `mcpp pack` configuration. See docs/10-pack-and-release.md. if (auto v = doc->get_string("pack.default_mode")) { const auto& s = *v; if (s != "static" && s != "bundle-project" && s != "bundle-all") { diff --git a/modules/manifest/src/types.cppm b/modules/manifest/src/types.cppm index f4dd4498..fdf1efb0 100644 --- a/modules/manifest/src/types.cppm +++ b/modules/manifest/src/types.cppm @@ -154,7 +154,7 @@ struct Target { // Aliases at the top of this file keep `mcpp::manifest::DependencySpec` // resolvable for unchanged call sites. -// `[toolchain]` section per docs/21-toolchain-and-tools.md +// `[toolchain]` section per docs/20-toolchains.md // linux = "gcc@15.1.0" // macos = "llvm@20" // windows = "msvc@system" @@ -314,7 +314,7 @@ inline void append(BuildInputs& dst, const BuildInputs& src) { // A build-graph node declared by a build program (`mcpp:action=`). // // The architectural point (see -// .agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md §3.1): +// .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §3.1): // build.mcpp answers "what does this build look like" — CONFIGURATION — and is // a bad place to do WORK. Generating sources, linting, signing and packaging // are work: they want to be incremental, parallel and attributable, which a @@ -1149,7 +1149,7 @@ struct LibConfig { std::filesystem::path path; // explicit override; empty = use convention }; -// `[pack]` — `mcpp pack` configuration. See docs/35-pack-design.md. +// `[pack]` — `mcpp pack` configuration. See docs/10-pack-and-release.md. // // `default_mode` picks the bundling strategy when the user runs bare // `mcpp pack` (no `--mode` flag): @@ -1263,7 +1263,7 @@ struct WorkspaceConfig { // `[hooks]` — project build lifecycle commands (#496). // // The commands are host-shell strings written by the project author, run by -// `mcpp build` around the build it performs. See docs/05-mcpp-toml.md §2.16. +// `mcpp build` around the build it performs. See docs/04-mcpp-toml.md §2.16. // // ONLY THE ROOT PROJECT'S HOOKS ARE EVER RUN. Every manifest mcpp parses // carries this field, including a DEPENDENCY's — and `mcpp build` reaches the @@ -1577,7 +1577,7 @@ struct Manifest { // [target.] tables — empty if user didn't declare any. std::map targetOverrides; - // [pack] — `mcpp pack` config (see docs/35-pack-design.md). + // [pack] — `mcpp pack` config (see docs/10-pack-and-release.md). PackConfig packConfig; // [lib] — library root interface convention (M5.x+). diff --git a/modules/toolchain-model/src/fingerprint.cppm b/modules/toolchain-model/src/fingerprint.cppm index 61dbf437..2403f861 100644 --- a/modules/toolchain-model/src/fingerprint.cppm +++ b/modules/toolchain-model/src/fingerprint.cppm @@ -1,6 +1,6 @@ // mcpp.toolchain.fingerprint — 11-field fingerprint that gates BMI cache safety. // -// Per docs/08-toolchain-internals.md, the fingerprint MUST cover: +// Per docs/91-toolchain-internals.md, the fingerprint MUST cover: // 1. compiler id 2. compiler version // 3. compiler driver identity 4. target triple // 5. stdlib id+version 6. C++ standard diff --git a/modules/toolchain-model/src/linkmodel.cppm b/modules/toolchain-model/src/linkmodel.cppm index f625c9d1..44eec313 100644 --- a/modules/toolchain-model/src/linkmodel.cppm +++ b/modules/toolchain-model/src/linkmodel.cppm @@ -223,7 +223,7 @@ struct ClangDriverModel { // A third, declared-metadata source (a persisted `.xpkg-exports.json` // written by the installer) was evaluated and removed: its only consumer // would have been this resolver, while the two sources above already cover -// every real payload — see docs/08-toolchain-internals.md for the record. +// every real payload — see docs/91-toolchain-internals.md for the record. // // Returns the loader's absolute path, or empty when none was found (callers // then omit --dynamic-linker and the hermeticity check reports the gap). diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index cf098f54..8b01aa08 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -571,7 +571,7 @@ inline Triple host_triple() { // ── Version pins (single site; §4.6 of the design doc) ─────────────────────── // Every default/convention toolchain version literal lives here. Help and // error strings format these — never inline a pinned version elsewhere. -// Changing a pin: update this block, then sync docs/03-toolchains.md and the +// Changing a pin: update this block, then sync docs/20-toolchains.md and the // README platform table (drawn from kKnownTargets above). namespace pins { // First-run auto-install defaults (prepare.cppm), per host platform/arch. diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 9558c133..8444cd22 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -23,7 +23,7 @@ // SINGLE SOURCE OF TRUTH. `.github/tools/check_version_pins.sh` reads the // literal below and cross-checks it against `mcpp.toml`'s `[package].version`; // tests/e2e/01_help_and_version.sh checks it against `mcpp --version` at -// runtime. Both must be updated together — see docs/09-release.md. +// runtime. Both must be updated together — see docs/92-release.md. export module mcpp.version; diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 0702743c..128ea6b4 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -1656,7 +1656,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { // same result — not a race, never run. // // PER PACKAGE, not per build. `include_dir` colours only the declaring - // package's own TUs (docs/07-build-mcpp.md), so a generated header is + // package's own TUs (docs/30-build-mcpp.md), so a generated header is // visible to exactly one package and a build-wide phony would encode a // dependency that does not exist. It would also land on the critical path // of a build whose wall clock IS its critical path. diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 2524d422..776a6467 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -1004,7 +1004,7 @@ export struct BuildOverrides { // Is this git remote reachable without a network round-trip? // -// `--offline` means "never touch the network" (docs/05-mcpp-toml.md), and its +// `--offline` means "never touch the network" (docs/04-mcpp-toml.md), and its // standing promise is that anything already on disk still builds. A remote that // names a local directory — or a file:// URL — is served by plain filesystem // reads, so refusing it would break that promise without buying any isolation. diff --git a/src/cli.cppm b/src/cli.cppm index 3cebb232..a3e4db36 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -525,7 +525,7 @@ int run(int argc, char** argv) { .option(cl::Option("toolchain").takes_value() .help("Ask about this toolchain, e.g. llvm@22.1.8")) .option(cl::Option("format").takes_value().value_name("json") - .help("Machine-readable output (enveloped; see docs/11-machine-output.md)")) + .help("Machine-readable output (enveloped; see docs/50-machine-output.md)")) .action(wrap_rc(cmd_why))) .subcommand(cl::App("resolve") .description("Re-resolve the build plan and explain it") @@ -630,7 +630,7 @@ int run(int argc, char** argv) { .option(cl::Option("json") .help("Emit machine-readable JSON (legacy payload, kept for ever)")) .option(cl::Option("format").takes_value().value_name("json") - .help("Machine-readable output (enveloped; see docs/11-machine-output.md)")) + .help("Machine-readable output (enveloped; see docs/50-machine-output.md)")) .option(cl::Option("allow-unknown") .help("Downgrade unknown mcpp-segment keys from error to warning")) .option(cl::Option("all-os") @@ -650,7 +650,7 @@ int run(int argc, char** argv) { .description("Install / list / select / remove C++ toolchains") .subcommand(cl::App("list").description("List installed toolchains") .option(cl::Option("format").takes_value().value_name("json") - .help("Machine-readable output (enveloped; see docs/11-machine-output.md)"))) + .help("Machine-readable output (enveloped; see docs/50-machine-output.md)"))) .subcommand(cl::App("install") .description("Install a toolchain via mcpp's xlings") // Both `mcpp toolchain install gcc 16.1.0` and `mcpp toolchain @@ -686,7 +686,7 @@ int run(int argc, char** argv) { .option(cl::Option("json") .help("Emit machine-readable JSON (legacy payload, kept for ever)")) .option(cl::Option("format").takes_value().value_name("json") - .help("Machine-readable output (enveloped; see docs/11-machine-output.md)"))) + .help("Machine-readable output (enveloped; see docs/50-machine-output.md)"))) .subcommand(cl::App("info") .description("Show details (incl. key inputs) for a cached package") .arg(cl::Arg("pkg").help("@").required())) @@ -777,7 +777,7 @@ int run(int argc, char** argv) { .subcommand(cl::App("env") .description("Print mcpp paths and configuration") .option(cl::Option("format").takes_value().value_name("json") - .help("Machine-readable output (enveloped; see docs/11-machine-output.md)"))) + .help("Machine-readable output (enveloped; see docs/50-machine-output.md)"))) .subcommand(cl::App("config") .description("Show or modify mcpp's private xlings configuration") .option(cl::Option("mirror").takes_value().value_name("CN|GLOBAL") diff --git a/src/config.cppm b/src/config.cppm index d26ab064..75d4b1ae 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -1,6 +1,6 @@ // mcpp.config — global config + paths + xlings binary acquisition. // -// Layout (per docs/14-data-layout.md): +// Layout: // $MCPP_HOME/ default ~/.mcpp/ // bin/mcpp mcpp binary (self-contained mode) // registry/ XLINGS_HOME for mcpp's xlings diff --git a/src/hooks.cppm b/src/hooks.cppm index dc1314e2..2f01c82c 100644 --- a/src/hooks.cppm +++ b/src/hooks.cppm @@ -3,7 +3,7 @@ // EXPERIMENTAL. A hook cannot currently change whether a build succeeded: // every failure is a warning, and `side_effect = true` is refused by the // manifest parser. Only the ROOT project's hooks are ever run — a dependency's -// `[hooks]` is inert. See docs/05-mcpp-toml.md §2.16. +// `[hooks]` is inert. See docs/04-mcpp-toml.md §2.16. // // The CONFIGURATION is not parsed here: `[hooks]` is a section of mcpp.toml // and mcpp.toml has one parser (mcpp.manifest). What lives here is the part diff --git a/src/modgraph/p1689.cppm b/src/modgraph/p1689.cppm index a591c641..7d7aa5af 100644 --- a/src/modgraph/p1689.cppm +++ b/src/modgraph/p1689.cppm @@ -14,7 +14,7 @@ // We hand-parse the .ddi (no nlohmann::json dep — same justification // as fetcher.cppm). // -// Spec: docs/27-p1689-dyndep.md. +// Spec: P1689R5, the dependency format ninja consumes. module; diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index de0dd2c2..413800c5 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -15,7 +15,7 @@ // mcpp.pack.binfmt reads the import table instead, so a Linux box packaging a // Windows build is simply what happens when nothing has to be executed. // -// See docs/35-pack-design.md for the full design. Three modes: +// See docs/10-pack-and-release.md for the full design. Three modes: // Static full musl static, no PT_INTERP / RUNPATH // BundleProject bundle only the project's third-party .so (default) // BundleAll bundle every dynamic dep incl. libc / libstdc++ / ld diff --git a/src/pm/publisher.cppm b/src/pm/publisher.cppm index c0411416..79cb671d 100644 --- a/src/pm/publisher.cppm +++ b/src/pm/publisher.cppm @@ -1,6 +1,6 @@ // mcpp.pm.publisher — generate xpkg Lua entry from mcpp.toml + scanner. // -// See docs/04-schema-xpkg-extension.md for the produced layout. +// See docs/11-publishing-a-library.md for the produced layout. module; diff --git a/src/publish/pipeline.cppm b/src/publish/pipeline.cppm index e08cd70f..d1f7ca46 100644 --- a/src/publish/pipeline.cppm +++ b/src/publish/pipeline.cppm @@ -180,7 +180,7 @@ export int publish_package(bool dry_run, bool allow_dirty) { // `mcpp publish --auto` to: // - gh release create v // - fork mcpp-index, add pkg lua, gh pr create - // See docs/34-release-readiness.md §3. + // See docs/11-publishing-a-library.md. std::println("Tip: future versions of mcpp may automate steps 2-3 via the gh CLI."); return 0; } diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 243dfdb2..a8b26377 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -3,7 +3,7 @@ module; // mcpp.toolchain.stdmod — pre-build the `import std` BMI and cache it. // -// GCC 15 flow (from docs/11-gcc15-cookbook.md §2): +// GCC 15 flow (see docs/91-toolchain-internals.md): // g++ -std=c++23 -fmodules -Og -c -o std.o // ⇒ produces gcm.cache/std.gcm + std.o // diff --git a/tests/e2e/150_clang_module_operator_template.sh b/tests/e2e/150_clang_module_operator_template.sh index b1a1368b..2b6cd53b 100755 --- a/tests/e2e/150_clang_module_operator_template.sh +++ b/tests/e2e/150_clang_module_operator_template.sh @@ -117,7 +117,7 @@ if [[ "$ACTUAL" != "$EXPECTED" ]]; then if [[ "$ACTUAL" == "ok" ]]; then echo " The importer now compiles — the Clang regression appears FIXED." echo " Update the expectation table here and the hazard section in" - echo " docs/03-toolchains.md, and tell mcpp-community/mcpp#256." + echo " docs/20-toolchains.md, and tell mcpp-community/mcpp#256." else echo " The importer now crashes on a version previously known good." echo " A toolchain bump has re-broken module operator templates." diff --git a/tests/e2e/186_build_mcpp_protocol_and_bound.sh b/tests/e2e/186_build_mcpp_protocol_and_bound.sh index 9317f886..c3371a99 100755 --- a/tests/e2e/186_build_mcpp_protocol_and_bound.sh +++ b/tests/e2e/186_build_mcpp_protocol_and_bound.sh @@ -16,7 +16,7 @@ # * run bound — a build program that hangs used to hang the whole build with # no diagnostic at all. # -# See .agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md §4. +# See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §4. set -e TMP=$(mktemp -d) diff --git a/tests/e2e/188_build_actions.sh b/tests/e2e/188_build_actions.sh index ddace435..ecf1f8a3 100755 --- a/tests/e2e/188_build_actions.sh +++ b/tests/e2e/188_build_actions.sh @@ -22,7 +22,7 @@ # # Also: a malformed action is refused rather than silently skipped. # -# See .agents/docs/2026-08-05-build-mcpp-extensibility-architecture.md §3.1. +# See .agents/docs/2026-08-30-build-mcpp-extensibility-architecture.md §3.1. set -e TMP=$(mktemp -d) diff --git a/tests/e2e/315_blocking_check_gates_compilation.sh b/tests/e2e/315_blocking_check_gates_compilation.sh index be91eb69..1e9b5ac9 100755 --- a/tests/e2e/315_blocking_check_gates_compilation.sh +++ b/tests/e2e/315_blocking_check_gates_compilation.sh @@ -5,7 +5,7 @@ # # WHAT THIS DEFENDS. `blocking` was typed (`BuildAction::blocking`), emitted # over the build-program protocol (`hostprogram.cppm`), parsed -# (`directives.cppm`), documented in both languages (`docs/07-build-mcpp.md` +# (`directives.cppm`), documented in both languages (`docs/30-build-mcpp.md` # and its Chinese counterpart) and demonstrated in a shipped example # (`examples/08-build-rules/rules-tidy`) — and read by nothing. The only # order-only edge the ninja backend emitted was the staged-BMI one. So a check diff --git a/tests/e2e/616_examples_and_their_index_agree.sh b/tests/e2e/616_examples_and_their_index_agree.sh index 44c5c010..de5c6898 100755 --- a/tests/e2e/616_examples_and_their_index_agree.sh +++ b/tests/e2e/616_examples_and_their_index_agree.sh @@ -22,7 +22,7 @@ ROOT="${ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}" fails=0 fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); } -for doc in "$ROOT/docs/01-examples.md" "$ROOT/docs/zh/01-examples.md"; do +for doc in "$ROOT/docs/03-examples.md" "$ROOT/docs/zh/03-examples.md"; do [ -f "$doc" ] || { fail "missing $doc"; continue; } printf -- '--- %s ---\n' "${doc#"$ROOT"/}" diff --git a/tests/unit/test_wire.cpp b/tests/unit/test_wire.cpp index 9d7765a3..3c4811dd 100644 --- a/tests/unit/test_wire.cpp +++ b/tests/unit/test_wire.cpp @@ -160,7 +160,7 @@ TEST(WireFormat, LegacyJsonIsNotTheSameFormatAsJson) { // ── Golden shapes, one per kind ──────────────────────────────────────────── // -// docs/11-machine-output.md promises that within a kindVersion fields are +// docs/50-machine-output.md promises that within a kindVersion fields are // added and never removed or renamed. A promise nobody can break is the thing // this whole module was written against — `xlings interface --list` declares // 20 capabilities whose outputSchema is, for all 20, only @@ -179,7 +179,7 @@ void expect_has_keys(const nlohmann::json& obj, << what << " lost published key '" << k << "'. Removing or renaming one is a breaking change: bump the " "kind's version in mcpp.wire and say so in " - "docs/11-machine-output.md."; + "docs/50-machine-output.md."; } } // namespace