diff --git a/.agents/skills/mcpp-docs-style/SKILL.md b/.agents/skills/mcpp-docs-style/SKILL.md new file mode 100644 index 00000000..ec667d26 --- /dev/null +++ b/.agents/skills/mcpp-docs-style/SKILL.md @@ -0,0 +1,154 @@ +--- +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. +--- + +# mcpp 文档风格规范 + +## 适用范围 + +`docs/**`(含 `docs/zh/**`)、`README.md`、`.agents/docs/**` 的对外部分。 + +代码注释与 commit message **不受本规范约束** —— 它们的读者、篇幅与目的都不同, +那里允许并鼓励叙述「为什么」以及实测过程。本规范约束的是**面向用户的文档**。 + +## 一、总原则 + +文档是**参考资料**,不是博客,也不是聊天记录。判据只有一条: + +> 一位不认识作者、只想解决自己问题的工程师,能不能在最短时间内 +> 拿到准确的事实,并且不会误以为某个说法比实际更随意或更绝对。 + +由此得到三条可执行的规则:陈述、精确、克制。 + +## 二、标题 + +**标题一律是名词短语或陈述句,不使用疑问句、不使用口语片段。** + +疑问句标题把「读者已经知道自己在找什么」这个前提丢掉了 —— 目录里一列问句, +读者要先把每个问句翻译成主题才能定位。 + +| 不采用 | 采用 | +|---|---| +| 一段话讲完 | 概述 | +| 打什么由谁决定 | 打包内容的决定依据 | +| 哪些 `.cppm` 会被发布 | 发布的接口单元 | +| 消费者的构建会检查什么 | 消费端的构建检查 | +| 怎么消费 | 消费方式 | +| 老版本 mcpp 拿到这种包会怎样 | 旧版本 mcpp 的行为 | +| 为什么两者都不许裁剪 | 两个集合不可裁剪的原因 | +| 这些说法验证到哪一步、在哪台机器上 | 验证范围 | +| The whole idea in one paragraph | Overview | +| 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 | + +「…的原因」「…的依据」「…的范围」是把 why 型标题转成名词短语的常用形。 +**保留 why 本身,去掉疑问语气。** + +## 三、词汇 + +### 不采用的类别 + +1. **网络用语与口语**:搞定、干活、坑、真香、翻车、打脸、一把梭、白给、 + 凉了、炸了、神器、黑科技、敲黑板、划重点。 +2. **拟人与比喻性行话**:姊妹篇、腿(fat package 的一份产物)、travel(源码 + 「旅行」)、picky、happy path 的中文直译。技术术语本身可以是比喻 + (rpath、sysroot),但**不要新造比喻**。 +3. **填充语**:其实、说白了、简单来说、众所周知、显然、当然、值得一提的是。 + 如果一件事显然,就不必说;如果不显然,「显然」会让读者怀疑自己。 +4. **含糊的程度词**:很快、非常、极其、基本上、差不多。用数字或范围替代 —— + 「2.42×」「64.77s」「四个平台中的三个」。 + +### 人称 + +默认**不使用第二人称**。写动作的对象,不写「你」。 + +- 不采用:你可以在 `mcpp.toml` 里写 … +- 采用:在 `mcpp.toml` 中声明 … + +例外:**教程体**文档可以使用第二人称,因为那里读者正在跟着做。教程体是 +**列出来的,不是推断的**:`00-getting-started.md`、`01-examples.md`、 +`04-build-from-source.md`。其余全部按参考文档处理。 + +引用 mcpp 自身输出的部分不受此限:`did you mean 'x86_64-linux-musl'?` 与 +`your toolchain : …` 是程序打印的原文,**逐字复现是要求,不是文风问题**。 +检查脚本因此会先剔除行内代码段再判定。 + +## 四、句式 + +- **陈述句优先。** 命令式仅用于操作步骤(「运行 `mcpp build`」)。 +- **一句话一个事实。** 从句套从句的长句拆开。 +- **不使用反问。**「难道不应该……吗?」没有信息量。 +- **不使用感叹号。** +- **破折号克制使用**:插入语用逗号或括号;破折号留给「随后是对前半句的 + 重述或收束」这一种用法。 + +## 五、断言的强度必须与证据相符 + +这是本规范里最实质的一条,也是最容易违反的一条。 + +| 证据 | 允许的表述 | +|---|---| +| 跑过、有输出 | 「实测」「测量得到」,并给出数字或报错原文 | +| 读代码推断 | 「按 X 的实现」「由 Y 决定」 | +| 未验证 | 「未验证」「尚无测试覆盖」—— **必须写出来** | + +**不要把推断写成实测。** 反例(本仓库真实发生过):把「守卫在原生构建上失效」 +写成实测结论,而它是从「`targetTriple` 结构上可能为空」推断的;实际运行时 +它非空,结论不成立。判据:**「结构上可能」不等于「运行时确实」—— +要么读运行时产物,要么不要写成实测。** + +同理,不要用「完全」「永远」「所有平台」这类全称词,除非确实逐个验证过; +写「已在 Linux / macOS / Windows 验证」比写「全平台可用」更有价值, +因为前者可被检验。 + +## 六、双语对照 + +`docs/X.md` 与 `docs/zh/X.md` 是**同一份文档的两个版本**,不是两篇文章。 + +- 章节结构、标题层级、表格行数必须一一对应; +- 代码块、命令、报错原文**逐字相同**,不翻译; +- 术语表统一:module interface unit / 模块接口单元、implementation partition / + 实现分区、import library / 导入库、install name / install name(不译)。 +- 改动一侧时**同时改另一侧**。只改一侧会让两份文档随时间分叉, + 而读者无从知道哪一份是新的。 + +## 七、结构 + +- 顶部一段引言说明**这份文档回答什么问题**,以及相关文档的链接 + (用「相关文档:」,不用「姊妹篇」)。 +- 表格用于枚举与对照,散文用于因果。**不要用散文列举**。 +- 「当前边界 / Current limitations」一节是必要的,不是可选的: + 没有写出边界的文档,读者只能靠踩到才知道。 + +## 八、机器检查 + +规则里可判定的那一半由 `.github/tools/check_docs_style.sh` 执行: + +``` +bash .github/tools/check_docs_style.sh +``` + +它检查三条:标题不是疑问句/口语片段;参考文档不使用第二人称; +`docs/X.md` 与 `docs/zh/X.md` 的标题结构一致(按层级序列比对, +并剔除代码块内的 `#` 注释 —— 第一版脚本把 ```sh 块里的 `# GET, never HEAD` +数成了标题,报出一个并不存在的结构分歧)。 + +**它不检查第五节** —— 断言强度与证据是否相符需要读者判断,而那是本规范里 +最重要的一条。脚本能做的事不等于规范的全部。 + +## 九、自检清单 + +提交文档改动前: + +``` +[ ] 标题没有疑问句、没有口语片段 +[ ] 没有网络用语、没有新造比喻 +[ ] 没有第二人称(教程体除外) +[ ] 每条「实测」都有数字、路径或报错原文 +[ ] 没有未经验证的全称断言 +[ ] 中英两版结构对应,代码块逐字一致 +[ ] 有「当前边界」一节 +[ ] `bash .github/tools/check_docs_style.sh` 通过 +``` diff --git a/.github/tools/check_docs_style.sh b/.github/tools/check_docs_style.sh new file mode 100755 index 00000000..56e220ff --- /dev/null +++ b/.github/tools/check_docs_style.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# check_docs_style.sh — the mechanically checkable half of .agents/skills/mcpp-docs-style. +# +# A style rule nobody can check is a suggestion. This covers the three rules +# that are decidable from the text alone: +# +# 1. headings are not questions and not conversational fragments +# 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 +# +# 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. +# +# Usage: bash .github/tools/check_docs_style.sh +set -uo pipefail +cd "$(dirname "$0")/../.." || exit 1 + +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" + +# 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 +# parity gap in docs/10 that did not exist. +headings() { + awk ' + /^```/ { infence = !infence; next } + !infence && /^#{1,6} / { print } + ' "$1" +} + +for f in docs/*.md docs/zh/*.md; do + base="$(basename "$f")" + + # ── 1. heading register ─────────────────────────────────────────────── + while IFS= read -r h; do + case "$h" in + *"?"*|*"吗"*|*"呢"*) + bad "$f: question heading — use a noun phrase: $h" ;; + esac + case "$h" 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") + + # ── 2. second person in reference docs ──────────────────────────────── + case " $TUTORIALS " in + *" $base "*) ;; + *) + # Prose only: quoted program output keeps its own wording ("your + # toolchain : …" comes out of mcpp and must be reproduced verbatim), so + # fenced blocks and lines that are clearly transcript are skipped. + hits=$(awk ' + /^```/ { infence = !infence; next } + infence { next } + { + # Inline code spans are quoted material — mcpp prints + # `did you mean ...?` and `your toolchain : ...`, and reproducing + # those verbatim is required, not a style lapse. Blank them before + # matching rather than exempting whole lines, so prose on the same + # line is still checked. + line = $0 + gsub(/`[^`]*`/, "", line) + if (line ~ /\|\|\/ || line ~ /你/) + print FILENAME ":" FNR ": " $0 + } + ' "$f") + if [[ -n "$hits" ]]; then + while IFS= read -r line; do + bad "$f: second person in a reference doc: ${line#*: }" + done <<< "$hits" + fi ;; + esac +done + +# ── 3. bilingual heading parity ─────────────────────────────────────────── +for f in docs/*.md; do + z="docs/zh/$(basename "$f")" + [[ -f "$z" ]] || continue + # The LEVEL SEQUENCE, not just the count: two documents can have the same + # number of headings and still disagree about which are sections and which + # are subsections. Comparing counts would call that identical. + levels() { headings "$1" | sed -E 's/^(#+).*/\1/' | awk '{print length($0)}'; } + if ! diff -q <(levels "$f") <(levels "$z") >/dev/null; then + ne=$(headings "$f" | wc -l); nz=$(headings "$z" | wc -l) + bad "$(basename "$f"): heading structure differs (en=$ne zh=$nz headings); first divergence:" + diff <(levels "$f") <(levels "$z") | head -4 | sed 's/^/ /' + fi +done + +if [[ "$fail" -eq 0 ]]; then + echo "OK: docs style checks pass" +fi +exit "$fail" diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 76eefa4f..8decb8ef 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -55,6 +55,12 @@ jobs: - name: Check version / xlings pin consistency run: bash .github/tools/check_version_pins.sh + # Same placement and the same reason: pure text, no toolchain, and the + # drift it catches — a 简体中文 page that has silently fallen behind its + # English original — is invisible to every other job. + - name: Check documentation style and bilingual parity + run: bash .github/tools/check_docs_style.sh + - uses: ./.github/actions/bootstrap-mcpp - name: Configure mirror + Build mcpp from source (self-host) diff --git a/docs/02-pack-and-release.md b/docs/02-pack-and-release.md index a9100649..ff44a63f 100644 --- a/docs/02-pack-and-release.md +++ b/docs/02-pack-and-release.md @@ -13,7 +13,7 @@ > deliverable. Three routes turn it into one — and none of them uses the host's > C library. -## Three ways to ship +## Three distribution routes Every route below produces an artifact whose C runtime comes from the ecosystem, never from `/lib64`. That is deliberate: mcpp builds against a @@ -24,16 +24,16 @@ libc to distribute would give that away at the last step. | | Route | Command | Where its C runtime comes from | Choose it when | |---|---|---|---|---| | **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 | you want a single file with no runtime at all | +| **B** | One static file | `mcpp build --target x86_64-linux-musl` | nowhere — it is linked in | a single file with no runtime dependency | | **C** | Carry the runtime | `mcpp pack --mode self-contained` | shipped inside the bundle | any Linux, including older than the build machine | -**On route A, and the thing that surprises people.** The `PT_INTERP` baked into -a freshly built binary points at *your* machine's payload, so copying that file -to another machine by hand does not work — the path is not there. That is not a -property of the artifact so much as of the copy: installed through `xlings`, the -package's ELF files are repointed at the target machine's own payloads at -install time. The baked path is a build-machine detail, not a distribution -format. If you are hand-copying binaries between machines, you want B or C. +**On route A.** The `PT_INTERP` recorded in a freshly built binary points at the +build machine's payload, so copying that file to another machine by hand does not +work: the path does not exist there. This is a property of the copy rather than of +the artifact — installed through `xlings`, the package's ELF files are repointed at +the target machine's own payloads at install time. The recorded path is a +build-machine detail, not a distribution format. Routes B and C are the ones that +survive hand-copying. **On route B.** `--target …-musl` implies a static link, so there is no loader, no RUNPATH and nothing to find at run time. It is the smallest and most @@ -213,10 +213,10 @@ exec "$here/lib/ld-linux-x86-64.so.2" --library-path "$here/lib" "$here/bin/myap The layout and wrapper above use an x86_64 example. The packer derives the loader name from the target; for aarch64 it is `ld-linux-aarch64.so.1`. -#### Trap: `/proc/self/exe` under the bundled loader +#### `/proc/self/exe` under the bundled loader Being started *by* the loader has a consequence the layout above does not -show: the kernel sets `/proc/self/exe` to the **loader**, not to your program, +show: the kernel sets `/proc/self/exe` to the **loader**, not to the program, and `/proc/self/cmdline` carries the `--library-path` argument. Every "find my resources next to the executable" path therefore resolves against `lib/` instead of the bundle root — and it does so silently. In practice that means @@ -244,7 +244,7 @@ own resolution, say — use `--mode vendored` instead. It repoints `PT_INTERP` at the host loader, at the cost of requiring the host's glibc to be at least as new as the one the artifact was built against. -### Windows (PE) — a `.zip`, and the DLLs sit beside the `.exe` +### Windows (PE): a `.zip`, with the DLLs beside the `.exe` A Windows target produces a **`.zip`**, not a `.tar.gz`, and the layout is flat: diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 289aa58f..29f5fbeb 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -8,7 +8,7 @@ C++23 modules are fairly sensitive to compiler versions, and different releases ## Automatic Installation -The first time you run `mcpp build`, if no toolchain is configured yet, mcpp +On the first `mcpp build` with no toolchain configured, mcpp installs and persists a default pair for the current host. The choice is host-aware: @@ -171,7 +171,7 @@ splits on the *env* segment, not on the OS: | `x86_64-windows-msvc` | `foo.lib` (MSVC convention) | Before 2026.8.3.3 a mingw build on a Windows host emitted `foo.lib` — a GNU -archive wearing an MSVC name, which MSVC cannot consume. If you have a script +archive wearing an MSVC name, which MSVC cannot consume. A script that globs `*.lib` out of a `windows-gnu` build, it needs to glob `*.a` now. ## Linux ELF from Windows (`x86_64-linux-musl`, no WSL required) @@ -218,10 +218,10 @@ Targets block, that host genuinely cannot serve it (implemented by An MSVC toolset reaches a build one of two ways, and the **version axis of the spec** says which: -| Spec | Origin | Which compiler you get | +| Spec | Origin | Compiler resolved | |---|---|---| | `msvc@system` (or bare `msvc`) | the machine's own Visual Studio | whatever is installed here | -| `msvc@` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the one you named, on every machine | +| `msvc@` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the named toolset, identically on every machine | They are not alternatives to pick between once — they answer different questions. `msvc@system` asks *"use what this developer already has"*; @@ -236,7 +236,7 @@ Pinned toolsets coexist with each other and with a system Visual Studio. > refusing to use what is already installed would cost more than it buys: > Visual Studio is very often present and cannot always be redistributed. > `@system` for any other family is an error that names both things -> you might have meant. (The family-less `[toolchain] … = "system"` — the PATH +> that may have been intended. (The family-less `[toolchain] … = "system"` — the PATH > compiler — is a separate and deliberate escape hatch, and is unaffected.) ### `msvc@system` — the machine's own Visual Studio @@ -267,7 +267,7 @@ Default set to msvc@system (was: llvm@20.1.7) ``` If no Visual Studio is installed, mcpp says so and offers both routes — a -pinned toolset it can install for you, or the Visual Studio Installer / +pinned toolset it can install, or the Visual Studio Installer / `winget install Microsoft.VisualStudio.2022.BuildTools`. `mcpp toolchain list` shows the detected MSVC in a separate `System:` section, @@ -459,7 +459,7 @@ unaffected, so this is a regression somewhere between Clang 18 and 20. This bites the module-package pattern directly. Wrapping an upstream header whose operators are `static inline` templates, and mirroring their signatures with a trivially-true constraint (the standard mixed-TU subsumption recipe), -is exactly how you hit it. +is exactly how it is reached. **The rule of thumb:** every template parameter should be pinned by the **first** function argument. Shapes that break this are the poisonous ones: diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 4f572b5e..e5facc93 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -1,6 +1,6 @@ # 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 your project root and `mcpp build` will discover and read it automatically. +`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 @@ -52,9 +52,9 @@ repo = "https://github.com/user/myapp" # Repository URL (optional) - `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`: use when you need C++26 language features. +- `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`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key. +- `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. @@ -162,7 +162,7 @@ the package/feature boundary, not on an individual target. ```toml [build] sources = ["src/**/*.cppm", "src/**/*.cpp"] # Source globs (default: src/**/*.{cppm,cpp,cc,c,S,s,asm}) -module_extensions = [".ixx"] # Extra extensions your module INTERFACES use (§ below) +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) @@ -219,7 +219,7 @@ 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 mysteriously slower than you asked for. +restores the default is a build slower than requested, with no indication why. `bmi_schedule` decides when importers are unblocked. @@ -246,7 +246,7 @@ through CI on every platform. `edit-comment` — there is no owed work to move off the critical path, and the key buys nothing. See the [benchmark](../README.md#benchmark). -**How it works** differs per compiler and is chosen for you: gcc publishes its +**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. @@ -294,7 +294,7 @@ 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. If you publish a package that uses +> 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`) @@ -389,7 +389,7 @@ 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 your own code emits (weak/COMDAT `std::string` +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. @@ -434,8 +434,8 @@ never staged: it may not be redistributed. > 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. If your -> project set it while the key did nothing, decide which one you actually want. +> 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 @@ -457,7 +457,7 @@ toolchain's libraries first. > 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 your code. See mcpp-community/mcpp#336. +> 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 @@ -639,7 +639,7 @@ under `mcpp build --strict`. 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 quietly put you on a newer tip. Ask for the newer tip +another machine cannot silently move the build to a newer tip. A newer tip must be requested explicitly: ```bash @@ -681,7 +681,7 @@ There is no ordered fallback or fuzzy, index-wide search by short name: [dependencies] chriskohlhoff.asio = "1.38.1" -# ✅ Correct — namespace sub-table (preferred when you have several from one org) +# ✅ Correct — namespace sub-table (preferred for several packages from one org) [dependencies.chriskohlhoff] asio = "1.38.1" @@ -715,8 +715,8 @@ package: ``` What reaches `mcpp.lock`, the install layer and the cache is the canonical -identity, so the ambiguous spelling lives in exactly one place — your manifest -— until you change it. `mcpp add gtest@1.15.2` changes it for you. +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 @@ -749,7 +749,7 @@ 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 your index CI. Descriptor identity +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/spec/package-identity.md`. @@ -771,13 +771,13 @@ 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 -**versions your local index knows**. If `1.3.0` was published upstream after -your last refresh, you will not see it until you ask: +**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 # what you have locally: state, age and revision +mcpp index status # local state: state, age and revision ``` Controls, in order of precedence: @@ -825,7 +825,7 @@ The selector `` has three forms: | **`cfg(...)` predicate** | a compound condition (arch / env / combinators) | `[target.'cfg(all(linux, not(arch = "aarch64")))']` | | **exact triple** | one specific target (also carries `toolchain` / `linkage`) | `[target.x86_64-linux-musl]` | -Under a selector you may put platform-conditional **dependencies** and **build flags**: +A selector may carry platform-conditional **dependencies** and **build flags**: ```toml # Concise bare-alias form — pull OpenBLAS and link it only on Windows. @@ -843,7 +843,7 @@ 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(...)` -when you need arch/env conditions or combinators. +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 @@ -957,7 +957,7 @@ simd = { sources = ["src/simd/**"], flags = [ 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 you pick +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. @@ -1000,7 +1000,7 @@ that turns a silently-wrong or missing backend into a loud configure-time error. 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* you declare it, not a flag. +optionality is expressed by *where* the entry is declared, not by a flag. ```toml [features] @@ -1100,7 +1100,7 @@ ldflags = [] - **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 you + rebuild. It also means the disk cost scales with the number of profiles actually use. ### 2.10 `[build] cache` — The Global Dependency Cache @@ -1345,8 +1345,8 @@ Four properties worth knowing: 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 - your build. That is safe precisely because an executable has no ABI contact - with your code. + 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 @@ -1358,7 +1358,7 @@ Four properties worth knowing: - **Cached globally**, keyed on package version × host toolchain × features × its own dependency closure — built once per machine, not once per project. -#### `[tools.overrides]` — use a binary you already have +#### `[tools.overrides]` — use an existing binary ```toml [tools.overrides] @@ -1401,7 +1401,7 @@ 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 you already have, written in **C++** — no second language, which is the +manager already in use, written in **C++** — no second language, which is the whole point of `build.mcpp` existing. **The module name is the package's `name`.** mcpp registers the host module @@ -1418,7 +1418,7 @@ missing one is reported as *"host module 'x': no interface unit at …"*. bundled `mcpp` module, but not a third package. A rule package is a leaf. *Build-time only:* a `host-module = true` dependency is **not** compiled into -or linked with your target. It exists to run during `build.mcpp` and nowhere +or linked with the target. It exists to run during `build.mcpp` and nowhere else — the same separation Cargo draws with `[build-dependencies]`. (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.) @@ -1457,13 +1457,13 @@ 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 your build program's tool namespace without saying so. - Handing something to your consumers is a supply-chain statement; it has to be + 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 a dependency you already declare.** gRPC +- **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` / @@ -1520,7 +1520,7 @@ 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 for you. +generates the resource script automatically. | Key | Type | Meaning | |---|---|---| @@ -1538,9 +1538,9 @@ 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 your working tree, not about the target, so a typo in -`icon = "assets/app.ico"` is caught by your Linux or macOS build (and by their -CI jobs) instead of waiting for the Windows one. If you do not want an icon, +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 @@ -1548,15 +1548,15 @@ delete the line. the version verbatim, so a form the numeric fields cannot hold (`1.0.0-rc1`) still shows up in the Properties dialog. -#### Writing your own `.rc` +#### Supplying a hand-written `.rc` ```toml [resources] files = ["res/app.rc"] ``` -With `files` set, mcpp stops generating a version resource — you own the -resource ID space. Set `version-info = true` alongside it if you want both (and +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 @@ -1576,11 +1576,11 @@ never changes what ships. 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 your icon relinks. Angled includes (``) are the toolchain's +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 asks you to declare it: +scan. mcpp names what it could not resolve and requires an explicit declaration: ```toml extra-inputs = ["assets/app.ico"] @@ -1604,7 +1604,7 @@ o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o") 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 gives you `ninja: no work to +command: nothing tracks it, and editing the file produces `ninja: no work to do`. ## Appendix A. Schema Ownership Principle (admission criteria for new fields) diff --git a/docs/06-workspace.md b/docs/06-workspace.md index 8f1fd3b4..d1172241 100644 --- a/docs/06-workspace.md +++ b/docs/06-workspace.md @@ -1,6 +1,6 @@ # Workspace -A workspace lets you organize and manage 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. +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. ## 1. Overview diff --git a/docs/07-build-mcpp.md b/docs/07-build-mcpp.md index 8a5f6edb..96f7142e 100644 --- a/docs/07-build-mcpp.md +++ b/docs/07-build-mcpp.md @@ -2,13 +2,13 @@ **English** | [简体中文](zh/07-build-mcpp.md) -Most projects need nothing more than `mcpp.toml`. When you need build-time logic — +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 -`build.mcpp` in your project root. It is the mcpp analog of Zig's `build.zig` and +`build.mcpp` in the project root. It is the mcpp analog of Zig's `build.zig` and Cargo's `build.rs`, but written in **C++**: no second language, and it dogfoods mcpp itself. -mcpp compiles `build.mcpp` with your toolchain and runs it **before** the main +mcpp compiles `build.mcpp` with the project's toolchain and runs it **before** the main build. The program talks to mcpp by printing `mcpp:` directives to stdout; those directives augment the build. @@ -39,7 +39,7 @@ mcpp build # compiles + runs build.mcpp, then builds the project ## Directives Print these to stdout (one per line). Any line that does not start with `mcpp:` -is ignored, so you can freely log diagnostics. +is ignored, so diagnostics may be logged freely. | Directive | Effect | |---|---| @@ -56,7 +56,7 @@ is ignored, so you can freely log diagnostics. | `mcpp:rerun-if-env-changed=` | re-run `build.mcpp` when this env var changes | The program **requests** build edges (flags, libraries, sources). It cannot add a -registry dependency — keep your dependency graph declarative in `mcpp.toml` +registry dependency — the dependency graph stays declarative in `mcpp.toml` (including platform-conditional `[target.windows.dependencies]`). `build.mcpp` is for *leaf* decisions: flags, codegen, link requirements. @@ -68,9 +68,9 @@ interface and belongs in the declarative manifest/descriptor ## Typed API: `import mcpp;` (recommended) -Instead of printing raw strings you can write `build.mcpp` **modules-first** — +Instead of printing raw strings, `build.mcpp` can be written **modules-first** — `import mcpp;`, no `#include` needed. The `mcpp` module is bundled in the -mcpp binary (so it always matches your mcpp's protocol) and is compiled on demand; +mcpp binary (so it always matches that mcpp's protocol) and is compiled on demand; its functions just emit the directives above: ```cpp @@ -120,15 +120,15 @@ int main() { ``` mcpp builds that `kind = "bin"` target **for the build machine** (even under -`--target`), caches it globally, and hands you the path. The request lives in +`--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, including `[tools.overrides]` and `reexport = true` (which is how a library -hands you the whole toolchain so you declare **one** dependency instead of +provides the whole toolchain, so a project declares **one** dependency instead of four). -### Globbing your inputs: `rerun_if_changed_glob` (2026.8.6.2+) +### Globbing inputs: `rerun_if_changed_glob` (2026.8.6.2+) The re-run key is built from *declared* inputs. Declare files and it works; glob a directory and it does not — adding a `.proto` changes no declared file's @@ -214,7 +214,7 @@ link the command would never run and the build would say nothing. > Naming a pre-built object in `[build].ldflags` also reaches the linker, and > 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 gives you `ninja: no work to do`. For Windows resources specifically, use +> it reports `ninja: no work to do`. For Windows resources specifically, use > [`[resources]`](05-mcpp-toml.md) — > `object` is the escape hatch for everything else. @@ -230,7 +230,7 @@ a.output(gen.c_str()).provides("my.generated").imports("std").submit(); ``` mcpp seeds a placeholder carrying exactly that declaration so the prepare-time -scan agrees with what your generator will emit — the same assertion-plus- +scan agrees with what the generator will emit — the same assertion-plus- verification trade `[modules].scan_overrides` makes, and the compiler's own P1689 output checks it at build time. @@ -258,7 +258,7 @@ Two ways to talk to mcpp, and they carry **different compatibility promises**: | Unknown directive | **Hard error** | Warning, then ignored | Programs using `import mcpp;` automatically announce the protocol version they -were built against (`mcpp:protocol=`, emitted before `main` runs — you never +were built against (`mcpp:protocol=`, emitted before `main` runs — it never write it yourself). mcpp uses that two ways: - A program announcing a **newer** protocol than mcpp understands is **refused**, @@ -272,7 +272,7 @@ write it yourself). mcpp uses that two ways: A `printf`-style program announces nothing, so it keeps the historical warn-and-ignore behaviour. That surface is **frozen at the eleven directives in the table above** — it still works and will keep working, but new capabilities -land only in the typed API. Prefer `import mcpp;` for anything you intend to +land only in the typed API. Prefer `import mcpp;` for anything intended to maintain. ### `import std;` (mcpp 2026.8.2.1+) @@ -363,7 +363,7 @@ mcpp::generated(out.string().c_str()); ``` `mcpp::out_dir()` is always absolute, so this is correct in both roles and -needs no branch on which one you are in. +needs no branch on which of the two applies. A generated **module interface** is fine here: `.cppm` goes through the same scan as any other source, so a generated file declaring `export module …` can @@ -376,19 +376,19 @@ directives and re-runs only when something it depends on changed: - the `build.mcpp` source itself, - the toolchain, -- any file you declared with `rerun-if-changed`, -- any env var you declared with `rerun-if-env-changed`, +- any file declared with `rerun-if-changed`, +- any env var declared with `rerun-if-env-changed`, - (or a `generated` output / `source=` selection went missing), - (or the cache was written by an mcpp that interpreted a directive differently — the entry carries a format **epoch**, and a foreign one re-runs the program once instead of replaying values under the wrong meaning). -So **declare your inputs**: if your program reads `config.h` or the `USE_FAST` +So **declare the inputs**: if the program reads `config.h` or the `USE_FAST` variable, emit `mcpp:rerun-if-changed=config.h` / `mcpp:rerun-if-env-changed=USE_FAST`. This replaces the old "process exited 0, so assume it's fine" guesswork with an explicit input/output contract — incremental builds stay correct. -When nothing changed you'll see `build.mcpp up to date (cached)`; otherwise +When nothing changed the output is `build.mcpp up to date (cached)`; otherwise `build.mcpp compiling` / `running`. ## Notes & limits @@ -399,7 +399,7 @@ When nothing changed you'll see `build.mcpp up to date (cached)`; otherwise For purely declarative target gating, `[target.'cfg(...)']` tables remain the first choice — see [05 - mcpp.toml Manifest Guide](05-mcpp-toml.md). - **CWD is the project root**, so relative paths (`src/generated.cpp`) land where - you expect. + expected. - A non-zero exit from `build.mcpp` aborts the build and prints its output. - **The run is bounded** (mcpp 2026.8.5.1+): a build program gets **600 s** by default, after which mcpp kills it and fails the build naming the package. @@ -421,7 +421,7 @@ When nothing changed you'll see `build.mcpp up to date (cached)`; otherwise The value comes from the **owning package's** manifest, because its author is the one who knows how long the generator takes. When a dependency's build program times out, the error names the exact `mcpp.toml` to edit — editing - your own would change nothing. + a hand-written one would change nothing. Omitting the key is not the same as `0`: unset means "use the default bound", `0` means "no bound at all". diff --git a/docs/08-toolchain-internals.md b/docs/08-toolchain-internals.md index 63b5b2e3..519071d4 100644 --- a/docs/08-toolchain-internals.md +++ b/docs/08-toolchain-internals.md @@ -255,7 +255,7 @@ binding it actually selected. ## 3. The link model (`src/toolchain/linkmodel.cppm`) -`ToolchainLinkModel` answers exactly one question — *how do we compile and +`ToolchainLinkModel` answers exactly one question — *how does mcpp compile and link against this toolchain's C library* — and every consumer derives its flags from it: @@ -309,7 +309,7 @@ auto-install, manifest auto-install). > subset. The manifest path ran *nothing*, which is how a freshly > auto-installed llvm kept a stale, environment-dependent cfg (issue #195), > and how gcc once shipped a sandbox that couldn't find `stdlib.h`. "Which -> command you installed with" must never decide "whether the toolchain +> command used to install it" must never decide "whether the toolchain > works". **Trigger semantics — ask every build, act once:** @@ -519,7 +519,7 @@ loader coupling, so a host binary that stumbles onto them is at worst confused. The machinery is already arch-parameterized; the work is data: 1. add the glibc/musl loader names to the triple map in - `linkmodel.cppm::loader_filename` (the glob fallback covers you until + `linkmodel.cppm::loader_filename` (the glob fallback holds until then); 2. ship payload assets for the arch (glibc, linux-headers, the toolchain itself) — the aarch64-linux-musl cross target is the working precedent @@ -585,7 +585,7 @@ An **unservable target is refused** rather than quietly built for the host: supports this target"; `host_can_serve` (`registry.cppm`) answers the different question "can this machine produce it", and `prepare.cppm` now asks it — with an explicit `[target.X] toolchain = "…"` as the escape hatch for a cross toolchain -you supply yourself. +supplied by the author. ## 8. Source map diff --git a/docs/09-release.md b/docs/09-release.md index 72a60bf1..8ecda2b1 100644 --- a/docs/09-release.md +++ b/docs/09-release.md @@ -1,7 +1,7 @@ # 09 — Releasing mcpp How a release of **mcpp itself** reaches users. This is maintainer-facing; for -packaging *your own* project see [02 — Packaging for Release](02-pack-and-release.md). +packaging an *ordinary* project see [02 — Packaging for Release](02-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. @@ -10,7 +10,7 @@ of those commit messages contains a misdiagnosis that is corrected in §5. | Site | Group | Moves when | |---|---|---| -| `mcpp.toml` `[package].version` | **being built** | you start work on a new version | +| `mcpp.toml` `[package].version` | **being built** | work starts on a new version | | `src/version.cppm` `MCPP_VERSION` | **being built** | same commit as above (compiled-in copy) | | `.xlings.json` `[workspace].mcpp` | **bootstrapped from** | separately, *after* a release is installable | | `ci-fresh-install.yml` `MCPP_PIN` | **version under test** | **nothing — it is derived at run time** (§5) | @@ -167,7 +167,7 @@ is not yet installable. Bump it only after the release is published, mirrored, **and merged into xim-pkgindex** — otherwise every CI job fails with `package 'mcpp@' not found`. Once its syntax issue is repaired, `check_version_pins.sh` enforces the weaker "never newer than the version being -built"; the index condition is on you. +built"; the index condition requires human judgement. ## 5. `MCPP_PIN` is derived, and why that matters diff --git a/docs/10-publishing-a-library.md b/docs/10-publishing-a-library.md index fd80e0f1..25b65205 100644 --- a/docs/10-publishing-a-library.md +++ b/docs/10-publishing-a-library.md @@ -7,10 +7,10 @@ How a library becomes something `[dependencies]` can name. This is the releasing mcpp itself, and [02 - Packaging & Release](02-pack-and-release.md) is about `mcpp pack` bundling an application. -## The chain, in the only order that works +## Release sequence ``` -your repo merge → git tag → GitHub auto-generates the tag tarball +the library repo merge → git tag → GitHub auto-generates the tag tarball ↓ gitcode mirror a byte-identical copy, for the CN region ↓ @@ -143,7 +143,7 @@ compiles those files as ordinary translation units — a *wrong build* rather than a clean failure: the module interface produces no BMI, and the failure surfaces later, somewhere that names neither the key nor the file. -So if a package you publish uses `module_extensions`, declare an mcpp version +A published package that uses `module_extensions` must declare an mcpp version floor in its index descriptor. The floor mechanism must **degrade**: a package that is unavailable because the client is too old has to be reported as *unavailable*, never as *absent* — a client told "no such package" will keep diff --git a/docs/11-machine-output.md b/docs/11-machine-output.md index d3060866..76a6c2ac 100644 --- a/docs/11-machine-output.md +++ b/docs/11-machine-output.md @@ -1,19 +1,19 @@ # 11 — Machine-Readable Output mcpp writes for two audiences. This chapter is the contract for the second one: -programs. If you are building an editor extension, a CI script, or anything -that parses mcpp's output, this is what you may rely on. +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. The rule that matters most +## 1. Primary rule > **Detect the protocol by parsing stdout. Never by exit code, and never by > "the command did not fail".** Read stdout, try to parse it as JSON, and require `schemaVersion` and `kind` -to be present. If either is missing, this mcpp does not speak the protocol you +to be present. If either is missing, this mcpp does not speak the protocol the caller asked for. This is not a stylistic preference. `mcpp --protocol-version` looks like it @@ -138,7 +138,7 @@ cannot separate the harmless from the thing a gate exists for: | effect | meaning | |---|---| -| `init-mcpp-home` | may create `$MCPP_HOME` on first use. **Outside your project.** | +| `init-mcpp-home` | may create `$MCPP_HOME` on first use. **Outside the project directory.** | | `read-project` | reads the manifest and sources | | `write-project` | writes into the project tree (`target/`, the compile DB) | | `write-global-cache` | writes the shared build cache | @@ -168,7 +168,7 @@ output, and a warning would land in the middle of it. Both spellings are produced from the same source, so they always describe the same thing — one answer, two shapes. -## 6. What you may rely on, and what changes +## 6. Stability guarantees For each `kind`, within a `kindVersion`: @@ -207,7 +207,7 @@ mcpp self env --format json This path is read-only, deliberately. The human `mcpp self env` initialises `$MCPP_HOME` if it is missing — someone typing it at a prompt expects that — but a client asking *where things are* should not be what puts them there. On -a machine that has never run mcpp you get the paths it **would** use and +a machine that has never run mcpp, the output is the paths it **would** use and `initialized: false`, and the disk is untouched. That is why this exists at all: without it a client has to reimplement mcpp's diff --git a/docs/12-binary-distribution.md b/docs/12-binary-distribution.md index 8502bd32..3ad47500 100644 --- a/docs/12-binary-distribution.md +++ b/docs/12-binary-distribution.md @@ -3,14 +3,14 @@ **English** | [简体中文](zh/12-binary-distribution.md) > Ship a library as **interface + prebuilt binaries** instead of as source. -> This is the closed-source case, and the offline case, and the "our build farm -> already compiled this once" case. +> It applies to closed-source distribution, offline environments, and builds whose +> artifacts a build farm has already produced. > -> [02 - Packaging & Release](02-pack-and-release.md) is the sibling: bundling an +> Related: [02 - Packaging & Release](02-pack-and-release.md) covers bundling an > *application*. [10 - Publishing a Library](10-publishing-a-library.md) is the > source route. -## The whole idea in one paragraph +## Overview `mcpp pack ` builds a library target and writes a directory that is an **ordinary mcpp package** — a normal `mcpp.toml`, the interface a consumer must @@ -20,12 +20,12 @@ no new resolution path. ```bash mcpp pack mathkit # a static library package -mcpp pack mathkit-shared # a dynamic one (Linux/ELF today) +mcpp pack mathkit-shared # a dynamic one (ELF, Mach-O, PE/MinGW) mcpp pack mathkit --target x86_64-linux-gnu \ --target aarch64-linux-gnu # one package, two legs ``` -## What decides what gets packed +## What determines the package contents `[targets.].kind`, and nothing else: @@ -50,7 +50,7 @@ soname = "libmathkit.so.1" ``` Run `mcpp pack` with no name and mcpp picks the only packable target, or tells -you which ones it found. +the candidates it found. ## The two interface modes @@ -69,7 +69,7 @@ mathkit-0.1.0-x86_64-linux-gnu-gcc16-libstdcxx16-c++23/ | whose input is it | the preprocessor's | the **compiler's** | | does the consumer compile it | no | **yes**, to get a BMI | | what it constrains | the libc ABI | compiler, C++ stdlib, C++ level | -| can you trim it | **no** — see below | **no**, it is computed | +| trimmable | **no** — see below | **no**, it is computed | `lib/` is keyed by **triple**, not by OS. MinGW and MSVC are both Windows and produce `libfoo.a` and `foo.lib` respectively. @@ -79,7 +79,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 can be trimmed +### Why 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 @@ -88,7 +88,7 @@ it was delivered. "Which headers are public" is already answered by the layout: `include/` is public, `src/` is not. A private header under `include/` is a project-layout mistake, not a packaging option. -## Which `.cppm` files travel +## Interface units included in the package The **module closure of the lib root** — `src/.cppm` by convention, or `[lib].path`. Whatever that unit's purview imports, transitively, @@ -108,7 +108,7 @@ src/impl.cpp module mathkit; → withheld Withheld capi.c, impl.cpp, secret.cppm ``` -**Read the second one** if you are shipping closed source. +**The second row is the one that matters for closed-source distribution.** > **`.m.o` is not the rule.** An implementation partition produces a BMI and an > object exactly like an interface unit does. Selecting sources by "does it @@ -146,7 +146,7 @@ Nothing to configure: the shape is the statement. The `c++` level is compared as a **floor**, not for equality: building at a higher level is fine, lower is not. -## What a consumer's build checks +## Consumer-side build checks Two things, both of which fail silently without a check: @@ -177,15 +177,15 @@ error: acme.mathkit@0.1.0: no prebuilt artifact matches this toolchain. stdlib needs libstdcxx15, this build has libstdcxx16 ``` -The tags it *does* have are part of the message: "not found" would send you -looking for a package already on your disk. +The tags it *does* have are part of the message: "not found" would send the +reader looking for a package that is already on disk. -## Consuming one +## Consuming a package Three spellings, one code path: ```toml -# a directory (what you hand a colleague) +# a directory (copied directly to a colleague) mathkit = { path = "vendor/mathkit-0.1.0-x86_64-linux-gnu-gcc16-libstdcxx16-c++23" } # a private git repo @@ -197,7 +197,7 @@ mathkit = "0.1.0" Nothing about the consumer's manifest says "this one is prebuilt". -### Building *inside* a package is refused +### Building inside a package is refused ``` error: … is a distribution package produced by `mcpp pack`, not a source tree. @@ -221,7 +221,7 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"] ``` Because selection happens in the **consumer's** build, where the resolved -target is known, a fat package cross-compiles correctly with no index-side or +target is known, a multi-target package cross-compiles correctly with no index-side or installer-side support at all. > The blocks are `cfg(...)` and never a bare `[target.'']` key. Before @@ -242,10 +242,10 @@ records them and the consumer resolves them: `path` and `git` dependencies are dropped: they address the publisher's disk, and republishing one hands the consumer an address that means something else. -If your library depends on one, either publish that dependency too or vendor it +A library that depends on one must either publish that dependency too, or vendor it before packing. -## What older mcpp does with these packages +## Behaviour of older mcpp releases **It builds against them.** Every key in the generated manifest already existed, so an older client reads the package and links it. What it does not do @@ -253,10 +253,10 @@ is run the two checks above — it has no way to know that `provenance = "mcpp-pack …"` means anything. 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 your release notes if -you publish to a mixed audience. +**the gate protects new clients only**, which belongs in the release notes of any +package published to a mixed-version audience. -## Current limits +## Current limitations | | status | |---|---| @@ -327,7 +327,7 @@ floor. `mcpp pack` treats an implementation partition (`module M:part;`, no `export`) as private: its source stays behind, its object ships inside the archive. -If your published interface *imports* one, the consumer cannot build the BMI +If the published interface *imports* one, the consumer cannot build the BMI without that source, so it IS published — and `mcpp pack` says so: ``` @@ -340,14 +340,14 @@ warning: secret.cppm is an implementation partition, and the published interface > held no edge from the unit importing a partition to the unit defining it. > Build order was unconstrained: GCC and macOS clang recovered through their own > dependency scan, Windows clang failed with `failed to read compiled module`. -> If you have been avoiding implementation partitions on Windows, that was why. +> Implementation partitions were unusable on Windows for this reason. -### A partition mcpp cannot classify +### Partitions whose kind cannot be determined `[scan_overrides.""]` says which modules a file provides and has nowhere to say whether the declaration carries `export`; a P1689 scanner may omit `is-interface`. Either way the source is published — the consumer cannot build the -BMI without it — and `mcpp pack` says which of the two situations you are in: +BMI without it — and `mcpp pack` reports which of the two situations applies: ``` warning: secret.cppm provides a module PARTITION and mcpp cannot tell which kind: @@ -361,7 +361,7 @@ warning: secret.cppm provides a module PARTITION and mcpp cannot tell which kind > and names the module; publishing too many ships private source and nothing > fails at all. Unknown has to be loud. -## How much of this is verified, and where +## Verification scope The e2e suite gates each test on host capabilities, so "the suite is green" and "this ran" are different statements. What runs where: @@ -369,9 +369,9 @@ The e2e suite gates each test on host capabilities, so "the suite is green" and | claim | linux | macOS | windows | |---|---|---|---| | layout, both interface modes, closure, the two gates, workspace root, named target, `sources = []`, bare-triple predicate | ✅ | ✅ | ✅ | -| fat package, two legs one artifact name (`gnu` + `musl`) | ✅ | *impossible* | — | -| fat package, two legs **two** artifact names (`msvc` + `mingw`) | — | *impossible* | ✅ | -| fat package crossing an OS boundary (a PE leg) | ✅ | — | — | +| multi-target package, two legs one artifact name (`gnu` + `musl`) | ✅ | *impossible* | — | +| multi-target package, two legs **two** artifact names (`msvc` + `mingw`) | — | *impossible* | ✅ | +| multi-target package crossing an OS boundary (a PE leg) | ✅ | — | — | | `lib.exe /REMOVE:` really removing | — | — | ✅ | | PE shared library: build, pack, link, run | ✅ (wine) | — | — | | Mach-O shared library relocating out of its build tree | — | ✅ | — | diff --git a/docs/zh/02-pack-and-release.md b/docs/zh/02-pack-and-release.md index 66adde31..08700fb0 100644 --- a/docs/zh/02-pack-and-release.md +++ b/docs/zh/02-pack-and-release.md @@ -3,7 +3,7 @@ > 默认的动态链接 `mcpp build` 产物会把 loader 与 RUNPATH 指向构建沙盒。它是 > 开发产物,不是交付物。有三条路把它变成交付物 —— **没有一条使用宿主的 C 库**。 -## 三种分发方式 +## 三种分发路径 下面每一条产出的产物,其 C 运行时都来自生态,而不是 `/lib64`。这是有意的: mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决于底下是哪个发行版; @@ -15,12 +15,11 @@ mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决 | **B** | 静态单文件 | `mcpp build --target x86_64-linux-musl` | 不来自任何地方 —— 已链进去 | 想要一个无任何运行时依赖的单文件 | | **C** | 自带运行时 | `mcpp pack --mode self-contained` | 随 bundle 一起分发 | 任何 Linux,含比构建机更老的 | -**关于 A,以及那个让人意外的地方。** 刚构建出的二进制里烙的 `PT_INTERP` 指向 -**你这台机器**的载荷,所以手工把这个文件拷到另一台机器上跑不起来——那个路径 -在那边不存在。与其说这是产物的性质,不如说是「手工拷贝」这个动作的性质:经 -`xlings` 安装时,包里的 ELF 会在**装机期被重指到目标机自己的载荷**。烙进去的 -路径是构建机的细节,不是分发格式。如果你就是要在机器之间手工拷二进制,那你要 -的是 B 或 C。 +**关于 A。** 刚构建出的二进制中记录的 `PT_INTERP` 指向**构建机**的载荷,因此 +手工复制该文件到另一台机器无法运行 —— 该路径在目标机上不存在。这是「手工复制」 +这一动作的性质,而非产物的性质:经 `xlings` 安装时,包内的 ELF 会在**安装期被 +重指到目标机自己的载荷**。记录的路径属于构建机细节,不属于分发格式。需要在机器 +之间手工复制二进制时,应选择 B 或 C。 **关于 B。** `--target …-musl` 隐含静态链接,所以没有 loader、没有 RUNPATH、 运行期不需要找任何东西。它的结果最小也最可移植,在程序不需要 glibc 专有行为 @@ -52,6 +51,35 @@ mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决 - 需兼容老旧 CentOS、麒麟等 glibc 版本较低的环境 → `self-contained` - 单个便携文件、无宿主依赖 → `static` +### 需要宿主提供能力的程序 + +「自包含」有一个下限。有些库只能来自目标机器:图形驱动的用户态部分与正在运行的 +内核模块版本绑定,而对专有栈而言,再分发是不被允许的。这类依赖应声明为运行期能力 +需求(`docs/zh/05-mcpp-toml.md` §2.11),模式表随之多出一列: + +| Mode | 需要宿主提供能力的程序 | +|---|---| +| `system` | ✅ | +| `vendored`(默认) | ✅ **这类程序的正确默认值** | +| `self-contained` | ❌ **打包期拒绝** | +| `static` | ❌ **打包期拒绝** | + +两处拒绝出自同一个事实:**自带 libc 的 bundle 无法消费宿主提供的库。** 那个 `.so` +带着它对**目标机 libc** 的要求到达,而该进程没有那份 libc —— 双向实测记录于 +mcpp#392 / mcpp#401:私有 glibc 遇上宿主加载的对象,会在重定位阶段、`main` 之前 +崩溃。此前这两种模式都是链接通过、启动时失败,或静默降级(图形栈的表现是软件 +渲染,而没有任何提示)。 + +`vendored` 会打包这类程序,并在 bundle 根目录写出一个 **`HOST-REQUIREMENTS`** +文件,说明目标机必须提供什么: + +``` +capability=opengl.glx.driver discovery=rpath-of-dispatch +``` + +`discovery` 是可行动的那一半 —— 各机制彼此独立,满足其一并不等于满足另一个。 +该文件仅在确有内容时写出:一个空文件等于**声称**什么都不需要。 + ### 模式名兼容性 上表是规范名称。旧名称仍是**永久兼容别名**:`bundle-project` = `vendored`, @@ -146,10 +174,10 @@ exec "$here/lib/ld-linux-x86-64.so.2" --library-path "$here/lib" "$here/bin/myap 上面的布局与 wrapper 以 x86_64 为例。打包器会按 target 推导 loader 名称;aarch64 对应 `ld-linux-aarch64.so.1`。 -#### 陷阱:经 bundled loader 启动后的 `/proc/self/exe` +#### 经 bundled loader 启动时 `/proc/self/exe` 的取值 -「由 loader 启动」有一个上面的布局看不出来的后果:内核会把 `/proc/self/exe` -指向 **loader**,而不是你的程序;`/proc/self/cmdline` 里也混进了 +「由 loader 启动」有一个上述布局看不出的后果:内核会把 `/proc/self/exe` +指向 **loader** 而非目标程序;`/proc/self/cmdline` 中也混入了 `--library-path`。于是所有「在可执行文件旁边找资源」的逻辑都会解析到 `lib/` 而不是 bundle 根目录——而且是**静默**的。实际表现是:GUI 框架找不到字体因而 文字渲染空白、`assets/` 目录看起来不存在、随包分发的辅助二进制定位失败。 @@ -172,7 +200,7 @@ if (!base) { 它把 `PT_INTERP` 重指到宿主 loader,`/proc/self/exe` 正常,代价是要求宿主 glibc 不低于构建时所用的那份。 -### Windows(PE)—— 产物是 `.zip`,DLL 就放在 `.exe` 旁边 +### Windows(PE):产物为 `.zip`,DLL 与 `.exe` 同目录 Windows 目标产出的是 **`.zip`** 而不是 `.tar.gz`,并且是扁平布局: diff --git a/docs/zh/03-toolchains.md b/docs/zh/03-toolchains.md index 08cd9105..c9e3a79f 100644 --- a/docs/zh/03-toolchains.md +++ b/docs/zh/03-toolchains.md @@ -116,7 +116,7 @@ Available toolchains (run `mcpp toolchain install `): UCRT 运行时 DLL,MSVC STL 与 Windows SDK 都只随 Visual Studio 的 "Desktop development with C++" 负载安装。而 llvm 在 Windows 上打的是 MSVC ABI, 两者都需要,所以 mcpp 首跑时会探测机器上是否有可用的 MSVC(STL **与** SDK -两件齐——只有一半才是真正的坑),探不到就落到这里,并把选择持久化,之后的 +两者齐备 —— 只具备其一才是真正的风险),探测失败则落到这里,并把选择持久化,之后的 构建不再重复提示。无需任何安装或配置。 同一道检查也会修复既有配置:如果 mcpp 早先自己选定的 `[toolchain] default` @@ -164,7 +164,7 @@ windows = "gcc@16" # Windows 上的 gcc family = MinGW-w64 | `x86_64-windows-msvc` | `foo.lib`(MSVC 约定) | 2026.8.3.3 之前,Windows 宿主上的 mingw 构建产出的是 `foo.lib` —— 一个 GNU -archive 顶着 MSVC 的名字,MSVC 拿不去用。如果你有脚本按 `*.lib` 去捞 +archive 顶着 MSVC 的名字,MSVC 拿不去用。若有脚本按 `*.lib` 收集产物 `windows-gnu` 的产物,现在要改成 `*.a`。 ## Windows 上产出 Linux ELF(`x86_64-linux-musl`,无需 WSL) @@ -207,10 +207,10 @@ Targets 一栏里没有的,就是这台机器确实服务不了(实现见 一个 MSVC toolset 有两条路径进入构建,由 **spec 的版本轴**决定是哪一条: -| Spec | 来源 | 你拿到的是哪个编译器 | +| Spec | 来源 | 解析到的编译器 | |---|---|---| | `msvc@system`(或裸 `msvc`) | 这台机器自己的 Visual Studio | 这里恰好装了什么就是什么 | -| `msvc@`(如 `msvc@14.44.35207`) | mcpp 安装的 xlings payload | 你点名的那一个,在每台机器上都一样 | +| `msvc@`(如 `msvc@14.44.35207`) | mcpp 安装的 xlings payload | 指名的那一个,在每台机器上一致 | 它们不是"二选一",而是回答了不同的问题。`msvc@system` 问的是*"用这位开发者 已经有的东西"*;`msvc@14.44.35207` 问的是*"用恰好这个编译器构建本工程"*。 @@ -221,7 +221,7 @@ pinned toolset 之间、以及与系统 Visual Studio 之间都可以共存。 > 依赖降到最低** —— 工具链来自 manifest 点名的 payload,于是每台机器用同一个 > 编译器。Windows 是唯一一处"拒绝使用已装好的东西"代价大于收益的地方: > Visual Studio 常常已经装了,又不总能重新分发。其它族写 `@system` 会 -> 直接报错,并同时给出你可能想要的两种写法。(不带族的 +> 直接报错,并同时给出两种可能的写法。(不带族的 > `[toolchain] … = "system"` —— 即 PATH 上的编译器 —— 是另一套、也是有意保留的 > 逃生口,不受影响。) @@ -251,7 +251,7 @@ Detected msvc 19.44.35211 (VS 2022 BuildTools) (VC tools 14.44.35207) Default set to msvc@system (was: llvm@20.1.7) ``` -若机器上没有 Visual Studio,mcpp 会说出来并同时给出两条路:一个它可以替你装的 +若机器上没有 Visual Studio,mcpp 会报告该情况并给出两条路径:一个可由 mcpp 安装的 pinned toolset,或者 Visual Studio Installer / `winget install Microsoft.VisualStudio.2022.BuildTools`。 diff --git a/docs/zh/05-mcpp-toml.md b/docs/zh/05-mcpp-toml.md index d4af28be..fabdcf31 100644 --- a/docs/zh/05-mcpp-toml.md +++ b/docs/zh/05-mcpp-toml.md @@ -134,7 +134,7 @@ required_features = ["gui"] # 仅当 feature `gui` 激活时 **构建配置该放哪** —— 当多个二进制需要不同配置时: -| 你想要 | 用 | +| 目标 | 使用 | |---|---| | 某二进制**自己入口**上的不同宏/标志 | per-target `defines` / `cxxflags`(见上) | | 两个产品差异在它们**共享**的代码里 | 拆成 [workspace](06-workspace.md) member,各自 `[build]` 标志,共享一个 `lib` | @@ -257,7 +257,7 @@ GCC 用 `-x c++`,MSVC 用 `/interface /TP`),所以即使编译器驱动从没听 也能工作。这也是为什么任何扩展名都被允许:mcpp 不需要编译器认识它。 > **发布须知**:旧版 mcpp 不认识这个键 —— 它会警告、忽略,然后把那些文件当作普通 -> 翻译单元编译,得到一个**错误的构建**而不是一次干净的失败。如果你要发布一个用了 +> 翻译单元编译,得到一个**错误的构建**而不是一次干净的失败。发布一个用了 > `module_extensions` 的包,请在它的索引描述符里声明 mcpp 版本下限。 ### 构建程序超时(`build_program_timeout`) @@ -338,7 +338,7 @@ C++ 运行时的进程。 在 ELF 上显式写 `shared = "self-contained"` 是支持的,而且就是字面意思:库会内嵌 运行时。此时 mcpp 会额外发 `-Wl,--exclude-libs`(针对标准库归档),让内嵌的那份 -留在库的动态符号表之外,链接它的任何东西都捡不走。你自己代码产生的模板实例化 +留在库的动态符号表之外,链接它的任何东西都捡不走。本工程代码产生的模板实例化 (`std::string` 之类的 weak/COMDAT 符号)仍然会导出 —— 那是 C++ ABI 的预期行为, 不是这里要防的泄漏。 @@ -380,7 +380,7 @@ Windows 组件(Win10 起),mcpp 从不分发它;而 `vcruntime140.dll` / > 自 2026.8.16 起它真的生效,于是一份从那个年代带着 > `cxx_runtime = "self-contained"` 的 manifest **会在升级时换掉 CRT 模型**: > 从 `/MD` 变成 `/MT`。它不是同一个模型的更严格版本,而且因为这个值一直是合法的, -> 切换是**静默**的。如果你的工程是在这条键还不起作用时写下它的,请重新决定你真正要哪一个。 +> 切换是**静默**的。若工程是在这条键尚未生效时写下它的,应重新确认所需的取值。 把它和 `/MT` 一起用是**矛盾**而不是缺功能 —— 静态 CRT 根本没有 DLL 可以耦合 —— 所以会被报出来并落到 `self-contained`。另一半由 `mcpp pack` 兜底:什么都不打包的 @@ -397,7 +397,7 @@ Windows 组件(Win10 起),mcpp 从不分发它;而 `vcruntime140.dll` / > `ios_base::Init` 守卫 —— 于是从 `libc++.a` 里拉出来的流初始化器本来会排在 > 程序自己的全局构造函数**之后**:一个在构造函数里碰 `std::cout` 的全局对象会 > 读到尚未构造的流,进程启动即崩。mcpp 会把一个极小的生成对象排在链接最前面 -> 把流顶上去,你的代码不需要做任何事。详见 mcpp-community/mcpp#336。 +> 把流顶上去,调用方代码无需改动。详见 mcpp-community/mcpp#336。 `defines` 接受**裸**宏名(不带 `-D`),把每个条目脱糖为 `-D`,同时作用于 C 和 C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入**编译器自己的** @@ -565,7 +565,7 @@ feature(库若支持该旋钮,应在自己的 `[features]` 中声明 `backend-*` **Git 依赖与 `mcpp.lock`**:`tag` 和 `rev` 本身就指向历史中的固定点,而 `branch` 是会动的。首次构建把分支解析成一个 commit 并写进 `mcpp.lock`,此后每次构建都重建 **那个** commit —— lock 是权威而不是缓存提示,所以删掉 `~/.mcpp/git` 或换一台机器 -都不会悄悄把你挪到更新的分支头上。要新的分支头,得显式要: +都不会静默切换到更新的分支头。需要新的分支头时,必须显式指定: ```bash mcpp update mylib # 丢掉记录的 commit,下次构建重新解析 @@ -631,8 +631,8 @@ package: (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` 会替你改。 +写进 `mcpp.lock`、install 与 cache 的是**规范身份**;歧义拼写只存在于工程的 +`mcpp.toml` 中,直到被改写 —— `mcpp add gtest@1.15.2` 会完成这次改写。 过渡期**不适用于**写明 namespace 的 selector(`mcpplibs.gtest` 未命中就是未命中), 裸名也**仍然**到不了第三方 namespace。 @@ -662,6 +662,129 @@ package = { 省略。省略 `tname` 时选择唯一显式 default;若未写 `default = true` 且只有一个模板, 该单模板自动成为默认。多个未标默认的模板会报错,绝不按目录顺序选择。规范表见 `docs/spec/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.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`) | `[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+)。 +- **`build` 接受的恰好是*可叠加的构建输入*集合** —— 那些以追加方式合并、 + 并在谓词求值之后被消费的东西。`linkage`、`target` 与档案开关刻意不在其中: + 它们是**目标选择的输入**(用一个针对 `target` 求值的谓词去条件化 `target` + 是循环的),或者需要覆盖而非追加的语义。 +- **按解析后的目标求值** —— 交叉构建取 `--target` 三元组,否则取宿主。因此原生 + Linux 构建**根本不会下载** `[target.windows]` 依赖。 +- **优先级**:精确三元组表胜过 `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` 仅限精确三元组** —— 它们描述某一个具体的交叉目标, + 因此写在 `[target.]` 下(见上),而不是裸别名或 `cfg(...)` 下。 + +### 2.8 `[features]` —— Feature(Cargo 风格,可加性) + #### 表形式 —— 让 feature 贡献的不止是隐含 feature `[features]` 的条目除了写成数组,还可写成**表**,从而让该 feature 在隐含 feature @@ -701,35 +824,6 @@ simd = { sources = ["src/simd/**"], flags = [ feature `flags` 是**私有 per-TU 构建旗标**——永不传播给消费者(与 `[build].flags` 同契约),因此不破坏加性模型:glob 限定作用面、顺序确定、无跨包效应。 -#### 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.8.1 `provides` / `requires` —— 能力(后端选择) **capability(能力)** 是一个共享的抽象名字(如 `blas`)。包可以 *provide*(提供) @@ -804,6 +898,42 @@ features = { } ``` +### 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 @@ -1019,8 +1149,8 @@ grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] } - **永远是 host 二进制。** 即使 `mcpp build --target `,工具依然为**本机** 构建 —— 代码生成器必须在这里跑。它是一次独立的、面向 host 的子构建:工具包 - 自己的 `[toolchain]`、自己的依赖解析生效,不需要与你的构建一致。之所以安全, - 是因为可执行文件与你的代码**零 ABI 接触**。 + 自己的 `[toolchain]`、自己的依赖解析生效,不需要与当前构建一致。安全的原因是 + 可执行文件与工程代码**零 ABI 接触**。 - **单一版本轴。** 工具的版本**就是**依赖的版本,所以「protoc 与其运行时不匹配」 这种情况**不可表达**。(把工具单独打包正是会出这个问题,而且它在**运行期**才咬人, 不是编译期。) @@ -1030,7 +1160,7 @@ grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] } - **全局缓存**,按 包版本 × host 工具链 × feature × 自身依赖闭包 键控 —— 每台机器 构建一次,而不是每个工程一次。 -#### `[tools.overrides]` —— 用你已经有的二进制 +#### `[tools.overrides]` —— 使用已有的二进制 ```toml [tools.overrides] @@ -1069,13 +1199,13 @@ mcpp 会把该包的 lib 根模块**为 host 编译,且与 `build.mcpp` 在同 这正是 BMI 能用的前提:一个模块接口只对「在 standard / dialect / 编译器身份上与 它一致」的编译可导入。 -于是规则**有版本、能测试、能通过你已有的包管理器分发**,而且是用 **C++** 写的 +于是规则**有版本、能测试、能通过既有的包管理器分发**,而且是用 **C++** 写的 —— 不引入第二门语言,这正是 `build.mcpp` 存在的理由。 **模块名就是包的 `name`。** mcpp 用依赖的裸 `package.name`(而**不是** `.`)注册这个 host 模块,所以规则包的名字必须是合法的 C++ 模块名: `grpcgen` 可以,`grpc-rules` 不行 —— 连字符在包名里合法、在模块名里非法,而且报出来 -的是 `module 'grpc_rules' not found`,不会提示你名字有问题。 +的是 `module 'grpc_rules' not found`,不会指出名字有问题。 lib 根必须在 `src/.cppm`(或 `[lib] path` 指向的位置);缺失时报 *"host module 'x': no interface unit at …"*。 @@ -1083,7 +1213,7 @@ lib 根必须在 `src/.cppm`(或 `[lib] path` 指向的位置);缺失时 *限制:* 规则接口是单独编译的,因此可以 import `std` 与内置 `mcpp` 模块, 但不能 import 第三个包。规则包按构造是叶子。 -*仅构建期:* `host-module = true` 的依赖**不会**被编进、也不会被链进你的 target。 +*仅构建期:* `host-module = true` 的依赖**不会**被编进、也不会被链进本工程的 target。 它只在 `build.mcpp` 期间运行,别处都不出现 —— 与 Cargo 用 `[build-dependencies]` 划出的是同一条界线。(2026.8.5.2 之前它还会被当作普通库再编一遍,这正是规则里 `import mcpp;` 失败的原因:在那第二次编译里内置模块并不存在。) @@ -1119,7 +1249,7 @@ int main() { return grpcgen::generate_all() ? 0 : 1; } ``` - **默认关闭,并且刻意不复用边上的 `visibility`。** `visibility` 本身默认就是 - `"public"`,搭它的车意味着任意深度的依赖都能不声不响地往你的构建程序的工具 + `"public"`,复用该可见性意味着任意深度的依赖都能静默地向构建程序的工具 命名空间里塞东西。「把某样东西交给消费者」是一条供应链主张,必须写下来。 - **一次声明只走一跳。** 被再导出的提供物到达声明它的那个包的消费者;要继续 往上走,下一个包必须自己也写 `reexport`。每个包只决定**它**交出什么。 @@ -1175,19 +1305,19 @@ icon = "assets/app.ico" | 键 | 类型 | 含义 | |---|---|---| | `icon` | 路径 | 作为应用图标嵌入(资源序号 1) | -| `files` | 路径列表 | 你自己的 `.rc` 脚本,mcpp 编译并**跟踪**为构建输入 | +| `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)` 谓词 —— +不出诊断、构建逐字节不变。**无需**(也不能)加 `cfg(windows)` 谓词 —— 无条件写一次即可。 **声明了却不存在的文件会让构建失败 —— 在每个目标上都是。** 资源和源码一样是 构建输入;mcpp 不会悄悄产出一个缺了它的二进制。校验刻意**不**按 PE 设门: -路径存不存在是关于你工作树的事实,不是关于目标的事实,所以 `icon = "assets/app.ico"` -里的拼写错误由你的 Linux/macOS 构建(以及它们的 CI job)当场抓住,而不是等 +路径是否存在是关于工作树的事实,不是关于目标的事实,所以 `icon = "assets/app.ico"` +里的拼写错误由 Linux/macOS 构建(以及对应的 CI job)当场抓住,而不是等 Windows 那条。不想要图标,把那一行删掉。 **版本字段。** `FILEVERSION` 取 `[package].version` 的四段数值,每段必须放得进 @@ -1201,7 +1331,7 @@ Windows 那条。不想要图标,把那一行删掉。 files = ["res/app.rc"] ``` -写了 `files`,mcpp 就不再生成版本资源 —— 资源 ID 空间归你。想两者都要就同时写 +写了 `files`,mcpp 就不再生成版本资源 —— 资源 ID 空间由工程自行支配。两者都需要时同时写 `version-info = true`(注意冲突:序号 1 的 `RT_VERSION` 只能有一个)。 想从生成的脚本起步而不是从空文件起步:把它从构建目录里拷出来 @@ -1222,7 +1352,7 @@ mcpp 会读 `.rc`,把引号形式的 `#include` 和资源语句(`ICON`、`RCDATA (``)属于工具链,由工具链 fingerprint 覆盖。 通过宏间接引用的文件名(`1 ICON APP_ICON`)扫描看不见。mcpp 会**指名**它没能解析 -的东西,并要求你显式声明: +的东西,并要求显式声明: ```toml extra-inputs = ["assets/app.ico"] diff --git a/docs/zh/07-build-mcpp.md b/docs/zh/07-build-mcpp.md index 9656b510..21f60c04 100644 --- a/docs/zh/07-build-mcpp.md +++ b/docs/zh/07-build-mcpp.md @@ -2,11 +2,11 @@ [English](../07-build-mcpp.md) | **简体中文** -绝大多数工程只需要 `mcpp.toml`。当你需要构建期逻辑——探测主机、生成源码、依据环境 +绝大多数工程只需要 `mcpp.toml`。需要构建期逻辑时 —— 探测主机、生成源码、依据环境 决定某个编译开关——就在工程根目录放一个 `build.mcpp`。它是 mcpp 版的 Zig `build.zig` / Cargo `build.rs`,但用 **C++** 编写:不引入第二种语言,而且 mcpp 自己吃自己的狗粮。 -mcpp 用你的工具链编译 `build.mcpp`,并在主构建**之前**运行它。程序通过向 stdout 打印 +mcpp 用当前工具链编译 `build.mcpp`,并在主构建**之前**运行它。程序通过向 stdout 打印 `mcpp:` 指令与 mcpp 通信,这些指令会增补本次构建。 ## 快速示例 @@ -35,7 +35,7 @@ mcpp build # 编译 + 运行 build.mcpp,然后构建工程 ## 指令 -把这些打印到 stdout(每行一条)。任何不以 `mcpp:` 开头的行都会被忽略,因此你可以 +把这些打印到 stdout(每行一条)。任何不以 `mcpp:` 开头的行都会被忽略,因此可以 自由打印诊断日志。 | 指令 | 作用 | @@ -62,8 +62,8 @@ manifest/描述符里(`[build] include_dirs`),而不是构建期程序里。 ## 类型化 API:`import mcpp;`(推荐) -除了打印裸字符串,你还可以把 `build.mcpp` 写成**模块优先**——`import mcpp;`,不需要 -`#include`。`mcpp` 模块**内置在 mcpp 二进制里**(因此永远和你这版 mcpp +除打印裸字符串外,`build.mcpp` 也可以写成**模块优先**形式——`import mcpp;`,不需要 +`#include`。`mcpp` 模块**内置在 mcpp 二进制里**(因此始终与当前这版 mcpp 的协议匹配),按需编译;它的函数只是 emit 上面那些指令: ```cpp @@ -113,10 +113,10 @@ int main() { ``` mcpp 会**为构建机器**构建那个 `kind = "bin"` target(即使在 `--target` 下), -全局缓存,并把路径交给你。这个请求写在 `mcpp.toml` 而不是这里,理由和依赖本身 +全局缓存,并把路径交回。这个请求写在 `mcpp.toml` 而不是这里,理由和依赖本身 一样:向依赖图索取一个额外产物是**图级别**的请求,而图必须保持可静态分析。 完整契约(含 `[tools.overrides]` 与 `reexport = true` —— 库据此把整条工具链交给 -你,于是你只写**一条**依赖而不是四条)见 [05 §2.14](05-mcpp-toml.md)。 +调用方,因此只需写**一条**依赖而不是四条)见 [05 §2.14](05-mcpp-toml.md)。 ### 用通配符声明输入:`rerun_if_changed_glob`(2026.8.6.2+) @@ -144,7 +144,7 @@ int main() { 构建输出目录与 `.git` 永远不进入集合,因此再宽的模式也不会让程序对着自己的产物 无限重跑。 -### 声明工作而不是干活:`mcpp::action`(2026.8.5.1+) +### 声明产出而非执行动作:`mcpp::action`(2026.8.5.1+) 在**这里**直接把源码写出来是省事的路,超过一定规模就是错的:它每次 prepare 跑 一遍、全量、串行,失败还只报「build.mcpp exited 1」。**声明**这份工作,它就成为 @@ -207,7 +207,7 @@ int main() { a.output(gen.c_str()).provides("my.generated").imports("std").submit(); ``` -mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与你的生成器将要产出的 +mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与生成器将要产出的 内容一致 —— 与 `[modules].scan_overrides` 同一条「声明 + 验证」的取舍,build 期由 编译器自己的 P1689 输出复核。 @@ -229,12 +229,12 @@ mcpp 会播下一个带着该声明的占位文件,使 prepare 期的扫描与 | | `import mcpp;` | 手写 `printf("mcpp:…")` | |---|---|---| -| 兼容性 | 该模块**内置在 mcpp 二进制里**,由运行它的那个 mcpp 现场编译,程序与引擎不可能不一致 | 你的字符串是冻结的文本,没有任何东西替你校验 | +| 兼容性 | 该模块**内置在 mcpp 二进制里**,由运行它的那个 mcpp 现场编译,程序与引擎不可能不一致 | 字符串是冻结的文本,没有任何机制校验它 | | 新指令 | 以新函数的形式到来 | **不会再新增** | | 未知指令 | **硬错误** | 警告后忽略 | 用 `import mcpp;` 的程序会自动声明它编译时对应的协议版本(`mcpp:protocol=`, -在 `main` 之前发出——你不需要自己写)。mcpp 用它做两件事: +在 `main` 之前发出,无需自行编写)。mcpp 用它做两件事: - 程序声明的协议**高于** mcpp 所理解的 → **拒绝执行**,并给出升级提示。继续跑会 静默丢掉构建依赖的指令,而「构建成功了但那个 flag 根本没到」是最难查的一类问题。 @@ -342,11 +342,11 @@ mcpp **不会**每次构建都重跑 `build.mcpp`。它会缓存程序产出的 - (或该缓存是由一个对某条指令解释不同的 mcpp 写下的——条目带一个格式 **epoch**, 遇到不认识的 epoch 就重跑一次,而不是把值按错误的含义重放)。 -所以请**声明你的输入**:如果程序读了 `config.h` 或 `USE_FAST` 变量,就分别 emit +因此必须**声明输入**:如果程序读了 `config.h` 或 `USE_FAST` 变量,就分别 emit `mcpp:rerun-if-changed=config.h` / `mcpp:rerun-if-env-changed=USE_FAST`。这用一份明确的 输入/输出契约取代了过去「进程退出码为 0 就当成功」的猜测——让增量构建保持正确。 -无变化时你会看到 `build.mcpp up to date (cached)`;否则是 `build.mcpp compiling` / +无变化时输出 `build.mcpp up to date (cached)`;否则是 `build.mcpp compiling` / `running`。 ## 说明与限制 @@ -355,7 +355,7 @@ mcpp **不会**每次构建都重跑 `build.mcpp`。它会缓存程序产出的 下,程序用宿主解析的工具链编译、在宿主运行,并看到 `MCPP_TARGET` = 交叉三元组。 纯声明式的目标门控仍首选 `[target.'cfg(...)']` 表——参见 [05 - mcpp.toml 工程文件指南](05-mcpp-toml.md)。 -- **当前工作目录是工程根目录**,因此相对路径(`src/generated.cpp`)会落在你预期的位置。 +- **当前工作目录是工程根目录**,因此相对路径(`src/generated.cpp`)会落在预期位置。 - `build.mcpp` 非零退出会中止构建并打印其输出。 - **运行有时间上限**(mcpp 2026.8.5.1+):构建程序默认有 **600 秒**,超时后 mcpp 杀掉它并让构建失败,错误里会点名是哪个包。可按包配置: diff --git a/docs/zh/09-release.md b/docs/zh/09-release.md index 86af4477..05a7db9c 100644 --- a/docs/zh/09-release.md +++ b/docs/zh/09-release.md @@ -1,6 +1,6 @@ # 09 — 发布 mcpp -**mcpp 自身**的发布如何到达用户手上。这是给维护者看的;打包**你自己的项目**见 +**mcpp 自身**的发布如何到达用户手上。本章面向维护者;打包**自有项目**见 [02 — 打包发布](02-pack-and-release.md)。 在此之前这套流程只活在 commit message 和 workflow 注释里,其中一条 commit message @@ -151,7 +151,7 @@ $(find "$XLINGS_HOME" -name mcpp -type f -path '*/bin/*' | head -1) --version **唯一的硬约束是方向**:pin 绝不能指向一个尚不可安装的版本。只在发布已完成、 已镜像、**且已合入 xim-pkgindex 之后**再 bump —— 否则所有 CI 会以 `package 'mcpp@' not found` 失败。待其语法问题修复后, -`check_version_pins.sh` 能卡住较弱的「不得新于正在构建的版本」;索引那个条件靠你自己把关。 +`check_version_pins.sh` 能卡住较弱的「不得新于正在构建的版本」;索引那个条件需要人工把关。 ## 5. `MCPP_PIN` 改为推导,以及这为什么重要 diff --git a/docs/zh/10-publishing-a-library.md b/docs/zh/10-publishing-a-library.md index 11dcf59e..8a059172 100644 --- a/docs/zh/10-publishing-a-library.md +++ b/docs/zh/10-publishing-a-library.md @@ -6,10 +6,10 @@ [09 - 发布 mcpp](09-release.md) 讲的是发布 mcpp 自身, [02 - 发布打包](02-pack-and-release.md) 讲的是 `mcpp pack` 打包应用。 -## 这条链,只有一种顺序成立 +## 发布顺序 ``` -你的仓库 merge → git tag → GitHub 自动生成 tag tarball +库仓库 merge → git tag → GitHub 自动生成 tag tarball ↓ gitcode 镜像 逐字节相同的一份拷贝,供 CN 区 ↓ @@ -73,7 +73,7 @@ cmp cn.tar.gz pkg-0.0.48.tar.gz # 必须一致,而不只是"存在" 同样的字节;只写了其中一个,在另外两个平台上会以 "no such version" 失败, 而那读起来像是消费方 manifest 里打错了字。 -## 4. 等 artifact +## 4. 等待 artifact **索引是 artifact,不是 git clone。** 合进 `main` 还不够:必须等 `publish-artifact.yml` 跑完并推出内容哈希 artifact,客户端之上还有一层刷新 TTL。 @@ -84,7 +84,7 @@ gh run list --repo mcpplibs/mcpp-index --workflow publish-artifact.yml --limit 1 rm -rf ~/.mcpp/registry/data/ # 强制客户端刷新 ``` -## 5. 冷解析验证,然后再升消费方 +## 5. 冷解析验证与消费方升级 这一步的意义在于:本地那份库的 checkout 会掩盖上面每一个错误。要像一个陌生人那样解析它: @@ -101,7 +101,7 @@ mcpp build # 必须真的下载并编译 0.0.48 > payload 仓(约 800 MB 工具链)删掉。恢复办法是 `mcpp self doctor` 重新 provision, > 再 `mcpp update`。 -## 对着尚未发布的版本做测试 +## 针对尚未发布版本的测试 在上面这条链还没走完时,可以手工播种 registry,让消费方提前编译: @@ -120,6 +120,20 @@ mcpp 的构建沙箱是网络隔离的,`file://` 和 `http://127.0.0.1` 形式 **在相信"真的能用"之前,先把播种的那份删掉。** 播种的 0.0.48 和已发布的 0.0.48 对构建来说毫无区别 —— 而当发布其实失败了的时候,留在那里的正是播种的那一份。 +## 需要版本下限的 manifest 键 + +多数 `[build]` 键在较老的 mcpp 上会干净地降级:它警告该键不受支持、忽略它, +构建要么照常成功,要么以清晰的信息失败。`build_program_timeout` 属于这一类 —— +较老的 mcpp 回落到 600 秒默认值,若该值过短会明确报出。 + +**`module_extensions` 不属于这一类。** 较老的 mcpp 警告并忽略它,随后把那些文件 +当作普通翻译单元编译 —— 得到的是一个**错误的构建**而不是一次干净的失败: +模块接口不产生 BMI,故障在更晚的地方浮现,而报错既不点名这条键也不点名该文件。 + +使用 `module_extensions` 的已发布包必须在其索引描述符中声明 mcpp 版本下限。 +下限机制必须**降级**:因客户端过旧而不可用的包,必须被报告为**不可用**, +绝不能报告为**不存在** —— 被告知「无此包」的客户端会不断刷新索引去找它。 + ## 检查清单 - [ ] `mcpp.toml` 版本 == git tag diff --git a/docs/zh/11-machine-output.md b/docs/zh/11-machine-output.md index 2b72788a..b12ca2c2 100644 --- a/docs/zh/11-machine-output.md +++ b/docs/zh/11-machine-output.md @@ -1,16 +1,16 @@ # 11 — 机器可读输出 -mcpp 面向两类读者。本章是对第二类 —— **程序** —— 的契约。如果你在写编辑器扩展、CI -脚本,或任何解析 mcpp 输出的东西,这里写的是你可以依赖的部分。 +mcpp 面向两类读者。本章是对第二类 —— **程序** —— 的契约。编辑器扩展、CI 脚本,以及任何解析 mcpp 输出的程序, +可依赖的部分在此列出。 设计与背后的实测:`.agents/docs/2026-08-08-machine-readable-output-protocol-design.md`。 -## 1. 最重要的一条规则 +## 1. 首要规则 > **靠解析 stdout 来识别协议。不要靠退出码,也不要靠「命令没失败」。** 读 stdout,尝试按 JSON 解析,并要求 `schemaVersion` 与 `kind` 都在。缺任一,说明这个 -mcpp 不支持你要的东西。 +mcpp 不支持所请求的内容。 这不是风格偏好。`mcpp --protocol-version` 看起来像是入口,在支持它的版本上确实是个 捷径。但在**它出现之前发布的每一个 mcpp** 上,它自己就是一个未知选项 —— 而未知选项 @@ -89,7 +89,7 @@ $ echo $? | 70 | 内部错误(未捕获异常) | | 127 | 未知命令 | -## 4. effects —— 命令在输出之前做了什么 +## 4. effects —— 命令在输出前执行的操作 带 untrusted-workspace 门的 IDE 必须在**运行之前**决定。等信封到手,它描述的事情已经 发生了。所以同一份信息也静态提供: @@ -116,7 +116,7 @@ mcpp --protocol-version | effect | 含义 | |---|---| -| `init-mcpp-home` | 首次使用时可能创建 `$MCPP_HOME`。**在你的项目之外。** | +| `init-mcpp-home` | 首次使用时可能创建 `$MCPP_HOME`。**位于项目目录之外。** | | `read-project` | 读 manifest 与源码 | | `write-project` | 写入项目树(`target/`、compile DB) | | `write-global-cache` | 写共享构建缓存 | @@ -143,7 +143,7 @@ mcpp cache list --json -> {"root": …, "entries": [ … ]} 两种拼写由同一个来源产出,所以永远描述同一件事:一个答案,两种形状。 -## 6. 你可以依赖什么 +## 6. 稳定性承诺 对每个 `kind`,在同一 `kindVersion` 内: @@ -179,7 +179,7 @@ mcpp self env --format json 这条路径**刻意是只读的**。人类用的 `mcpp self env` 会在 `$MCPP_HOME` 缺失时初始化它 —— 在提示符下敲这条命令的人预期如此 —— 但一个客户端**询问东西在哪**,不该成为把东西 -放到那儿的原因。在从没跑过 mcpp 的机器上,你会拿到它**将会**使用的路径与 +放到那儿的原因。在从未运行过 mcpp 的机器上,得到的是它**将会**使用的路径与 `initialized: false`,而磁盘未被触碰。 这也是它存在的理由:没有它,客户端就得重新实现 mcpp 的 home 解析 —— 包括「PATH 上的 diff --git a/docs/zh/12-binary-distribution.md b/docs/zh/12-binary-distribution.md index 2454b578..33a454c6 100644 --- a/docs/zh/12-binary-distribution.md +++ b/docs/zh/12-binary-distribution.md @@ -3,12 +3,12 @@ [English](../12-binary-distribution.md) | **简体中文** > 把一个库以**接口 + 预编译二进制**的形式分发,而不是发源码。 -> 这是闭源场景、离线场景,以及「构建农场已经编过一遍了」的场景。 +> 适用于闭源分发、离线环境,以及构建产物已在构建农场生成的场景。 > -> 姊妹篇:[02 - 打包应用](02-pack-and-release.md) 讲的是打包**程序**; -> [10 - 发布一个库](10-publishing-a-library.md) 讲的是源码通路。 +> 相关文档:[02 - 打包应用](02-pack-and-release.md) 说明**程序**的打包; +> [10 - 发布一个库](10-publishing-a-library.md) 说明源码分发通路。 -## 一段话讲完 +## 概述 `mcpp pack ` 构建一个库目标,产出一个**普通的 mcpp 包** —— 一份正常的 `mcpp.toml`、消费者要编译的接口、以及它随后链接的二进制。 @@ -17,12 +17,12 @@ ```bash mcpp pack mathkit # 静态库包 -mcpp pack mathkit-shared # 动态库包(今天仅 Linux/ELF) +mcpp pack mathkit-shared # 动态库包(ELF、Mach-O、PE/MinGW) mcpp pack mathkit --target x86_64-linux-gnu \ - --target aarch64-linux-gnu # 一个包,两条腿 + --target aarch64-linux-gnu # 一个包,两个 target ``` -## 打什么由谁决定 +## 打包内容的决定依据 只由 `[targets.].kind` 决定: @@ -46,7 +46,7 @@ kind = "shared" soname = "libmathkit.so.1" ``` -不带名字直接 `mcpp pack`,mcpp 会挑唯一可打包的目标,或者告诉你有哪些候选。 +省略名字时,`mcpp pack` 选择唯一可打包的目标;存在多个候选时列出它们并要求指名。 ## 两种接口模式 @@ -70,14 +70,14 @@ mathkit-0.1.0-x86_64-linux-gnu-gcc16-libstdcxx16-c++23/ `lib/` 按**三元组**分目录,不按 OS 分:MinGW 与 MSVC 同为 Windows, 一个产 `libfoo.a` 一个产 `foo.lib`。 -### 为什么两者都不许裁剪 +### 两个集合不可裁剪的原因 同一个包的**源码**分发会把 `include_dirs` 里的每一个头都放到消费者的 include 路径上。二进制包若只发一部分,**同一个库就会因为分发形式不同而有不同的公开面**。 而且「哪些头是公开的」布局已经回答了:`include/` 公开,`src/` 不公开。 一个私有头放在 `include/` 下是工程布局的错误,不是打包选项。 -## 哪些 `.cppm` 会被发布 +## 发布的接口单元 **lib root 的模块闭包** —— 按约定是 `src/<包名尾段>.cppm`,或 `[lib].path`。 该单元 purview 里 import 到的东西,传递地,都发布;其余都不发。 @@ -130,7 +130,7 @@ x86_64-linux-gnu # 纯 extern "C" 接口 `c++` 档位按**下限**比对而不是相等:消费者档位更高可以,更低不行。 -## 消费者的构建会检查什么 +## 消费端的构建检查 两件事,而且都是不检查就会静默出错的: @@ -163,12 +163,12 @@ error: acme.mathkit@0.1.0: no prebuilt artifact matches this toolchain. 诊断里**列出它有哪些 tag** 是刻意的:一句「找不到」会让人去找一个 就在自己硬盘上的包。 -## 怎么消费 +## 消费方式 三种写法,同一条代码路径: ```toml -# 一个目录(你直接拷给同事的那种) +# 一个目录(可直接拷贝分发) mathkit = { path = "vendor/mathkit-0.1.0-x86_64-linux-gnu-gcc16-libstdcxx16-c++23" } # 私有 git 仓库 @@ -180,7 +180,7 @@ mathkit = "0.1.0" 消费者的 manifest 里没有任何一处写着「这个是预编译的」。 -### 在包目录里直接 build 会被拒绝 +### 包目录内的直接构建被拒绝 ``` error: … is a distribution package produced by `mcpp pack`, not a source tree. @@ -189,9 +189,9 @@ error: … is a distribution package produced by `mcpp pack`, not a source tree. 它的 `interface/` 里是声明,定义在旁边的归档里。在那儿构建会把声明编出来、 产出一个几乎空的库、然后报告成功。 -## 一个包,多个 target +## 单包多 target -`--target` 可重复。生成的 manifest 每条腿一个条件块,消费者的构建各选各的: +`--target` 可重复。生成的 manifest 每个 target 的产物一个条件块,消费者的构建各选各的: ```toml [target.'cfg(all(arch = "x86_64", os = "linux", env = "gnu"))'.build] @@ -202,7 +202,7 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"] ``` 因为选择发生在**消费者的构建期**(那时解析后的 target 已知), -胖包的交叉编译天然正确,**不需要索引侧或安装侧做任何支持**。 +多 target 包的交叉编译天然正确,**不需要索引侧或安装侧做任何支持**。 > 这些块是 `cfg(...)`,绝不是裸的 `[target.'<三元组>']` 键。 > 在 mcpp 2026.8.18.1 之前,裸三元组在没有显式 `--target` 时是失效的 —— @@ -219,17 +219,17 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"] ``` `path` 与 `git` 依赖会被丢弃:它们指向发布者的磁盘,原样发出去等于 -给消费者一个在他们那里含义完全不同的地址。如果你的库依赖这类东西, +会给消费者一个在其机器上含义完全不同的地址。若库依赖这类来源, 要么把它也发布出去,要么在打包前 vendor 掉。 -## 老版本 mcpp 拿到这种包会怎样 +## 旧版本 mcpp 的行为 **能构建。** 生成的 manifest 里每一个键都是既有的,所以老客户端读得懂、 链得上。它做不到的是执行上面那两道闸门 —— 它无从知道 `provenance = "mcpp-pack …"` 有什么含义。 这是**降级**而不是变砖,方向是对的。但它意味着**闸门只保护新客户端**, -如果你面向的是混合版本的用户群,这一条应当写进发布说明。 +面向混合版本用户群发布时,这一条应写进发布说明。 ## 当前边界 @@ -245,7 +245,7 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"] | 把依赖打包进去 | ❌ 改为声明依赖(见上) | | 用**原生 `cl.exe`** 消费这种包 | ❌ 见下 | -### MSVC 为什么拒绝 `kind = "shared"` +### MSVC 拒绝 `kind = "shared"` 的原因 **不是链接器的问题** —— `link /DLL /IMPLIB:` 本来就能用。是**符号导出**: MSVC 在没有 `__declspec(dllexport)`、也没有 `.def` 列出符号时,DLL **什么都不导出**, @@ -265,7 +265,7 @@ MinGW 的链接器会自动导出,这就是 `*-windows-gnu` 支持而 `*-windows ### 包里的链接 flag 是 GNU 拼写 -生成的 manifest 用这种方式选腿: +生成的 manifest 按下列形式选择产物: ```toml [target.'cfg(all(arch = "x86_64", os = "windows", env = "msvc"))'.build] @@ -300,13 +300,13 @@ warning: secret.cppm is an implementation partition, and the published interface > provides 空」** —— 一个文件 requires 自己的名字,于是图里**没有**从「import 分区 > 的单元」到「定义分区的单元」的边,构建顺序无约束:GCC 与 macOS clang 靠各自的 > 依赖扫描兜住了,**Windows clang 以 `failed to read compiled module` 失败**。 -> 如果你一直在 Windows 上回避实现分区,原因就是这个。 +> 此前在 Windows 上无法使用实现分区,原因即在于此。 -### mcpp 判不出类别的分区 +### 类别无法判定的分区 `[scan_overrides.""]` 说的是文件提供哪些模块,**没有地方能说**那条声明是否 带 `export`;P1689 扫描器也可能省略 `is-interface`。两种情况下源码都会被发布 —— -消费者没有它就编不出 BMI —— 而 `mcpp pack` 会告诉你你处在哪一种: +消费者没有它就编不出 BMI;`mcpp pack` 会指出当前属于哪一种情况: ``` warning: secret.cppm provides a module PARTITION and mcpp cannot tell which kind: @@ -318,7 +318,7 @@ warning: secret.cppm provides a module PARTITION and mcpp cannot tell which kind > 于是这样声明的实现分区被一声不响地发布了。**发布得太少**会让消费者编译失败并点名 > 模块;**发布得太多**会把私有源码发出去,而什么都不会失败。未知必须出声。 -## 这些说法验证到哪一步、在哪台机器上 +## 验证范围 e2e 套件按宿主能力给每条测试开门,所以「套件是绿的」和「这条跑了」是两句不同的话。 实际跑在哪里: @@ -326,9 +326,9 @@ e2e 套件按宿主能力给每条测试开门,所以「套件是绿的」和「 | 说法 | linux | macOS | windows | |---|---|---|---| | 布局、两种接口模式、闭包、两道闸门、workspace 根、指名 target、`sources = []`、裸三元组谓词 | ✅ | ✅ | ✅ | -| 胖包,两条腿**同一个**产物名(`gnu` + `musl`) | ✅ | *不可能* | — | -| 胖包,两条腿**两个**产物名(`msvc` + `mingw`) | — | *不可能* | ✅ | -| 跨 OS 边界的胖包(一条 PE 腿) | ✅ | — | — | +| 多 target 包,两个 target 的产物**同一个**产物名(`gnu` + `musl`) | ✅ | *不可能* | — | +| 多 target 包,两个 target 的产物**两个**产物名(`msvc` + `mingw`) | — | *不可能* | ✅ | +| 跨 OS 边界的多 target 包(含一个 PE target) | ✅ | — | — | | `lib.exe /REMOVE:` 真的删掉了 | — | — | ✅ | | PE 共享库:产出、打包、链接、运行 | ✅(wine) | — | — | | Mach-O 共享库离开构建树仍可加载 | — | ✅ | — | @@ -336,7 +336,7 @@ e2e 套件按宿主能力给每条测试开门,所以「套件是绿的」和「 | 已发布的 mcpp 消费本版产出的包 | 仅本机 | 仅本机 | 仅本机 | *不可能* 不是缺口:macOS 宿主只能服务一个 target(`host_can_serve`, -`registry.cppm`),那里根本产不出两条腿的包。 +`registry.cppm`),那里根本产不出两个 target 的产物的包。 最后一行如实记录一个真的洞:每个 CI job 都从一份已发布的 mcpp 自举,但那个入口是 xvm 的 **shim**,在 e2e 套件改过的环境里它回答「未安装」。所以老客户端检查的 diff --git a/docs/zh/README.md b/docs/zh/README.md index adabc090..8fd1580b 100644 --- a/docs/zh/README.md +++ b/docs/zh/README.md @@ -15,3 +15,11 @@ - [10 - 发布一个库到 mcpp-index](10-publishing-a-library.md) - [11 - 机器可读输出](11-machine-output.md) - [12 - 分发预编译库](12-binary-distribution.md) + +## 规范文档 + +规范性文档 —— 语义、约束与匹配规则,每条规则标注其实现状态。 +面向索引作者、贡献者与下游工具。 + +- [spec/](../spec/README.md) —— 全部规范的索引 + - [SPEC-001 —— 包身份、`[dependencies]` 选择器与匹配](../spec/package-identity.md)