diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml
new file mode 100644
index 000000000..af00b7d61
--- /dev/null
+++ b/.github/workflows/pypi-publish.yml
@@ -0,0 +1,200 @@
+name: pypi-publish
+
+# Publish the `mcpp-bin` wheels to PyPI (`pip install mcpp-bin`).
+#
+# Downstream of `release`, like aur-publish.yml and homebrew-publish.yml: the
+# wheels are built from the release's own mcpp-release.json and payloads, so
+# this runs only once the release workflow has completed.
+#
+# CREDENTIALS: none stored. Publishing uses PyPI Trusted Publishing (OIDC):
+# PyPI trusts this repository + workflow file + the `pypi` environment, and
+# the job exchanges its GitHub OIDC token for a short-lived upload token.
+# One-time setup is in scripts/pypi/README.md.
+#
+# ARMING: the automatic trigger builds, verifies and reports, and publishes
+# only when the repository variable PYPI_AUTOPUBLISH is `true`, for the reason
+# aur-publish.yml gives: an unattended push to a third-party service must be
+# armed by a human who has watched one publish succeed, not inherited from a
+# merge. `workflow_dispatch` carries its own explicit `publish` switch.
+
+on:
+ workflow_run:
+ workflows: [release]
+ types: [completed]
+ # Changes to the packaging itself build and pip-install the wheels of the
+ # latest release on every platform. A pull request never publishes.
+ pull_request:
+ paths:
+ - scripts/pypi/**
+ - tests/scripts/test_pypi_wheels.py
+ - .github/workflows/pypi-publish.yml
+ workflow_dispatch:
+ inputs:
+ publish:
+ description: 'Upload to PyPI (false builds and verifies only)'
+ type: boolean
+ required: true
+ default: false
+ tag:
+ description: 'Release tag, e.g. v2026.9.21.3 (default: the latest release)'
+ type: string
+ required: false
+
+concurrency:
+ group: pypi-mcpp-bin
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: build wheels
+ if: >-
+ github.event_name != 'workflow_run' ||
+ github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ outputs:
+ version: ${{ steps.resolve.outputs.version }}
+ publish: ${{ steps.resolve.outputs.publish }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.workflow_run.head_sha || github.ref }}
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Builder contract tests
+ run: python3 tests/scripts/test_pypi_wheels.py
+
+ - name: Resolve the release and whether to publish
+ id: resolve
+ env:
+ TRIGGER: ${{ github.event_name }}
+ INPUT_TAG: ${{ inputs.tag }}
+ MANUAL_PUBLISH: ${{ inputs.publish }}
+ AUTOPUBLISH: ${{ vars.PYPI_AUTOPUBLISH }}
+ run: |
+ set -euo pipefail
+ if [[ -n "${INPUT_TAG:-}" ]]; then
+ tag="$INPUT_TAG"
+ elif [[ "$TRIGGER" == "workflow_run" ]]; then
+ # The released commit's mcpp.toml carries the released version.
+ tag="v$(grep -m1 -E '^\s*version\s*=' mcpp.toml | sed -E 's/.*"([^"]+)".*/\1/')"
+ else
+ tag="$(gh release view -R "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
+ fi
+ version="${tag#v}"
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+
+ # PyPI never accepts the same file twice, so an existing version is
+ # a finished job rather than something to retry.
+ code=$(curl -s -o /dev/null -w '%{http_code}' --retry 3 --retry-all-errors \
+ "https://pypi.org/pypi/mcpp-bin/$version/json")
+ if [[ "$code" == "200" ]]; then
+ echo "::notice::mcpp-bin $version is already on PyPI; nothing to publish."
+ publish=false
+ elif [[ "$TRIGGER" == "workflow_run" ]]; then
+ if [[ "${AUTOPUBLISH:-}" == "true" ]]; then
+ publish=true
+ else
+ publish=false
+ echo "::notice::PYPI_AUTOPUBLISH is not set — building and verifying $tag without publishing."
+ fi
+ elif [[ "$TRIGGER" == "workflow_dispatch" ]]; then
+ publish="${MANUAL_PUBLISH:-false}"
+ else
+ publish=false
+ fi
+ echo "publish=$publish" >> "$GITHUB_OUTPUT"
+ echo "mcpp-bin $version from $tag; publish=$publish" >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Build wheels from the release manifest
+ run: python3 scripts/pypi/build_wheels.py --tag "${{ steps.resolve.outputs.tag }}" --out dist
+
+ - name: Check metadata
+ run: |
+ python3 -m pip install --quiet twine
+ python3 -m twine check --strict dist/*.whl
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: mcpp-bin-wheels
+ path: dist/*.whl
+ if-no-files-found: error
+
+ # pip, not this workflow, picks the wheel: each runner installs from the
+ # directory of all four, so a wrong platform tag fails here rather than on a
+ # user's machine. The run then checks the two properties the launcher exists
+ # for: the per-user home is outside the Python environment, and the bundled
+ # xlings is the one seeded into it.
+ smoke:
+ name: pip install (${{ matrix.os }})
+ needs: build
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-24.04, ubuntu-24.04-arm, macos-14, windows-latest]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 20
+ steps:
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - uses: actions/download-artifact@v4
+ with:
+ name: mcpp-bin-wheels
+ path: dist
+ - name: Install and run
+ shell: bash
+ env:
+ VERSION: ${{ needs.build.outputs.version }}
+ run: |
+ set -euo pipefail
+ python -m venv venv
+ if [[ -x venv/Scripts/python.exe ]]; then py=venv/Scripts/python.exe; bin=venv/Scripts; else py=venv/bin/python; bin=venv/bin; fi
+ "$py" -m pip install --quiet --no-index --find-links dist mcpp-bin
+ home="$RUNNER_TEMP/home"; mkdir -p "$home"
+ export HOME="$home" USERPROFILE="$home"
+ unset MCPP_HOME MCPP_VENDORED_XLINGS
+ out="$("$bin/mcpp" --version)"
+ echo "$out"
+ [[ "$out" == *"$VERSION"* ]] || { echo "::error::expected $VERSION, got: $out"; exit 1; }
+ "$bin/mcpp" self env | tee env.txt
+ grep -F "MCPP_HOME" env.txt | grep -F ".mcpp" \
+ || { echo "::error::MCPP_HOME is not the per-user home"; exit 1; }
+ if grep -F "MCPP_HOME" env.txt | grep -qF "site-packages"; then
+ echo "::error::MCPP_HOME resolved into the Python environment"; exit 1
+ fi
+ # On Windows mcpp runs the vendored xlings in place (src/config.cppm,
+ # make_xlings_env), so the seeded copy is checked on POSIX only.
+ if [[ "$RUNNER_OS" != "Windows" ]]; then
+ ls "$home/.mcpp/registry/bin/" | grep -q '^xlings' \
+ || { echo "::error::the bundled xlings was not seeded into the home"; exit 1; }
+ fi
+
+ publish:
+ name: publish to PyPI
+ needs: [build, smoke]
+ if: needs.build.outputs.publish == 'true'
+ runs-on: ubuntu-24.04
+ timeout-minutes: 15
+ environment:
+ name: pypi
+ url: https://pypi.org/project/mcpp-bin/${{ needs.build.outputs.version }}/
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: mcpp-bin-wheels
+ path: dist
+ - uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: dist/
diff --git a/README.md b/README.md
index f169b4915..7c517cad2 100644
--- a/README.md
+++ b/README.md
@@ -13,20 +13,12 @@
|:---:|
| [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) |
| [](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml) [](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml) [](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml) |
+| Plugins · [mcpp-language-server (mcppls)](https://github.com/Sunrisepeak/mcpp-language-server) — a C++20/23 modules language server for VS Code, Zed, CLion, Neovim, AI agents (MCP) and CI |
-> **Note (2026.9.20.1):** the `[c-abi]` verification probe now selects the
-> target it is verifying. On a freestanding target it selected none and
-> answered for the build host, which on a Linux host passed for the wrong
-> reason and on a Windows host failed for one. The `hostStripMacros`
-> compensation 2026.9.18.3 added is removed with it. This release also adds
-> `[kernel-abi] provides-interfaces` / `requires-interfaces`, answered at
-> dependency resolution, and `[c-abi-absent]`, which states what a C library
-> does not supply and in what shape. See CHANGELOG and docs/22.
-
## Highlights
- **Modular build system** — C++ modules first: `import std` handled automatically, file-level incremental builds, automatic dependency analysis, nothing to configure
@@ -174,7 +166,23 @@ remain manually maintained and may intentionally lag.
-Option 4 — let an AI assistant install it for you
+Option 4 — pip (PyPI)
+
+```bash
+pip install mcpp-bin
+```
+
+Installs the `mcpp` command into the active Python environment; `pipx install
+mcpp-bin` gives it an environment of its own. The wheels carry the same
+prebuilt release binary for Linux x86_64 / aarch64, macOS 14+ on Apple silicon
+and Windows x86_64. Per-user data still lives in `~/.mcpp/`, outside the Python
+environment. On PyPI the name `mcpp` belongs to an unrelated project, hence
+`mcpp-bin` (see [`scripts/pypi/`](scripts/pypi/)).
+
+
+
+
+Option 5 — let an AI assistant install it for you
Copy the following prompt to your AI coding assistant (Claude Code / Cursor / Copilot, etc.):
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 036185f0d..6f96a5303 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -1,6 +1,6 @@
# mcpp
-> 一个 现代C++ 模块化构建工具 — 纯 C++23 模块编写,已实现自举
+> 以模块为先的现代 C++ 构建工具。mcpp 完全由 C++23 模块写成,并已实现自举。
[English](README.md) | **简体中文**
@@ -13,6 +13,7 @@
|:---:|
| [包索引 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) |
| [](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml) [](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml) [](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml) |
+| 支持的插件 · [mcpp-language-server(mcppls)](https://github.com/Sunrisepeak/mcpp-language-server) —— C++20/23 模块语言服务器,面向 VS Code、Zed、CLion、Neovim、AI Agent(MCP)与 CI |
@@ -20,28 +21,27 @@
## 核心特性
-- **模块化构建系统** — 专注 C++ 模块:`import std` 自动处理,文件级增量构建,模块依赖自动分析,零手动配置
-- **构建插件与异构硬件编程** — `build.mcpp` 与规则包扩展构建;CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C 各是一个规则包
-- **包管理与模块化库生态** — SemVer 约束、锁文件、跨项目 BMI 缓存、自定义索引;[mcpplibs](https://github.com/mcpplibs) 的库两行引入即可 `import`
-- **工具链管理与通用交叉构建** — `family@version` 按需安装;`--target` 让同一次构建换到 Windows、macOS、Cortex-M 或 RISC-V 裸机,一份源码经 openkal 触及多个有操作系统的目标
-- **环境与运行时** — xlings 提供的用户态环境:工具链与依赖都留在隔离沙盒里,runner 把产物送上板子或模拟器
-- **纯模块化自举** — mcpp 完全由 C++23 模块接口单元写成,并用它自己构建自己
+- **模块化构建系统**:以 C++ 模块为先。`import std` 自动处理,文件级增量构建,模块依赖自动分析,无需配置
+- **构建插件与异构硬件**:`build.mcpp` 与规则包扩展构建;CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C 各对应一个规则包
+- **包管理与模块化库生态**:SemVer 约束、锁文件、跨项目 BMI 缓存、自定义索引;[mcpplibs](https://github.com/mcpplibs) 中的库在 `mcpp.toml` 中加两行即可 `import`
+- **工具链管理与交叉编译**:`family@version` 按需安装;`--target` 把同一次构建切换到 Windows、macOS、Cortex-M 或 RISC-V 裸机,一份源码经 openkal 可构建到多个带操作系统的目标
+- **环境与运行时**:由 xlings 提供用户态环境。工具链与依赖留在隔离的沙盒中,runner 把产物送到开发板或模拟器上运行
+- **纯模块化自举**:mcpp 完全由 C++23 模块接口单元写成,并由它自己构建
-## 为什么选择 mcpp
+## mcpp 的定位
-mcpp 专门为 **C++23 模块化开发** 打造。如果你想在项目中使用 `import std`、模块接口单元(`.cppm`)、模块分区等现代 C++ 特性,mcpp 在 Linux、macOS ARM64 和 Windows x86_64 上能为你提供便捷且友好的开发体验。
+mcpp 专为 **C++23 模块化开发**设计。需要使用 `import std`、模块接口单元(`.cppm`)、模块分区等现代 C++ 特性的项目,在 Linux、macOS ARM64 与 Windows x86_64 上都能得到顺畅、友好的开发体验。
-C++ 通常把这五件事分给五个工具,而 mcpp 用一条命令承担全部五件。第二行是每一列
-在既有认知里通常对应的东西。
+C++ 通常把下面五项工作交给五个工具,mcpp 用一个命令完成全部五项。第二行列出每一列通常对应的工具。
-| mcpp | 通用构建系统 | 构建插件 | 包管理 | 工具链管理 | 环境与运行时 |
+| mcpp | 构建系统 | 构建插件 | 包管理 | 工具链管理 | 环境与运行时 |
|---|---|---|---|---|---|
-| **最接近的** | CMake + Ninja | CMake modules、xmake rules | vcpkg、Conan | rustup、nvm | conda、Nix |
+| **最接近的工具** | CMake + Ninja | CMake modules、xmake rules | vcpkg、Conan | rustup、nvm | conda、Nix |
> [!NOTE]
-> **早期版本** — mcpp 仍在积极开发中,接口和行为可能在后续版本调整。
+> **早期版本**:mcpp 仍在积极开发中,接口与行为可能在后续版本中调整。
> 欢迎对现代 C++ 模块化构建工具感兴趣的开发者[参与贡献](#参与贡献)。
-> 问题 / 反馈 / 想法欢迎在 [issues](https://github.com/mcpp-community/mcpp/issues) 留言。
+> 问题、反馈与想法可以在 [issues](https://github.com/mcpp-community/mcpp/issues) 中提出。
## 快速开始
@@ -54,7 +54,7 @@ xlings install mcpp -y
```
-还没有 xlings?点击查看安装命令
+尚未安装 xlings:点击查看安装命令
**Linux / macOS**
```bash
@@ -66,20 +66,18 @@ curl -fsSL https://d2learn.org/xlings-install.sh | bash
irm https://d2learn.org/xlings-install.ps1.txt | iex
```
-> xlings 详情 → [xlings.d2learn.org](https://xlings.d2learn.org)
+> xlings 的更多信息见 [xlings.d2learn.org](https://xlings.d2learn.org)
-可选 —— 短命令(mp、mbuild、mrun …)
+可选:短命令(mp、mbuild、mrun 等)
```bash
xlings install mcpp-short-cmd -y
```
-装 30 个短命令,`mcpp build` 就是 `mbuild`。命名规则:除最后一个词外每词取首字母,
-最后一个词写全 —— `mcpp self doctor` → `msdoctor`;`mp` 就是裸 `mcpp`。
-它们指向 `mcpp` 这个 shim 而非固定路径,所以 `xlings use mcpp ` 也会一起切换。
+该包注册 30 个 shim,`mcpp build` 即可写成 `mbuild`。命名规则:除最后一个词外,每个词取首字母,最后一个词保留全拼,例如 `mcpp self doctor` → `msdoctor`。`mp` 即不带子命令的 `mcpp`。这些短命令指向 `mcpp` 这个 shim,而不是某个固定的二进制,因此 `xlings use mcpp ` 会同时切换它们。
| 短命令 | 展开 | 短命令 | 展开 |
| --- | --- | --- | --- |
@@ -101,44 +99,38 @@ xlings install mcpp-short-cmd -y
-**其他方式**
+**其他安装方式**
-方式 1 — 一键安装脚本(Linux x86_64/aarch64、macOS ARM64)
+方式 1:一键安装脚本(Linux x86_64/aarch64、macOS ARM64)
```bash
curl -fsSL https://github.com/mcpp-community/mcpp/releases/latest/download/install.sh | bash
```
-该脚本不支持 Windows;请使用上方 PowerShell 的 xlings 安装方式。它会安装到
-`~/.mcpp/`,并自动加入 shell PATH。删除 `~/.mcpp` 即可干净卸载。
+该脚本不支持 Windows,Windows 请使用上文 PowerShell 的 xlings 安装方式。脚本安装到 `~/.mcpp/`,并把它加入 shell 的 PATH。删除 `~/.mcpp` 即完成卸载。
-方式 2 — Homebrew(macOS / Linux)
+方式 2:Homebrew(macOS / Linux)
```bash
brew install mcpp-community/mcpp/mcpp-m
```
-一条命令即可,会自动 tap [`mcpp-community/homebrew-mcpp`](https://github.com/mcpp-community/homebrew-mcpp)
-并安装同一份预编译 release 二进制。macOS 需要 Apple 芯片 + macOS 14;
-每个用户的数据仍在各自的 `~/.mcpp/`。
+这一条命令会 tap [`mcpp-community/homebrew-mcpp`](https://github.com/mcpp-community/homebrew-mcpp),并安装同一份预编译的 release 二进制。macOS 要求 Apple 芯片与 macOS 14;每个用户的数据仍在各自的 `~/.mcpp/` 中。
-Homebrew 上 `mcpp` 属于一个无关的 C 预处理器,所以公式名是 `mcpp-m`,
-装出来的命令仍然是 `mcpp`。
+Homebrew 中的 `mcpp` 是一个无关的 C 预处理器,因此 formula 名为 `mcpp-m`,安装出的命令仍是 `mcpp`。
-**Homebrew 6 对第三方 tap 加了信任门。** 上面那条全限定命令会被当作显式意图、
-可以直接用;但**其它任何拼写**——短名 `brew install mcpp-m`、`mcpp` 别名、
-以及之后的升级——都会被拒:
+**Homebrew 6 对第三方 tap 设置了信任门。** 上面的全限定命令被视为显式意图,可以直接使用;其他写法,包括短名 `brew install mcpp-m`、`mcpp` 别名以及之后的升级,都会被拒绝:
```
Refusing to load formula mcpp-community/mcpp/mcpp-m from untrusted tap
mcpp-community/mcpp.
```
-信任这个 tap 一次,它们就都能用了:
+信任该 tap 一次之后,上述写法均可使用:
```bash
brew trust mcpp-community/mcpp
@@ -147,35 +139,42 @@ brew trust mcpp-community/mcpp
-方式 3 — Arch Linux(AUR)
+方式 3:Arch Linux(AUR)
```bash
yay -S mcpp-bin # 预编译 release 二进制
yay -S mcpp-m # 或源码构建(用 mcpp-bin 自举)
```
-系统级安装 `mcpp` 命令,每个用户的数据仍在各自的 `~/.mcpp/`。
-Arch 上 `mcpp` 这个名字属于一个无关的 C 预处理器,所以包名是
-`mcpp-bin` / `mcpp-m`(详见 [`scripts/aur/`](scripts/aur/))。
-稳定 release 的自动对账只管理 `mcpp-bin`;`mcpp-m` 与 `mcpp-git` 仍由人工维护,
-可能有意滞后。
+`mcpp` 命令安装到系统级位置,每个用户的数据仍在各自的 `~/.mcpp/` 中。Arch 上 `mcpp` 这个名字属于一个无关的 C 预处理器,因此包名为 `mcpp-bin` / `mcpp-m`(见 [`scripts/aur/`](scripts/aur/))。稳定版 release 的自动同步只管理 `mcpp-bin`;`mcpp-m` 与 `mcpp-git` 仍由人工维护,版本可能有意滞后。
-方式 4 — 让 AI 助手帮你安装
+方式 4:pip(PyPI)
-将以下提示词复制给你的 AI 编码助手(Claude Code / Cursor / Copilot 等):
+```bash
+pip install mcpp-bin
+```
+
+`mcpp` 命令安装到当前的 Python 环境中;使用 `pipx install mcpp-bin` 则为它单独创建一个环境。wheel 中是同一份预编译 release 二进制,支持 Linux x86_64 / aarch64、Apple 芯片上的 macOS 14+ 与 Windows x86_64。每个用户的数据仍在各自的 `~/.mcpp/` 中,不在 Python 环境内。PyPI 上 `mcpp` 这个名字属于一个无关的项目,因此包名为 `mcpp-bin`(见 [`scripts/pypi/`](scripts/pypi/))。
+
+
+
+
+方式 5:由 AI 助手安装
+
+将以下提示词发给 AI 编码助手(Claude Code、Cursor、Copilot 等):
```
阅读 https://github.com/mcpp-community/mcpp 的 README,
-帮我安装 mcpp 并创建一个 C++23 模块项目,构建并运行。
-项目的 .agents/skills/mcpp-usage/SKILL.md 有详细的使用指南。
+安装 mcpp,创建一个 C++23 模块项目,然后构建并运行它。
+仓库中的 .agents/skills/mcpp-usage/SKILL.md 提供了详细的使用指南。
```
-### 创建项目 & 构建运行
+### 创建、构建与运行项目
```bash
mcpp new hello
@@ -184,7 +183,7 @@ mcpp build
mcpp run
```
-> 注:首次构建会初始化环境并获取工具链,可能需要一些时间。
+> 首次构建会初始化环境并获取工具链,耗时较长。
### 项目结构
@@ -206,12 +205,11 @@ description = "A modular C++23 package"
license = "Apache-2.0"
```
-内置脚手架采用约定优于配置,不写 `[targets.hello]`:`src/main.cpp` 会推断出
-binary target,`mcpp test` 会自动发现 `tests/test_smoke.cpp`。
+内置脚手架依赖约定,不生成 `[targets.hello]`:mcpp 由 `src/main.cpp` 推断出 binary target,`mcpp test` 自动发现 `tests/test_smoke.cpp`。
### 使用模块化库
-在 `mcpp.toml` 中添加两行依赖,即可引用 [mcpplibs](https://github.com/mcpplibs) 社区模块化库:
+在 `mcpp.toml` 中添加两行依赖,即可引入 [mcpplibs](https://github.com/mcpplibs) 社区的模块化库:
```toml
[dependencies]
@@ -224,20 +222,20 @@ cmdline = "0.0.2"
import mcpplibs.cmdline;
```
-> 更多依赖配置方式(版本约束、命名空间、Git 引用、本地路径等)参见 [mcpp.toml 指南 — 依赖管理](docs/zh/04-mcpp-toml.md)。
+> 其他依赖写法(版本约束、命名空间、Git 引用、本地路径等)见 [mcpp.toml 指南 —— 依赖管理](docs/zh/04-mcpp-toml.md)。
## 功能概览
构建系统
-- C++20/23/26 模块原生支持(接口单元、实现单元、模块分区),另有 `c++latest` / `c++fly` 实验模式
+- 原生支持 C++20/23/26 模块(接口单元、实现单元、模块分区),另有 `c++latest` / `c++fly` 两种实验模式
- `import std` / `import std.compat` 全自动预编译与缓存
- 三层增量优化:前端脏检查 + 逐文件 P1689 dyndep + BMI copy-if-different restat
-- 指纹化 BMI 缓存:按编译器/标志/标准库哈希,跨项目共享
+- 带指纹的 BMI 缓存:按编译器、编译标志与标准库计算哈希,跨项目共享
- Ninja 后端:自动生成 build.ninja,并行编译
-- compile_commands.json 自动生成(clangd / ccls 即用);`mcpp build --configure-only` 可在编译普通源码之前先刷新它
-- C 语言一等支持:`.c` 文件自动检测,混合 C/C++ 项目
+- 自动生成 `compile_commands.json`(可直接供 clangd / ccls 使用);`mcpp build --configure-only` 可在编译普通源码之前刷新它
+- C 语言一等支持:自动识别 `.c` 文件,支持 C/C++ 混合项目
- 用户自定义 cflags / cxxflags / ldflags / c_standard
@@ -245,35 +243,35 @@ import mcpplibs.cmdline;
工具链管理
-- 内置 GCC 16.1.0 + LLVM/Clang 20.1.7,一键安装
-- 首次运行按宿主选择:Linux x86_64 使用原生 glibc GCC,其他 Linux 架构使用 musl GCC,macOS 与具备可用 MSVC 的 Windows 使用 LLVM,裸 Windows 使用 MinGW-w64 GCC
+- 内置 GCC 16.1.0 与 LLVM/Clang 20.1.7,一条命令安装
+- 按宿主选择默认值:Linux x86_64 使用原生 glibc GCC,其他 Linux 架构使用 musl GCC,macOS 以及有可用 MSVC 的 Windows 使用 LLVM,裸 Windows 使用 MinGW-w64 GCC
- 多版本共存:`mcpp toolchain install gcc 16` / `mcpp toolchain install llvm 20`
-- 隔离沙盒:所有工具链在 `~/.mcpp/registry/`,不影响系统
-- 按平台指定:`linux = "gcc@16"`, `macos = "llvm@20"`
-- GCC + Clang 编译管线平权(`BmiTraits` 抽象层驱动)
+- 隔离沙盒:所有工具链位于 `~/.mcpp/registry/`,不改动系统
+- 按平台指定:`linux = "gcc@16"`、`macos = "llvm@20"`
+- GCC 与 Clang 的编译管线对等(由 `BmiTraits` 抽象层驱动)
-交叉构建、裸机与设备
+交叉编译、裸机与设备
-- `mcpp build --target ` — 一个开关;该目标所需的工具链载荷会自动解析并安装
-- 从 `x86_64-linux-gnu` 到 Cortex-M、Cortex-A 与 RISC-V 裸机,完整的表见[平台支持](#平台支持)
-- freestanding 目标不带操作系统:C 库、启动代码、内存布局与模拟器随板级支持包走,而不随 mcpp 走
-- runner 用来触及构建机器上跑不了的产物 —— `mcpp run --runner flash`、`--list-runners`、`mcpp why runners`
-- `mcpp new --template riscv-virt-rt` — 由包自带的板级模板,按名字实例化
-- 基于 openkal 的交叉编译:一个可移植程序,为内核接口与 C 库都来自包的目标构建
+- `mcpp build --target `:一个选项;该目标所需的工具链 payload 自动解析并安装
+- 目标从 `x86_64-linux-gnu` 覆盖到 Cortex-M、Cortex-A 与 RISC-V 裸机,完整列表见[平台支持](#平台支持)
+- freestanding 目标不带操作系统:C 库、启动代码、内存布局与模拟器由板级支持包提供,而不由 mcpp 提供
+- runner 用于运行构建机上无法运行的产物:`mcpp run --runner flash`、`--list-runners`、`mcpp why runners`
+- `mcpp new --template riscv-virt-rt`:按名字实例化某个包自带的板级模板
+- 基于 openkal 的交叉编译:一个可移植程序,可以为内核接口与 C 库均由包提供的目标构建
-异构硬件构建与加速器
+异构构建与加速器
-- `[build] accel = "cuda12.9+{sm_89}, vulkan1.2"` — 一次构建可以点名一个或多个设备后端,该构建里 `cfg(accelerator = "cuda")` 为真
-- 目前有规则包的编程模型有五个:CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C
-- 设备翻译单元由它自己的编译器编译,产物进入普通链接;主机与设备之间的边界是生成的,不是写两遍的
-- 带约束的 glob 用来挑出设备源码:`{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`
-- 引擎里不含任何厂商名,因此第六个后端是一个包,而不是一次引擎改动
+- `[build] accel = "cuda12.9+{sm_89}, vulkan1.2"`:一次构建可以指定一个或多个设备后端,该构建中 `cfg(accelerator = "cuda")` 为真
+- 目前有规则包的编程模型共五个:CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C
+- 设备翻译单元由各自的编译器编译,产物进入普通链接;主机与设备之间的边界代码由 mcpp 生成,不需要写两遍
+- 用带约束的 glob 选择设备源码:`{ glob = "src/kernels/**/*.cu", accel = "cuda12.9+{sm_89}" }`
+- 引擎中不包含任何厂商名,因此第六个后端是一个包,而不是一次引擎改动
@@ -283,9 +281,9 @@ import mcpplibs.cmdline;
- SemVer 约束解析:`^`、`~`、范围、精确版本
- 三级解析:约束合并 → 多版本 mangling 回退 → 精确匹配
- 锁文件 mcpp.lock(v2 格式:索引快照 + 命名空间)
-- 命名空间系统:`[dependencies.myteam] foo = "1.0"`
+- 命名空间:`[dependencies.myteam] foo = "1.0"`
- 自定义包索引:`[indices] acme = "git@..."` / `{ path = "..." }`
-- 项目级索引隔离(`.mcpp/` 目录,不污染全局)
+- 项目级索引隔离(`.mcpp/` 目录,不影响全局状态)
- 依赖来源:索引 / Git / 本地路径
@@ -294,55 +292,53 @@ import mcpplibs.cmdline;
工作空间
- `[workspace] members = ["libs/*", "apps/*"]`
-- 统一锁文件 + 统一 target 目录
+- 统一的锁文件与 target 目录
- 版本集中管理:`[workspace.dependencies]` + `.workspace = true`
- 选择性构建:`mcpp build -p member-name`
-- 配置继承:工具链、构建标志、索引从根级联到成员
+- 配置继承:工具链、构建标志与索引从根级联到各成员
打包与发布
-- `mcpp pack`:四种 Linux 发布模式 — system / vendored(默认)/ self-contained / static;`bundle-project` 与 `bundle-all` 仍是兼容别名
-- musl 全静态二进制:单文件可分发,无 glibc 依赖(匹配的 Linux x86_64 或 aarch64 target)
-- `mcpp publish`:生成 xpkg.lua + 发布到包索引
-- 自动 patchelf 修正 RPATH(Linux)
+- `mcpp pack`:四种 Linux 发布模式,即 system / vendored(默认)/ self-contained / static;`bundle-project` 与 `bundle-all` 保留为兼容别名
+- musl 全静态二进制:单文件分发,不依赖 glibc(目标须为对应架构的 Linux x86_64 或 aarch64)
+- `mcpp publish`:生成 xpkg.lua 并发布到包索引
+- 通过 patchelf 自动修正 RPATH(Linux)
扩展构建
-- `build.mcpp` — 为 mcpp 没有现成规则的那一步写的构建程序,说的是一套带版本号的指令协议,而不是靠猜
-- `mcpp::action` 用显式的输入与输出声明一份工作,于是生成物参与增量图,而不是待在图外
-- 规则包把那一步带给别的项目:包声明一个 rule 模块,消费者以 feature 的形式选中它
-- 载荷、运行时适配包与板级支持包都是普通的包 —— 一个工具、一个驱动或一块板子,由安装库的那个解析器安装
+- `build.mcpp`:为 mcpp 没有现成规则的构建步骤编写的构建程序,它与 mcpp 通过一套带版本号的指令协议通信,而不是由 mcpp 猜测其行为
+- `mcpp::action` 以显式的输入与输出声明一项工作,生成的文件因此进入增量构建图,而不是游离在图外
+- 规则包把这类步骤提供给其他项目:包声明一个 rule 模块,使用方以 feature 的形式选用它
+- payload、运行时适配包与板级支持包都是普通的包:工具、驱动或开发板,都由安装库的同一个解析器安装
开发体验
-- `mcpp new` — 创建模块化项目;`--template [ns.]name[@version][:tname]` 与 `mcpp add` 使用同一精确身份风格并选择**包自带模板**。只有一个模板时即使未写 `default = true` 也自动成为默认;歧义时用 `--list-templates [ns.]name[@version]` 列举
-- `mcpp run [-- args]` — 构建并运行
-- `mcpp test [pattern] [-- args]` — 自动发现并运行测试(按名字过滤;`--list`、`--timeout `、`--message-format json`)
-- `mcpp search` — 搜索包索引
-- `mcpp add / remove / update` — 依赖管理
-- 命令行上的 profile 与 feature:`--release` / `--profile `(`build`、`run`),`--features `(`build`、`run`、`test`)
-- `mcpp why [toolchain|runtime|deps|runners]` — 解释已解析的构建决策;`--format json` 供程序读取
-- `mcpp emit sbom` — 为已记录的那次解析产出一份 CycloneDX 格式的 SBOM
-- `mcpp --offline` / `MCPP_OFFLINE=1` — 仅使用已存在的本地状态
-- `mcpp explain E0001` — 错误码详细解释
-- `mcpp self doctor` — 环境自诊断
+- `mcpp new`:创建模块化项目;`--template [ns.]name[@version][:tname]` 与 `mcpp add` 使用同一种精确身份写法,选用**包自带的模板**。包中只有一个模板时,即使未写 `default = true`,它也是默认模板;存在歧义时用 `--list-templates [ns.]name[@version]` 列出可选模板
+- `mcpp run [-- args]`:构建并运行
+- `mcpp test [pattern] [-- args]`:自动发现并运行测试(按名字过滤;`--list`、`--timeout `、`--message-format json`)
+- `mcpp search`:搜索包索引
+- `mcpp add / remove / update`:依赖管理
+- 命令行上的 profile 与 feature:`build` 与 `run` 接受 `--release` / `--profile `,`build`、`run` 与 `test` 接受 `--features `
+- `mcpp why [toolchain|runtime|deps|runners]`:解释已解析的构建决策;`--format json` 供程序读取
+- `mcpp emit sbom`:为刚记录的那次解析生成 CycloneDX 格式的物料清单(SBOM)
+- `mcpp --offline` / `MCPP_OFFLINE=1`:只使用本地已有的状态
+- `mcpp explain E0001`:错误码的详细解释
+- `mcpp self doctor`:环境自诊断
## 性能对比
-用**四个构建引擎**编译 **mcpp 自己** —— 锁定的工作负载有 137 个模块接口单元、
-57k 行,每一个都 `import std;` —— 并且**给它们同一个编译器二进制**。每格是 **3 轮的中位数**,以及
-相对 cmake 的倍率。所有列出自**同一次跑**。
+测量对象是构建 **mcpp 自身**:锁定的工作负载有 137 个模块接口单元、57k 行代码,每个单元都 `import std;`。四个构建引擎使用**同一个编译器二进制**。每格数据是 **3 次采样的中位数**,以及相对 cmake 的倍率。所有列来自**同一次运行**。
| 场景 | `mcpp` | `mcpp +优化` | `mcpp (旧版)` | `cmake` | `xmake` |
@@ -353,168 +349,137 @@ import mcpplibs.cmdline;
| `edit-body` | 80.87s · 1.1x | **29.83s · 2.9x** | 81.19s · 1.1x | 85.30s · 1.0x | 84.33s · 1.0x |
| `edit-comment` | **0.40s · 207.0x** | **0.40s · 207.0x** | 79.11s · 1.1x | 83.21s · 1.0x | 82.15s · 1.0x |
-`cold` 还没编过 · `noop` 什么都没改 · `touch-hub` 只碰 mtime,内容不变 · `edit-body` 真的改了一个函数体 · `edit-comment` 在 hub 接口里加一行注释。
+`cold` 尚未构建过 · `noop` 没有任何改动 · `touch-hub` 只更新 mtime,内容不变 · `edit-body` 在函数体内做一次真实修改 · `edit-comment` 在 hub 接口中添加一行注释。
-`mcpp` = mcpp@2026.8.13.1,被测的这一版 · `mcpp +优化` = **和 `mcpp` 同一个二进制**,开了 opt-in 的 `[build] bmi_schedule = "on"`(默认关闭) · `mcpp (旧版)` = mcpp@2026.8.11.3,上一个已发布版。
+`mcpp` = mcpp@2026.8.13.1,即被测版本 · `mcpp +优化` = **与 `mcpp` 同一个二进制**,开启 opt-in 的 `[build] bmi_schedule = "on"`(默认关闭)· `mcpp (旧版)` = mcpp@2026.8.11.3,即上一个已发布版本。
Linux x86_64 · i9-13900K · gcc 16.1.0 · n=3 · 锁定的工作负载 `a749e9f` ·
-cmake 4.4.2 / xmake 3.1.0 · 所有大于 1s 的中位数 min/max 都在 ±4% 以内 ·
-数据:[`standard-20260814-linux-x86_64`](bench/results/standard-20260814-linux-x86_64/)。
-
-* **`touch-hub` 与 `edit-comment` 两行由级联抑制决定。**
- cmake 与 xmake 按时间戳判断,重编全部下游单元;mcpp 将编译器刚产出的 BMI 与上
- 一份比较,接口未变则不触发级联。这是默认行为,无需任何配置。`mcpp (旧版)` 一列
- 测得上一个发布版为 81.72s,与 cmake 同量级,因此该效果在本版本中才生效。
-* **`edit-body` 是对照行,mcpp 在这一行有意不快。** 扰动往接口单元里插入一条语句,
- GCC 记录的声明位置随之移动,BMI 因而改变,每一个导入者都欠一次重建 —— 在这一行
- 跑得快的引擎,漏掉的是它欠下的工作。一次改动欠不欠级联取决于函数体写在哪里:
- 原地等长的修改,或者写在独立 `.cpp` 里的函数体,都不欠级联,落在约 200x 的那一档。
- 实测见
- [`.agents/docs/2026-08-15-module-edit-granularity.md`](.agents/docs/2026-08-15-module-edit-granularity.md)。
-* **`bmi_schedule` 为 opt-in,默认关闭**(`auto` 解析为 off)。它将代码生成移出关键
- 路径,因此只在级联确实欠着时才有收益 —— `cold` 86.69s → 35.73s、`edit-body`
- 80.87s → 29.83s,而在 mcpp 本已跳过级联的两行上没有收益。调度错误的表现是静默
- 失效而非报错,因此不以单台机器的证据变更默认值。
-
-**[方法、锁定的版本、完整数据 → `bench/README.zh-CN.md`](bench/README.zh-CN.md)** ·
+cmake 4.4.2 / xmake 3.1.0 · 所有大于 1s 的中位数,其 min/max 与中位数相差不超过 4% ·
+数据:[`standard-20260814-linux-x86_64`](bench/results/standard-20260814-linux-x86_64/)。
+
+* **`touch-hub` 与 `edit-comment` 两行的差异来自级联抑制。**
+ cmake 与 xmake 按时间戳判断,重新编译全部下游单元;mcpp 把编译器刚产出的 BMI 与上一份比较,接口未变时跳过级联。这是默认行为,不需要配置。`mcpp (旧版)` 一列测得上一个发布版为 81.72s,与 cmake 处于同一量级,因此这一效果是本版本新增的。
+* **`edit-body` 是对照行,mcpp 在这一行有意不快。**
+ 扰动在接口单元中插入一条语句,GCC 记录的源码位置随之移动,BMI 因此改变,每个导入者都需要重新构建。在这一行上快的引擎,是跳过了本应完成的工作。一次修改是否引起级联,取决于函数体写在哪里:原地等长的修改,或写在独立 `.cpp` 中的函数体,都不引起级联,结果落在约 200x 的那一档。实测见 [`.agents/docs/2026-08-15-module-edit-granularity.md`](.agents/docs/2026-08-15-module-edit-granularity.md)。
+* **`bmi_schedule` 为 opt-in,默认关闭**(`auto` 解析为 off)。
+ 它把代码生成移出关键路径,因此只在确实需要级联时才有收益:`cold` 86.69s → 35.73s,`edit-body` 80.87s → 29.83s;在 mcpp 本已跳过级联的两行上没有收益。调度出错时的表现是静默失效而不是报错,因此默认值不会依据单台机器的数据改变。
+
+**[方法、锁定的版本与完整数据 → `bench/README.zh-CN.md`](bench/README.zh-CN.md)** ·
[English](bench/README.md)
## 平台支持
-mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family ∈ gcc | llvm | msvc),
-**目标** = 三段 triple `arch-os[-env]`。交叉编译只需 `mcpp build --target `——
-对应的工具链包会自动解析并安装。`mcpp toolchain list` 查看本机实时状态。
+mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`(family ∈ gcc | llvm | msvc),**目标**是三段式 triple `arch-os[-env]`。交叉编译只需 `mcpp build --target `,对应的工具链 payload 会自动解析并安装。`mcpp toolchain list` 显示本机的实时状态。
-**宿主**(mcpp 本身运行在哪):Linux x86_64 / aarch64、macOS arm64、Windows x86_64。
+**宿主**(mcpp 本身运行的平台):Linux x86_64 / aarch64、macOS arm64、Windows x86_64。
-**目标**(`--target` 接受什么;表里的行与它们的档位取自
-`modules/toolchain-model/src/triple.cppm`,也就是 `mcpp toolchain list` 为本机
-报告的那一份):
+**目标**(`--target` 接受的值;表中的行及其档位取自 `modules/toolchain-model/src/triple.cppm`,`mcpp toolchain list` 为本机报告的也是这一份):
| Target | 约定工具链 | 档位 |
|---|---|:---:|
-| `x86_64-linux-gnu` | gcc(*Linux 默认*)或 llvm | verified |
-| `x86_64-linux-musl` | gcc 16,全静态 | verified |
-| `aarch64-linux-musl` | gcc 16,全静态——x86_64 交叉(qemu)或原生 | verified |
-| `x86_64-windows-gnu` | gcc 16 MinGW-w64——Windows 原生,Linux 交叉(wine)(*无 Visual Studio 时的 Windows 默认*) | verified |
-| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*有 Visual Studio 时的 Windows 默认*) | verified |
-| `x86_64-windows-musl` | llvm 22——带 musl C 库的 PE,没有 gcc 能产出它;系统由依赖图供给 | preview |
-| `aarch64-macos` | llvm(*macOS 默认*) | verified |
-| `riscv64-none-elf` · `riscv32-none-elf` | llvm 22——裸机,`xim:picolibc-riscv` ² | verified |
-| `thumbv6m-none-eabi` · `thumbv7m-none-eabi` | llvm 22——Cortex-M0/M0+/M1、Cortex-M3 ² | verified |
-| `thumbv7em-none-eabihf` · `thumbv8m.main-none-eabi` | llvm 22——Cortex-M4F/M7F 硬浮点、Cortex-M33/M55 软浮点 ² | verified |
-| `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 22——Cortex-A 32 位,第一条带 MMU 的行 ² | verified |
-| `aarch64-none-elf` · `x86_64-none-elf` | llvm 22——裸机,默认不带 C 库 ² | preview |
-| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22——Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview |
+| `x86_64-linux-gnu` | gcc(*Linux 默认*)或 llvm | verified |
+| `x86_64-linux-musl` | gcc 16,全静态 | verified |
+| `aarch64-linux-musl` | gcc 16,全静态;从 x86_64 交叉编译(qemu)或原生构建 | verified |
+| `x86_64-windows-gnu` | gcc 16 MinGW-w64;Windows 上原生构建,Linux 上交叉编译(wine)(*无 Visual Studio 时的 Windows 默认*) | verified |
+| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*有 Visual Studio 时的 Windows 默认*) | verified |
+| `x86_64-windows-musl` | llvm 22;带 musl C 库的 PE,gcc 无法产出;系统部分由依赖图提供 | preview |
+| `aarch64-macos` | llvm(*macOS 默认*) | verified |
+| `riscv64-none-elf` · `riscv32-none-elf` | llvm 22;裸机,`xim:picolibc-riscv` ² | verified |
+| `thumbv6m-none-eabi` · `thumbv7m-none-eabi` | llvm 22;Cortex-M0/M0+/M1、Cortex-M3 ² | verified |
+| `thumbv7em-none-eabihf` · `thumbv8m.main-none-eabi` | llvm 22;Cortex-M4F/M7F 硬浮点,Cortex-M33/M55 软浮点 ² | verified |
+| `armv7a-none-eabi` · `armv7a-none-eabihf` | llvm 22;Cortex-A 32 位,第一个带 MMU 的目标 ² | verified |
+| `aarch64-none-elf` · `x86_64-none-elf` | llvm 22;裸机,默认不带 C 库 ² | preview |
+| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview |
| `riscv64-linux-musl` · `aarch64-linux-gnu` · `x86_64-macos` | — | planned |
-| `wasm32-emscripten` | `emsdk@6.0.9` —— Emscripten 自带 sysroot 和它自己的 libc++ 模块面;`mcpp run` 用载荷声明的 `node`(`xim:node`)把模块跑起来,不取 PATH 上的 | verified |
-| `x86_64-linux-android` | `android-ndk@30.0.16248370` —— bionic 来自 NDK,一个载荷服务两个 ABI;在 API 24 的 x86_64 模拟器镜像上跑过 | verified |
-| `aarch64-linux-android` | 同一个载荷、同样的构建;在 qemu-user 上、配系统镜像自带的 bionic 跑过 —— 这是平台模拟器从 x86_64 宿主做不到的 | verified |
-| `aarch64-ios-sim` | llvm 22 加上机器自己的 iPhoneSimulator SDK,mcpp 定位而不安装它;经 `simctl-run` 在模拟器上跑过 ³ | verified |
-| `aarch64-ios` | 真机取同一种切分;产物命名 iOS 平台,而把它跑在一台设备上需要开发者自己拥有的签名 ³ | preview |
-| `x86_64-ios-sim` | 同一次构建;没有东西跑过它,因为模拟器跑宿主的架构,而被测的那台是 Apple 芯片 ³ | preview |
-
-`verified` 该行的镜像已被构建**并运行**过,qemu 与 wine 都算 · `preview` 可构建
-可链接,未记录过模拟器运行 · `planned` 已登记在词表中,尚未接线 —— 面向这类目标
-的构建会被拒绝,而不是被尝试。
-
-> Linux release 二进制为 x86_64 与 aarch64 的 musl 全静态构建
-> (`x86_64-linux-musl` 与 `aarch64-linux-musl`)。
-> 旧拼写——`x86_64-w64-mingw32`、`gcc@16.1.0-musl`、`mingw-cross@…`、`musl-gcc@…`——
-> 作为别名**永久接受**,归一到上表的 canonical 形式。
+| `wasm32-emscripten` | `emsdk@6.0.9`;Emscripten 自带 sysroot 与 libc++ 模块接口;`mcpp run` 使用 payload 声明的 `node`(`xim:node`)运行模块,不使用 PATH 上的 `node` | verified |
+| `x86_64-linux-android` | `android-ndk@30.0.16248370`;bionic 来自 NDK,一个 payload 服务两个 ABI;已在 API 24 的 x86_64 模拟器镜像上运行 | verified |
+| `aarch64-linux-android` | 同一个 payload、同样的构建;已在 qemu-user 上配合系统镜像自带的 bionic 运行,平台模拟器无法在 x86_64 宿主上做到这一点 | verified |
+| `aarch64-ios-sim` | llvm 22 加上本机的 iPhoneSimulator SDK(mcpp 定位该 SDK,不安装它);已通过 `simctl-run` 在模拟器上运行 ³ | verified |
+| `aarch64-ios` | 真机采用同样的分工;产物标记为 iOS 平台,在设备上运行需要开发者自己的签名 ³ | preview |
+| `x86_64-ios-sim` | 同一次构建;尚未运行过,因为模拟器运行宿主的架构,而测量所用的机器是 Apple 芯片 ³ | preview |
+
+`verified`:该行的镜像已构建**并运行**过,qemu 与 wine 均计入 · `preview`:可以构建和链接,尚无模拟器运行记录 · `planned`:已在词表中登记,尚未接通。针对这类目标的构建会被拒绝,而不会被尝试。
+
+> Linux release 二进制是 x86_64 与 aarch64 的 musl 全静态构建(`x86_64-linux-musl` 与 `aarch64-linux-musl`)。
+> 旧写法(`x86_64-w64-mingw32`、`gcc@16.1.0-musl`、`mingw-cross@…`、`musl-gcc@…`)作为别名**永久接受**,并归一化为上表中的规范形式。
>
-> ¹ Windows 上 llvm 打的是 MSVC ABI,因此依赖已安装的 **MSVC BuildTools 或
-> Visual Studio**(UCRT、Windows SDK、MSVC STL)。这件事你不需要自己安排:mcpp 首跑会
-> 探测是否有可用的 MSVC,探不到就默认走 `x86_64-windows-gnu`(winlibs MinGW-w64)——
-> 完全自包含、不需要 Visual Studio、`import std` 可用。无需安装或配置,裸 Windows 上
-> `mcpp new && mcpp build` 直接可用。而 `mcpp.toml` 里显式写的 `[toolchain]` 永远按你
-> 写的执行——mcpp 只修正自己选的默认值,不改你的。
+> ¹ Windows 上 llvm 使用 MSVC ABI,因此需要已安装的 **MSVC BuildTools 或 Visual Studio**(UCRT、Windows SDK、MSVC STL)。这一点无需手动处理:mcpp 首次运行时探测是否有可用的 MSVC,探测不到就默认使用 `x86_64-windows-gnu`(winlibs MinGW-w64)。该工具链完全自包含,不需要 Visual Studio,并支持 `import std`。在未做任何配置的 Windows 上,`mcpp new && mcpp build` 可直接使用。`mcpp.toml` 中显式写出的 `[toolchain]` 始终按原样生效:mcpp 只修正它自己选择的默认值,不改动用户的设置。
>
-> ² 裸机的那些行不带操作系统:clang 与 lld 天生就是交叉编译器,因此任何能安装
-> LLVM 载荷的宿主都能产出这些目标。C 库、启动代码、内存布局与模拟器随板级支持包
-> 走,而不随 mcpp 走 —— 见
-> [40 — 裸机与 freestanding 目标](docs/zh/40-baremetal.md)。
+> ² 裸机各行不带操作系统。clang 与 lld 本身就是交叉编译器,因此任何能安装 LLVM payload 的宿主都能产出这些目标。C 库、启动代码、内存布局与模拟器由板级支持包提供,而不由 mcpp 提供,见 [40 —— 裸机与 freestanding 目标](docs/zh/40-baremetal.md)。
>
-> ³ 三条 iOS 行需要一台 macOS 宿主,而编译器仍然是生态的:`xim:llvm` 能为一个 iOS
-> 部署目标产出 arm64 Mach-O。机器供给的是 SDK —— 它在 Xcode 里且不可再分发,所以
-> mcpp 通过 `xcrun` 定位它,与它一直以来定位 macOS SDK 的方式完全相同 —— 并在找不到
-> 时点名那个 SDK 拒绝。模拟器那次会话属于 `xim:apple-simulator-tools`。见
-> [20 —— 工具链管理](docs/zh/20-toolchains.md)。
+> ³ 三个 iOS 目标需要 macOS 宿主,编译器仍来自生态:`xim:llvm` 能为 iOS 部署目标产出 arm64 Mach-O。本机提供的是 SDK。SDK 位于 Xcode 中且不可再分发,因此 mcpp 通过 `xcrun` 定位它,方式与定位 macOS SDK 相同;找不到时,mcpp 报出该 SDK 的名字并拒绝构建。模拟器会话由 `xim:apple-simulator-tools` 提供。见 [20 —— 工具链管理](docs/zh/20-toolchains.md)。
## 文档
-[`docs/zh/`](docs/zh/README.md) 是手册。章节号的第一位说明它属于哪一部分;索引
-另有一份反向查表 —— 从读者眼前的一个 manifest 键或一条命令,查到拥有它的那一章。
+[`docs/zh/`](docs/zh/README.md) 是使用手册。章节号的第一位数字表示它所属的部分;索引另附一份反查表,可以从一个 manifest 键或一条命令查到负责它的章节。
-| 部分 | 从这里开始 |
+| 部分 | 入口章节 |
|---|---|
| `0x` 基础 | [01 快速开始](docs/zh/01-getting-started.md) · [04 mcpp.toml 工程文件指南](docs/zh/04-mcpp-toml.md) · [09 按场景选命令](docs/zh/09-commands-by-scenario.md) |
| `1x` 发布 | [10 发布打包](docs/zh/10-pack-and-release.md) · [11 发布一个库到 mcpp-index](docs/zh/11-publishing-a-library.md) · [12 分发预编译库](docs/zh/12-binary-distribution.md) |
| `2x` 工具链与目标 | [20 工具链管理](docs/zh/20-toolchains.md) · [21 目标三元组](docs/zh/21-the-target-triple.md) · [24 基于 openkal 的交叉构建](docs/zh/24-openkal-cross.md) |
-| `3x` 扩展 mcpp | [30 构建程序:`build.mcpp`](docs/zh/30-build-mcpp.md) · [31 编写规则包](docs/zh/31-authoring-a-rule-package.md) · [34 编写板级支持包](docs/zh/34-authoring-a-bsp.md) |
-| `4x` 设备与加速器 | [40 裸机与 freestanding 目标](docs/zh/40-baremetal.md) · [41 抵达一台设备](docs/zh/41-devices.md) · [42 异构硬件构建](docs/zh/42-heterogeneous-builds.md) |
-| `5x` 给程序的契约 | [50 机器可读输出](docs/zh/50-machine-output.md) · [51 受支持的版本与兼容性](docs/zh/51-supported-versions.md) · [规范](docs/specs/README.md) |
+| `3x` 扩展 mcpp | [30 构建程序:`build.mcpp`](docs/zh/30-build-mcpp.md) · [31 编写规则包](docs/zh/31-authoring-a-rule-package.md) · [34 编写板级支持包](docs/zh/34-authoring-a-bsp.md) |
+| `4x` 设备与加速器 | [40 裸机与 freestanding 目标](docs/zh/40-baremetal.md) · [41 在设备上运行](docs/zh/41-devices.md) · [42 异构硬件构建](docs/zh/42-heterogeneous-builds.md) |
+| `5x` 面向程序的契约 | [50 机器可读输出](docs/zh/50-machine-output.md) · [51 受支持的版本与兼容性](docs/zh/51-supported-versions.md) · [规范](docs/specs/README.md) |
| `9x` mcpp 自身 | [90 从源码构建与参与贡献](docs/zh/90-build-from-source.md) · [92 发布 mcpp](docs/zh/92-release.md) |
-[`examples/`](examples/) 下的每一个目录都是一个能构建的工程,
-[03 — 示例项目](docs/zh/03-examples.md) 说明哪一个教什么。任意命令的完整选项
-可通过 `mcpp --help` 查阅。
+[`examples/`](examples/) 下的每个目录都是一个可以构建的项目,[03 —— 示例项目](docs/zh/03-examples.md) 说明每个示例演示什么。任意命令的完整选项可通过 `mcpp --help` 查看。
-**AI 辅助学习**:你可以将以下提示词发给 AI 编码助手,让它帮你快速了解 mcpp:
+**AI 辅助学习**:将以下提示词发给 AI 编码助手,可以快速了解 mcpp:
```
阅读 https://github.com/mcpp-community/mcpp 仓库的
.agents/skills/mcpp-usage/SKILL.md 和 docs/ 目录下的文档,
-告诉我如何用 mcpp 创建一个带依赖的 C++23 模块项目。
+说明如何用 mcpp 创建一个带依赖的 C++23 模块项目。
```
-## 谁在使用 mcpp
+## 使用 mcpp 的项目
-用 mcpp 构建的真实项目 —— 可直接 `import` 的 C++23 模块,以及它所依赖的工具链:
+以下是用 mcpp 构建的实际项目,包括可直接 `import` 的 C++23 模块,以及 mcpp 所依赖的工具链底座:
| 项目 | 说明 |
| --- | --- |
-| [mcpp](https://github.com/mcpp-community/mcpp) | mcpp 自身 —— 由 C++23 模块写成,完全自举 |
-| [xlings](https://github.com/openxlings/xlings) | mcpp 依赖的工具链与包管理底座 |
-| [tinyhttps](https://github.com/mcpplibs/tinyhttps) | 极简 C++23 HTTP/HTTPS 客户端,支持 SSE 流式 |
-| [llmapi](https://github.com/mcpplibs/llmapi) | 现代 C++ LLM API 客户端(OpenAI 兼容) |
-| [imgui-m](https://github.com/mcpplibs/imgui-m) | Dear ImGui 的 C++23 模块封装包 |
-| [cmdline](https://github.com/mcpplibs/cmdline) | 命令行解析库/框架(mcpp 自身在用) |
+| [mcpp](https://github.com/mcpp-community/mcpp) | mcpp 自身,由 C++23 模块写成,完全自举 |
+| [xlings](https://github.com/openxlings/xlings) | mcpp 所依赖的工具链与包管理底座 |
+| [tinyhttps](https://github.com/mcpplibs/tinyhttps) | 极简的 C++23 HTTP/HTTPS 客户端,支持 SSE 流式传输 |
+| [llmapi](https://github.com/mcpplibs/llmapi) | 现代 C++ LLM API 客户端(兼容 OpenAI) |
+| [imgui-m](https://github.com/mcpplibs/imgui-m) | 以 C++23 模块包形式提供的 Dear ImGui |
+| [cmdline](https://github.com/mcpplibs/cmdline) | 命令行解析库 / 框架(mcpp 自身使用) |
-更多模块化库 → [mcpplibs](https://github.com/mcpplibs) · 包索引 → [mcpp-index](https://mcpplibs.github.io/mcpp-index/)
+更多模块化库见 [mcpplibs](https://github.com/mcpplibs) · 包索引见 [mcpp-index](https://mcpplibs.github.io/mcpp-index/)
## 参与贡献
-欢迎通过 Issue 和 PR 参与项目开发。项目接受开发者使用 AI Agent 参与开发与贡献。
+欢迎通过 Issue 与 PR 参与开发。项目接受借助 AI Agent 完成的贡献。
**基本流程**
-1. 创建 Issue — Bug 修复、新功能、优化等,先在 [issues](https://github.com/mcpp-community/mcpp/issues) 创建讨论
-2. 实现改动 — Fork 仓库,创建分支,并按改动范围验证(行为改动运行 `mcpp build` 与相关测试;纯文档改动复核示例和链接)
-3. 提交 PR — 使用 `gh pr create`,确保 CI 通过
-4. CI 必须通过 — CI 不通过的 PR 不会被合入
+1. 创建 Issue:Bug 修复、新功能或改进,先在 [issues](https://github.com/mcpp-community/mcpp/issues) 中发起讨论
+2. 实现改动:Fork 仓库并创建分支,按改动范围验证(行为改动运行 `mcpp build` 与相关测试;纯文档改动核对示例与链接)
+3. 提交 PR:使用 `gh pr create`,并确保 CI 通过
+4. CI 必须通过:CI 未通过的 PR 不会被合入
-**提交信息规范**:`feat:` / `fix:` / `test:` / `docs:` / `refactor:` 前缀
+**提交信息规范**:使用 `feat:` / `fix:` / `test:` / `docs:` / `refactor:` 前缀
-**AI Agent 贡献**:项目的 [`.agents/skills/mcpp-contributing/SKILL.md`](.agents/skills/mcpp-contributing/SKILL.md) 提供了完整的 Agent 贡献流程和项目结构说明。将以下提示词发给 AI 助手即可:
+**AI Agent 贡献**:仓库中的 [`.agents/skills/mcpp-contributing/SKILL.md`](.agents/skills/mcpp-contributing/SKILL.md) 给出完整的 Agent 贡献流程与项目结构说明。将以下提示词发给 AI 助手即可:
```
阅读 https://github.com/mcpp-community/mcpp 仓库的
.agents/skills/mcpp-contributing/SKILL.md,
-按照指南帮我给 mcpp 项目提交一个贡献。
+按照其中的指南为 mcpp 项目提交一个贡献。
```
## 社区 & 生态
-- [社区论坛](https://forum.d2learn.org/category/20) — 交流群 (Q: 1067245099)
-- [mcpp-index](https://mcpplibs.github.io/mcpp-index/) — 默认包索引
-- [mcpplibs](https://github.com/mcpplibs) — 模块化 C++ 库集合
+- [社区论坛](https://forum.d2learn.org/category/20):交流群(QQ:1067245099)
+- [mcpp-index](https://mcpplibs.github.io/mcpp-index/):默认包索引
+- [mcpplibs](https://github.com/mcpplibs):模块化 C++ 库集合
### 致谢
-项目依赖和灵感来源:
+项目依赖与灵感来源:
-- [xlings](https://github.com/openxlings/xlings) — 工具链 / 包管理底座
-- [mcpplibs.cmdline](https://github.com/mcpplibs/cmdline) — CLI 框架
-- [ninja](https://github.com/ninja-build/ninja) — 底层构建引擎
-- [xmake](https://github.com/xmake-io/xmake) — 跨平台构建工具
-- [cargo](https://github.com/rust-lang/cargo) — Rust 包管理器
+- [xlings](https://github.com/openxlings/xlings):工具链 / 包管理底座
+- [mcpplibs.cmdline](https://github.com/mcpplibs/cmdline):CLI 框架
+- [ninja](https://github.com/ninja-build/ninja):底层构建引擎
+- [xmake](https://github.com/xmake-io/xmake):跨平台构建工具
+- [cargo](https://github.com/rust-lang/cargo):Rust 包管理器
diff --git a/docs/92-release.md b/docs/92-release.md
index 22db5ba95..84bd3d144 100644
--- a/docs/92-release.md
+++ b/docs/92-release.md
@@ -89,7 +89,7 @@ The workflow then downloads the public release again, regenerates the manifest,
and requires a byte-for-byte match. A workflow rerun accepts an existing
manifest only when it is already byte-identical; it never overwrites different
bytes for the same tag. Downstream release consumers (notably the `mcpp-bin`
-AUR reconciler) must consume this manifest instead of guessing completeness
+AUR reconciler and the `mcpp-bin` PyPI wheel builder in `scripts/pypi/`) must consume this manifest instead of guessing completeness
from a moving workspace or from a subset of release assets.
Two steps are **not** automated:
diff --git a/docs/zh/00-what-mcpp-is.md b/docs/zh/00-what-mcpp-is.md
index a572265b2..e6618a506 100644
--- a/docs/zh/00-what-mcpp-is.md
+++ b/docs/zh/00-what-mcpp-is.md
@@ -1,99 +1,99 @@
# 00 —— mcpp 是什么
-## 背景:C++ 工程侧的工具现状
+## 背景:C++ 工程侧的工具现状
-一个 C++ 工程需要四样东西同时成立:一份构建描述、一组依赖、一个新到足以编译这份
-代码的编译器,以及一个能让产物真正运行起来的环境。C++ 没有任何一个工具同时负责
-这四样,它们由四类互不隶属的工具分别承担,而把它们对齐是工程自己的工作。
+一个 C++ 工程需要四样东西同时成立:一份构建描述、一组依赖、一个新到足以编译这份
+代码的编译器,以及一个能让产物真正运行起来的环境。C++ 没有任何一个工具同时负责
+这四样,它们由四类互不隶属的工具分别承担,而把它们对齐是工程自己的工作。
-CMake 是其中第一样的事实标准。事实标准陈述的是采用率,不是使用体验:CMake 不解析
-依赖、不安装编译器、也不描述运行期环境,而这三件恰好是新参与者在第一天遇到的。
-补齐它们意味着再引入一个包管理器、发行版的软件包,以及 README 里的一段环境说明
-—— 三套新的模型,而它们之间的对齐仍然落在工程身上。
+CMake 是其中第一样的事实标准。事实标准陈述的是采用率,不是使用体验:CMake 不解析
+依赖、不安装编译器、也不描述运行期环境,而这三件恰好是新参与者在第一天遇到的。
+补齐它们意味着再引入一个包管理器、发行版的软件包,以及 README 里的一段环境说明
+—— 三套新的模型,而它们之间的对齐仍然落在工程身上。
-最经常失效的是环境这一层,因为只有它没有任何东西在检查。构建描述会在配置阶段
-报错,包管理器会解析失败,而「机器上的库不是这份代码构建时所依据的那一份」在
-链接之前不产生任何信号,有时到程序运行时才产生。
+最经常失效的是环境这一层,因为只有它没有任何东西在检查。构建描述会在配置阶段
+报错,包管理器会解析失败,而「机器上的库不是这份代码构建时所依据的那一份」在
+链接之前不产生任何信号,有时到程序运行时才产生。
-模块给这个结构再加一条约束。C++20 的模块改变的是一个翻译单元的代价:接口只声明
-一次并被 import,而不是被每个需要它的文件从头文件里重新解析一遍;消费者看到的是
-作者导出的东西,不是头文件恰好 include 进来的一切。代价是这个语言特性由构建系统
-实现:构建要扫描源里的 `import`、据此给编译定序、缓存编译好的接口并正确地让它
-失效,而且要在一个新到具备这个特性的编译器上完成这些。`import std` 再加一条:
-标准库自己的模块必须先被构建出来,别的东西才能用它。
+模块给这个结构再加一条约束。C++20 的模块改变的是一个翻译单元的代价:接口只声明
+一次并被 import,而不是被每个需要它的文件从头文件里重新解析一遍;消费者看到的是
+作者导出的东西,不是头文件恰好 include 进来的一切。代价是这个语言特性由构建系统
+实现:构建要扫描源里的 `import`、据此给编译定序、缓存编译好的接口并正确地让它
+失效,而且要在一个新到具备这个特性的编译器上完成这些。`import std` 再加一条:
+标准库自己的模块必须先被构建出来,别的东西才能用它。
-于是采用模块的工程需要上述四层全部成立,而不是其中三层。这个落差是具体的。在写
-这一章的这台机器上:
+于是采用模块的工程需要上述四层全部成立,而不是其中三层。这个落差是具体的。在写
+这一章的这台机器上:
```console
$ g++ --version
g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
```
-这个编译器编不了 `import std`。工程本身没有任何问题;是这台机器不是这个工程需要
+这个编译器编不了 `import std`。工程本身没有任何问题;是这台机器不是这个工程需要
的那台机器。
## 这一现状的代价
-**对人,代价是一次搭建**,每台机器一次、每个新参与者一次:装一个更新的编译器、
-确定哪些构建旗标把模块打开、取得依赖,然后在链接失败时判断坏的是这三者中的哪一
-件。这个代价不随工程成熟而下降,它按人数和机器数重复。
+**对人,代价是一次搭建**,每台机器一次、每个新参与者一次:装一个更新的编译器、
+确定哪些构建旗标把模块打开、取得依赖,然后在链接失败时判断坏的是这三者中的哪一
+件。这个代价不随工程成熟而下降,它按人数和机器数重复。
-**对 agent,代价是上下文**,并且在写下第一行代码之前就消耗在三处:读构建描述以
-确定它究竟做了什么、重建它所假定的环境,以及顺着一个头文件穿过传递 include 去
-确定究竟声明了什么。
+**对 agent,代价是上下文**,并且在写下第一行代码之前就消耗在三处:读构建描述以
+确定它究竟做了什么、重建它所假定的环境,以及顺着一个头文件的传递 include 一路
+追下去,才能确定其中究竟声明了什么。
-模块消掉了第三处,因为接口是显式的,`import` 陈述了用到什么。mcpp 消掉另外两处,
+模块消掉了第三处,因为接口是显式的,`import` 陈述了用到什么。mcpp 消掉另外两处,
并把第一处收敛为一条命令。
## mcpp 的组成
```
-mcpp = 通用构建系统
- + 构建插件
- + 包管理
- + 工具链管理
- + 环境与运行时(xlings)
+mcpp = build system
+ + build plugins
+ + package manager
+ + toolchain management
+ + the environment and runtime (xlings)
```
-多数 C++ 工程要把这五个部分从不同工具里拼起来,而上面那份搭建代价正是拼装本身的
-代价:构建文件假定机器上有某个编译器,包管理器假定一份不是它写的构建文件,而环境
+多数 C++ 工程要把这五个部分从不同工具里拼起来,而上面那份搭建代价正是拼装本身的
+代价:构建文件假定机器上有某个编译器,包管理器假定一份不是它写的构建文件,而环境
是 README 里的一段话。
-mcpp 是一个程序,五个部分共用同一套模型。
+mcpp 是一个程序,五个部分共用同一套模型。
-对于已经在用相应工具的读者:
+对于已经在用相应工具的读者:
| 组成部分 | 在 mcpp 中的形式 | 可对照的工具 |
|---|---|---|
| 通用构建系统 | `mcpp.toml`、模块图、ninja 后端 | CMake、Meson |
| 构建插件 | `build.mcpp`、规则包 | `build.zig`、xmake rules |
| 包管理 | `[dependencies]`、`mcpp.lock`、索引 | Conan、vcpkg |
-| 工具链管理 | 编译器作为被安装并钉住的载荷 | Zig 自带的工具链、rustup、手工安装 GCC / LLVM / MSVC |
+| 工具链管理 | 已安装并钉版本的编译器载荷 | Zig 自带的工具链、rustup、手工安装 GCC / LLVM / MSVC |
| 环境与运行时 | `[xlings]`、载荷、运行期搜索路径 | Nix、conda |
-整体上最接近的两个类比是 Cargo 与 Zig,二者各对应同一个想法的一半。Cargo 是一个
-程序同时充当构建、包管理、锁文件与测试运行器,于是一个 Rust 工程克隆下来直接构建、
-没有前置步骤。Zig 把工具链随工具一起发布,并且默认支持交叉编译,于是编译器不是
+整体上最接近的两个类比是 Cargo 与 Zig,二者各对应同一个想法的一半。Cargo 是一个
+程序同时充当构建、包管理、锁文件与测试运行器,于是一个 Rust 工程克隆下来直接构建、
+没有前置步骤。Zig 把工具链随工具一起发布,并且默认支持交叉编译,于是编译器不是
机器必须先具备的东西。
-mcpp 在 C++ 上是这个形状,外加二者都没有的一部分:环境层 —— 工程通过它声明构建
+mcpp 在 C++ 上是这个形状,外加二者都没有的一部分:环境层 —— 工程通过它声明构建
所需的非编译器工具。
-**这张表为各部分定位,不宣称等价。** 表中每个工具在它自己的领域里做的都比 mcpp
-多,需要那种深度的工程使用它。
+**这张表为各部分定位,不宣称等价。** 表中每个工具在它自己的领域里做的都比 mcpp
+多,需要那种深度的工程使用它。
## 核心保证
-> **拿到任何一个 mcpp 工程,`mcpp build` 就能构建** —— 不需要自行安装编译器、
-> 配置环境,也不需要寻找依赖。
+> **拿到任何一个 mcpp 工程,`mcpp build` 就能构建** —— 不需要自行安装编译器、
+> 配置环境,也不需要寻找依赖。
-这里写下两条边界,以便这句话可以被依赖:面向设备的工程第一次构建仍会下载该设备的
-工具包;而这台机器服务不了的目标会被点名拒绝,不会被错误地构建出来。
+这里写下两条边界,以便这句话可以被依赖:面向设备的工程第一次构建仍会下载该设备的
+工具包;而这台机器服务不了的目标会被点名拒绝,不会被错误地构建出来。
## 最小示例
-还是那台机器,唯一的 C++ 编译器是上面那个 GCC 13。
+还是那台机器,唯一的 C++ 编译器是上面那个 GCC 13。
```console
$ mcpp new hello
@@ -101,7 +101,7 @@ Created bin package 'hello' at /tmp/zero-demo/hello
Next: cd hello && mcpp build && mcpp run (or `mcpp test`)
```
-四个文件,manifest 五行:
+四个文件,manifest 五行:
```toml
[package]
@@ -112,7 +112,7 @@ license = "Apache-2.0"
```
**没有声明编译器、没有声明语言档位、没有声明任何依赖。** 这就是一个读者在改动
-这个工程之前需要理解的全部。而源码用的正是这台机器的编译器不具备的那个特性:
+这个工程之前需要理解的全部。而源码用的正是这台机器的编译器不具备的那个特性:
```cpp
import std;
@@ -133,17 +133,17 @@ Hello from hello!
Built with import std + std::println on modular C++23.
```
-墙钟 1.25 秒,含首次运行。完成这次构建的编译器:
+墙钟 1.25 秒,含首次运行。完成这次构建的编译器:
```console
$ mcpp self env
default toolchain = gcc@16.1.0
```
-mcpp 安装了 GCC 16 并使用它。宿主上没有任何东西被改动,而克隆这个工程的同事 ——
-或者 agent —— 得到的是同一个编译器,不是那台机器恰好自带的那个。
+mcpp 安装了 GCC 16 并使用它。宿主上没有任何东西被改动,而克隆这个工程的同事 ——
+或者 agent —— 得到的是同一个编译器,不是那台机器恰好自带的那个。
-**增加一个依赖是一行,不需要其他步骤:**
+**增加一个依赖是一行,不需要其他步骤:**
```toml
[dependencies]
@@ -154,18 +154,18 @@ mcpp 安装了 GCC 16 并使用它。宿主上没有任何东西被改动,而克
## 适用范围
-mcpp 围绕 **C++20/23 模块与较新的语言特性**建立,它维护的生态由此而来,而不是
-来自一个泛化的目标:
+mcpp 围绕 **C++20/23 模块与较新的语言特性**建立,它维护的生态由此而来,而不是
+来自一个泛化的目标:
| | |
|---|---|
| 模块化 C++ | `import std` 零配置、模块扫描、跨工程 BMI 缓存 |
| 嵌入式与裸机 | freestanding 目标、板级支持包、从源码到一个可运行镜像只需一条命令 |
-| 异构计算与 GPU | CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C,每一条都是规则包而不是引擎特性 |
-| 图形 | 着色器作为构建的一部分被编译,并以模块到达 |
+| 异构计算与 GPU | CUDA、HIP、SYCL、Vulkan/SPIR-V 与 Ascend C,每一条都是规则包而不是引擎特性 |
+| 图形 | 着色器随构建一起编译,并以模块到达 |
| 内核与底层 | 零 libc 档、显式的链接模型、没有隐藏的宿主依赖 |
-不需要其中任何一项的工程,同样得到上述保证;需要其中之一的工程,不必离开这个
+不需要其中任何一项的工程,同样得到上述保证;需要其中之一的工程,不必离开这个
工具去得到它。
## 阅读路径
@@ -176,5 +176,5 @@ mcpp 围绕 **C++20/23 模块与较新的语言特性**建立,它维护的生态
| 判断 mcpp 是否适用于当前工作 | [02 —— 场景](02-scenarios.md) |
| 阅读一个结构相近的工程 | [03 —— 示例项目](03-examples.md) |
-本章之后的一切都是参考:manifest 可以陈述什么、依赖怎样解析、目标怎样命名。本章
+本章之后的一切都是参考:manifest 可以陈述什么、依赖怎样解析、目标怎样命名。本章
按设计不写任何字段与旗标。
diff --git a/docs/zh/01-getting-started.md b/docs/zh/01-getting-started.md
index 4ee3852a0..d081df364 100644
--- a/docs/zh/01-getting-started.md
+++ b/docs/zh/01-getting-started.md
@@ -1,37 +1,41 @@
# 01 —— 快速开始
-**读者:**还什么都没装的新用户。
+**读者:** 还什么都没装的新用户。
-**本章回答的那一个问题:**从一台空机器开始,怎样把一个程序编译并运行起来。
+**本章回答的那一个问题:** 从一台空机器开始,怎样把一个程序编译并运行起来。
-**不在这里:**mcpp 有哪些部件 —— 那是 [00 —— mcpp 是什么](00-what-mcpp-is.md),
-本章假定它而不重复它;以及 manifest 可以写的每一个字段,那是
-[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md)。下一章:[03 —— 示例项目](03-examples.md)。
+**不在这里:** 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 全流程。
+> 5 分钟走完 install → new → build → run → pack 全流程。
## 安装
-支持的宿主为 Linux x86_64 / aarch64、macOS ARM64 与 Windows x86_64。GCC、xlings 以及
-其余构建依赖都由 mcpp 安装,一个都不需要预先具备。
+支持的宿主为 Linux x86_64 / aarch64、macOS ARM64 与 Windows x86_64。
+GCC、xlings 以及其余构建依赖都由 mcpp 安装,都不需要预先具备。
-**推荐方式是 [xlings](https://xlings.d2learn.org)**,它让 mcpp 与系统环境保持隔离:
+**推荐方式是 [xlings](https://xlings.d2learn.org)**,它让 mcpp 与系统
+环境保持隔离:
```bash
xlings install mcpp -y
```
-其它方式:独立安装脚本,以及首次运行会装什么
+其它方式:独立安装脚本,以及首次运行会装什么
-在 Linux x86_64/aarch64 或 macOS ARM64 上,有一个内置 xlings 的一键脚本,把一切装到
-`~/.mcpp/` 下。它不支持 Windows —— 那里的入口是 README 里的 PowerShell xlings 命令。
+在 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
```
-mcpp 首次运行时会把一条默认工具链装进 `~/.mcpp/`,按宿主选择:
+mcpp 首次运行时会把一条默认工具链装进 `~/.mcpp/`,按宿主选择:
| 宿主 | 默认 |
|---|---|
@@ -41,11 +45,12 @@ mcpp 首次运行时会把一条默认工具链装进 `~/.mcpp/`,按宿主选择
| 有可用 MSVC 的 Windows | `llvm@20.1.7` |
| 没有 MSVC 的 Windows | 面向 `x86_64-windows-gnu` 的 `gcc@16.1.0` |
-完整安装说明(含 Windows)见 [README 的「安装」小节](../../README.zh-CN.md#安装)。
+完整安装说明(含 Windows)见
+[README 的「安装」小节](../../README.zh-CN.md#安装)。
-安装完成后,启动新的 shell 会话,然后验证:
+安装完成后,启动一个新的 shell 会话,然后验证:
```bash
mcpp --version
@@ -53,12 +58,13 @@ mcpp --version
```
> [!TIP]
-> Unix release 安装脚本若提示 `command not found`,通常是 `~/.mcpp/bin`
-> 尚未加入当前 shell 的 PATH。重启终端,或执行 `source ~/.bashrc`(zsh 对应
-> `~/.zshrc`,fish 使用 `exec fish`)即可生效;该安装方式可直接通过
-> `~/.mcpp/bin/mcpp` 调用。若经 xlings 安装,应使用 xlings 当前激活的 bin
-> 目录。Windows 请使用 PowerShell 的 xlings 安装命令,重启 PowerShell 而不是
-> 执行 `source`,并用 `Get-Command mcpp.exe` 确认当前命令。
+> 若 Unix release 安装脚本提示 `command not found`,通常是因为
+> `~/.mcpp/bin` 尚未加入当前 shell 的 PATH。重启终端,或执行
+> `source ~/.bashrc`(zsh 对应 `~/.zshrc`,fish 用 `exec fish`)以生效;
+> 该安装方式下可直接用 `~/.mcpp/bin/mcpp` 调用。若通过 xlings 安装,应改
+> 用 xlings 当前激活的 bin 目录。Windows 上请通过 PowerShell 的 xlings
+> 命令安装,重启 PowerShell 而不是执行 `source`,并用
+> `Get-Command mcpp.exe` 确认当前生效的命令。
## 创建项目
@@ -66,18 +72,19 @@ mcpp --version
mcpp new hello && cd hello
```
-生成的目录结构如下:
+生成的目录结构如下:
```
hello/
-├── mcpp.toml ← 工程描述
+├── mcpp.toml ← project manifest
├── src/
│ └── main.cpp
└── tests/
- └── test_smoke.cpp ← 可由 `mcpp test` 运行
+ └── test_smoke.cpp ← runs with `mcpp test`
```
-生成的 manifest 只包含包元数据;mcpp 会从 `src/main.cpp` 推断 binary target。该文件默认为 C++23 模块化的 hello world:
+生成的 manifest 只含包元数据;mcpp 从 `src/main.cpp` 推断 binary
+target。默认情况下该文件是一个 C++23 模块化的 hello world:
```cpp
import std;
@@ -88,23 +95,24 @@ int main() {
}
```
-### 从包模板创建项目
+### 从包模板创建
-`mcpp new --template` 与 `mcpp add` 使用同一种精确包 selector 风格:
+`mcpp new --template` 与 `mcpp add` 使用完全相同的包 selector 语法:
```bash
mcpp new gui-demo --template ocornut.imgui@1.92.8:docking
mcpp new --list-templates ocornut.imgui@1.92.8
```
-文法是 `[namespace.]name[@version][:template]`,namespace、version 与模板名可分别
-省略。省略 namespace 只表示唯一默认命名空间 `mcpplibs`,不会按短名扫描整个索引。
-省略模板名时,mcpp 使用唯一的 `default = true`;若包只有一个模板且未显式声明
-default,该单模板自动成为默认。多个模板却没有 default 会明确报错并提示
-`--list-templates`。不再引入另一套 `--variant` 词汇。
+文法是 `[namespace.]name[@version][:template]`。namespace、version 与
+模板名各自独立可省略;省略 namespace 表示唯一的默认命名空间
+`mcpplibs`,而不是按短名扫描整个索引。省略模板名时,mcpp 使用唯一那个
+`default = true` 声明;若没有显式标记默认值而包只有一个模板,就自动使用
+那个唯一的模板。存在多个模板却没有默认值是一个错误,会指向
+`--list-templates`。没有另一套 `--variant` 词汇。
-包身份、版本与模板会在提交目标目录前全部解析完成;下载、渲染、hook 或校验失败时,
-不会留下半成品项目目录。
+包身份、版本与模板会在提交目标目录之前全部解析完成。下载、渲染、hook 或
+校验失败时,不会留下一个半成品的工程目录。
## 构建与运行
@@ -117,103 +125,114 @@ mcpp run
# Built with import std + std::println on modular C++23.
```
-首次构建需下载随宿主选择的默认工具链,期间显示进度与速度。下载完成后,所有 mcpp 项目共用同一份沙盒。
+首次构建会下载按宿主选择的默认工具链,期间显示进度与速度。下载完成后,
+所有 mcpp 工程共用同一份沙盒。
-### 在首次成功构建前配置 IDE
+### 在首次成功构建前配置编辑器
-源码尚未可构建时,可以先生成编译数据库,而不编译普通翻译单元或链接最终目标:
+源码尚不可构建时,可以只生成编译数据库,而不编译普通翻译单元、也不链接
+最终目标:
```bash
mcpp build --configure-only
# Configured hello (... compile commands)
```
-该命令与普通构建使用相同的包、workspace member、profile、feature、capability、
-target 和 toolchain 解析结果。生成的 `compile_commands.json` 同时覆盖普通源码与
-`tests/**/*.cpp`,并把测试专用依赖及匹配的 `[build].flags` 带入测试 TU,因此 clangd/ccls
-可以在代码尚未编译通过时索引工程。它是“只配置”而不是只读操作:`build.mcpp`、缺失的
-依赖或 toolchain、lock/resolution 元数据以及构建目录元数据仍可能被更新,只能在可信
-workspace 中运行。插件稳定依赖进程退出码和生成的 `compile_commands.json`,标准输出仍是
-面向人的文本,不作为机器协议。
+该命令解析的包、workspace 成员、profile、feature、capability provider、
+target 与 toolchain 与一次真实构建完全相同。生成的 `compile_commands.json`
+覆盖普通源码与 `tests/**/*.cpp`,包含仅测试使用的依赖以及匹配的
+`[build].flags`,因此 clangd/ccls 可以在工程仍处于编辑状态时为它建立
+索引。这是一次配置操作,不是只读操作:`build.mcpp`、缺失的依赖或
+toolchain、lock/resolution 元数据以及构建目录元数据都可能被更新。只应在
+可信的 workspace 中运行。进程退出码与生成的 `compile_commands.json` 是
+稳定的集成契约;标准输出仍是面向人的文本。
-不允许写入工程目录的编辑器在标准输出上取得同一份计划 *(mcpp 2026.9.15.1+)*:
+不允许写入工程目录的编辑器,在标准输出上取得同一份计划
+*(mcpp 2026.9.15.1+)*:
```bash
mcpp emit build-database --format json
```
-文档是 S1 构建数据库:每个翻译单元及其编译命令、它提供与导入的模块、工具链以及标准库
-模块单元。`--spec compile-commands` 改为输出 `compile_commands.json` 的条目。字段见
-[50 —— 机器可读输出](50-machine-output.md),规则见 [SPEC-005](../specs/build-database.md)。
+该文档是一份 S1 构建数据库:每个翻译单元及其编译命令、它提供与导入的
+模块、工具链,以及标准库模块单元。`--spec compile-commands` 改为输出
+`compile_commands.json` 的条目。字段列在
+[50 —— 机器可读输出](50-machine-output.md),规则见
+[SPEC-005](../specs/build-database.md)。
## 增量编译与测试
-[08 —— 测试](08-testing.md) 是拥有这个主题的章节;下面只是本教程需要的那一步。
-
```bash
-mcpp build # 增量构建
-mcpp clean # 清理 target/
-mcpp clean --stale # 只删 target/<三元组>/<指纹>/ 下已无构建使用的目录 (--dry-run 只列出; --older-than 3d 保留更新的未记录目录)
-mcpp test # 编译并运行 tests/**/*.cpp —— 每文件一个独立二进制,
- # 框架无关(裸 main,或经 [dev-dependencies] 使用 gtest)
-mcpp test # 只运行名字包含 的测试
-mcpp test --list # 只枚举测试,不构建
-mcpp test --timeout 30 # 单个测试**运行**超过 30s 被终止(默认 300;0 = 不限)
-mcpp test --build-timeout 120 # 单次编译/链接超过 120s 被终止(默认关闭)
+mcpp build # incremental build
+mcpp clean # clean target/
+mcpp clean --stale # drop only target/// dirs no build still uses
+ # (--dry-run lists and deletes nothing; --older-than 3d keeps newer unrecorded ones)
+mcpp test # compile and run tests/**/*.cpp — one binary per file,
+ # framework-agnostic (bare main, or gtest via [dev-dependencies])
+mcpp test # only tests whose name contains
+mcpp test --list # enumerate tests without building
+mcpp test --timeout 30 # kill a test still RUNNING after 30s (default 300; 0 = no limit)
+mcpp test --build-timeout 120 # kill a compile/link still running after 120s (off by default)
```
-**运行**那一半默认有界 —— 无人值守的 CI 不该被一个挂住的测试吃掉整个 job。两个期限
-覆盖的是不同的一半,互不蕴含:`--timeout` 约束测试**进程的运行**,`--build-timeout`
-约束**单次 ninja 驱动**(包级构建、批量测试构建、每个测试各自独立计时)。
-**链接卡死属于 `--build-timeout`,`--timeout` 设多大都无效。**
+**运行**那一半默认有界,这样无人值守的 CI 任务就不会被一个挂住的测试拖住
+整个 job。两个期限覆盖不同的一半,互不蕴含:`--timeout` 约束测试
+**进程**,`--build-timeout` 约束**一次 ninja 驱动**(包级构建、批量测试
+构建、每个测试各自的构建分别计时)。**一次永不返回的链接属于
+`--build-timeout` 管辖的情形;任何 `--timeout` 值都拦不住它。**
-`--build-timeout` 默认关闭,这个不对称是**实测**出来的而非风格选择:单个测试跑过 5 分钟
-不寻常,而冷依赖构建跑过 15 分钟很平常(mcpp-index 有一个成员要从源码建 OpenCV,
-linux 1019s、windows 1289s)。给它一个默认上限会把「慢但正确」的构建判红。构建可以跑多久
-是工程自身的性质,所以由工程来说。仅 POSIX 有效 —— Windows 上没有 kill-by-handle 路径,
-该值被忽略。
+`--build-timeout` 默认关闭,这种不对称是**实测**得到的,而不是风格选择:
+一个测试二进制运行超过五分钟不寻常,一次冷依赖构建运行超过十五分钟很
+平常(mcpp-index 有一个成员从源码构建 OpenCV,Linux 上 1019 秒、
+Windows 上 1289 秒)。给它一个默认上限会把「慢但正确」的构建判红。构建
+可以跑多久是工程自身的性质,所以由工程来说。仅 POSIX 有效——期限运行器
+在 Windows 上没有按句柄终止的路径,该值在那里被忽略。
## 添加依赖
-在 `mcpp.toml` 中声明依赖:
+在 `mcpp.toml` 中声明依赖:
```toml
[dependencies]
"mcpplibs.cmdline" = "^0.0.1"
```
-`mcpp build` 将自动从
+`mcpp build` 会自动针对
[mcpp-index](https://github.com/mcpplibs/mcpp-index) 解析 SemVer
-约束、拉取源码并加入编译图。完整示例参见
-[03 — 示例项目](03-examples.md) 中的 `02-with-deps`。
+约束、拉取源码并加入构建图。完整示例见
+[03 —— 示例项目](03-examples.md) 中的 `02-with-deps`。
## 生成发布包
-`mcpp pack` 将构建产物与运行期依赖打包为可独立分发的 tarball:
+`mcpp pack` 把构建产物与运行期依赖打包为可独立分发的 tarball:
```bash
-mcpp pack # 默认 vendored,打包项目第三方 .so
-mcpp pack --mode system # 依赖目标系统提供库
-mcpp pack --mode static # musl 全静态构建
-mcpp pack --mode self-contained # 打包 loader、libc 与依赖
+mcpp pack # vendored by default: bundle project third-party .so files
+mcpp pack --mode system # rely on target-system libraries
+mcpp pack --mode static # fully static musl build
+mcpp pack --mode self-contained # bundle loader, libc, and dependencies
```
-四种模式的差异及产物布局参见 [10 — 发布打包](10-pack-and-release.md)。`bundle-project` 与 `bundle-all` 仍分别是 `vendored` 与 `self-contained` 的兼容别名。
+四种模式的差异及产物布局见
+[10 —— 发布打包](10-pack-and-release.md)。`bundle-project` 与
+`bundle-all` 仍然是 `vendored` 与 `self-contained` 的可用别名。
## 后续阅读
-- [03 — 示例项目](03-examples.md) — 可直接运行的最小工程集合
-- [10 — 发布打包](10-pack-and-release.md) — 构建可分发产物
-- [20 — 工具链管理](20-toolchains.md) — 切换编译器与多版本管理
-- 任意命令的完整选项可通过 `mcpp --help` 查阅
-
+- [03 —— 示例项目](03-examples.md)——可直接运行的最小工程集合
+- [10 —— 发布打包](10-pack-and-release.md)——构建可分发产物
+- [20 —— 工具链管理](20-toolchains.md)——切换编译器与管理多个版本
+- 任意命令的完整选项都可通过 `mcpp --help` 查阅
## 更多入口
-- GUI 起步:`mcpp new myapp --template ocornut.imgui@1.92.8:docking`(模板随包分发;
- 省略 `:docking` 使用已声明 default/唯一模板,或运行
- `mcpp new --list-templates ocornut.imgui@1.92.8`)。
-- 解释默认决策:`mcpp why [toolchain|runtime|deps]`;主机能力体检:`mcpp self doctor`;
- 机器可读解析清单:构建产物 `target///resolution.json`。
-- 离线运行:`mcpp --offline` 或 `MCPP_OFFLINE=1` 可阻止索引刷新、下载和工具链安装。在从未使用过的 home 中,它同时跳过首次使用时的沙箱引导(索引克隆、ninja、patchelf),只提示一次,并让该 home 保持未引导状态;需要这些工具的命令会自行报告。
-
+- GUI 起步:`mcpp new myapp --template ocornut.imgui@1.92.8:docking`
+ (模板随包分发;省略 `:docking` 使用已声明的 default / 唯一模板,或
+ 运行 `mcpp new --list-templates ocornut.imgui@1.92.8`)。
+- 解释默认决策:`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
index 4f4f375cd..55990490c 100644
--- a/docs/zh/02-scenarios.md
+++ b/docs/zh/02-scenarios.md
@@ -1,17 +1,18 @@
# 02 —— 场景
-**读者:**正在判断 mcpp 适不适合手头这份工作、以及这份工作会用到它哪些功能的
+**读者:** 正在判断 mcpp 是否适合手头这份工作、以及这份工作会用到它哪些功能的
开发者。
-**本章回答的那一个问题:**mcpp 服务哪几类工程,而对其中一类,要用到哪些功能、
+**本章回答的那一个问题:** mcpp 服务哪几类工程,而对其中一类,要用到哪些功能、
按什么顺序。
-**不在这里:**字段参考,那是 [04](04-mcpp-toml.md);示例目录,那是
-[03](03-examples.md) —— 它按**示例**索引而不是按场景;以及命令查阅,那是
+**不在这里:** 字段参考,那是 [04](04-mcpp-toml.md);示例目录,那是
+[03](03-examples.md)——它按**示例**索引而不是按场景;以及命令查阅,那是
[09](09-commands-by-scenario.md)。本章按**工作本身**索引。
-每个场景陈述:处境、mcpp 对它贡献了什么、穿过章节的路径、可以跑的一个工程,以及
-**最常让人意外的那一件事**。只读与手头工作相符的那个场景即可;它们之间不相互叠加。
+每个场景陈述:处境、mcpp 对它贡献了什么、穿过各章节的路径、一个可以跑的工程,
+以及**最常让人意外的那一件事**。只读与手头工作相符的那一个场景即可;场景之间
+互不叠加。
## 场景一览
@@ -25,225 +26,236 @@
| [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` |
+| [9](#9-工程需要的一步构建工作) | 工程需要的一步构建工作,以及把它共享出去 | `examples/08-build-rules`、`12-a-new-device-language` |
| [10](#10-为别人打包一个工具一个驱动或一块板子) | 为别人打包一个工具、一个驱动或一块板子 | `xim-pkgindex` 与 `mcpp-index` 里的描述符 |
## 1. 命令行工具或服务
-**处境。** 一个由 C++23 模块构建的程序,带几个依赖,而且要跑在一台没有 mcpp 的
-机器上。
+**处境。** 一个由 C++23 模块构建的程序,带几个依赖,而且要跑在一台没有 mcpp
+的机器上。
-**mcpp 贡献了什么。** `import std` 零配置可用;编译器是被钉住的载荷,而不是机器
-上恰好有的那个;并且一条命令产出一个自带所需之物的二进制。
+**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) —— 当编译器版本要紧,或者工程必须钉住
+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`。
+6. [10 —— 发布打包](10-pack-and-release.md)——`mcpp pack`。
-**跑。** [`examples/01-hello`](../../examples/01-hello/),然后
-[`02-with-deps`](../../examples/02-with-deps/),再
+**跑。** [`examples/01-hello`](../../examples/01-hello/),然后
+[`02-with-deps`](../../examples/02-with-deps/),再
[`03-pack-static`](../../examples/03-pack-static/)。
-**最让人意外的一件事。** debug 构建与 release 构建互不失效。每种配置在 `target/`
-下有自己的指纹目录,所以在两者之间来回切换不是一次重建。
+**最让人意外的一件事。** debug 构建与 release 构建互不失效。每种配置在
+`target/` 下有自己的指纹目录,所以在两者之间来回切换不是一次重建。
## 2. 供他人 import 的库
-**处境。** 别的包会把它写进 `[dependencies]` 的代码,其中可能有可选的部分。
+**处境。** 别的包会把它写进自己的 `[dependencies]`,其中可能含可选的部分。
-**mcpp 贡献了什么。** 模块接口就是被发布的表面;feature 让其中一部分成为可选而
-不必拆出第二个包;预建二进制可以声明它与哪些工具链兼容。
+**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) —— 如果同时交付二进制。
+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/) 是
-生产方与消费方成对的那个。
+**跑。** [`examples/11-features`](../../examples/11-features/) 把三种 feature
+形态都声明了一遍;[`05-lib-distribution`](../../examples/05-lib-distribution/)
+是生产方与消费方成对的那一个。
-**最让人意外的一件事。** 判断一个可选后端是否真的可选,判据**不是**「默认构建仍然
-能过」,而是「默认构建的**解析里不出现**那个包」。一个被解析出来却没被用到的依赖,
-仍然要付下载的代价。
+**最让人意外的一件事。** 判断一个可选后端是否真的可选,判据**不是**「默认
+构建仍然能过」,而是「默认构建的**解析里不出现**那个包」。一个被解析出来却
+没被用到的依赖,仍然要付下载的代价。
## 3. 一个仓库里的多个包
-**处境。** 多个包一起开发,彼此按路径依赖。
+**处境。** 多个包一起开发,彼此按路径相互依赖。
-**mcpp 贡献了什么。** 一条命令构建或测试整组;成员之间不经注册表就能相互解析;
-每个成员保留自己的 manifest 与身份。
+**mcpp 贡献了什么。** 一条命令构建或测试整组;成员之间不经注册表就能相互
+解析;每个成员保留自己的 manifest 与身份。
**路径。**
-1. [07 —— 工作空间](07-workspace.md) —— `[workspace]`、path 依赖。
-2. [05 —— 依赖与解析](05-dependencies.md) —— path 依赖做什么、不做什么。
-3. [08 —— 测试](08-testing.md) —— 扇出。
+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 贡献了什么。** 窗口与渲染栈就是普通依赖;模板脚手架出一个能跑的工程;产物
-在运行期需要什么是**被声明的**,不是被发现的。
+**mcpp 贡献了什么。** 窗口与渲染栈就是普通依赖;模板脚手架出一个能跑的工程;
+产物在运行期需要什么是**被声明的**,不是被发现的。
**路径。**
-1. [01 —— 快速开始](01-getting-started.md) —— `mcpp new --template`。
-2. [05 —— 依赖与解析](05-dependencies.md) —— 那套栈。
-3. [10 —— 发布打包](10-pack-and-release.md) —— 可执行文件旁边还要带什么。
+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 —— 都经由工程
-声明的适配包到达,而不是靠它装在机器上。
+**最让人意外的一件事。** 在 Linux 上,mcpp 的产物跑在一个**不查 `/usr/lib`**
+的私有加载器后面。任何必须由宿主供给的东西——图形驱动、Vulkan ICD——都经由
+工程声明的适配包到达,而不是靠它装在机器上。
## 5. 为另一个操作系统构建
**处境。** 一份源码树要产出 Linux、Windows 与 macOS 的二进制。
-**mcpp 贡献了什么。** 目标是一个参数,不是第二份检出;交叉工具链是载荷;而这台
-宿主服务不了的目标会被**拒绝**,不会被悄悄按宿主构建。
+**mcpp 贡献了什么。** 目标是一个参数,不是第二份检出;交叉工具链是载荷;这台
+宿主服务不了的目标会被**拒绝**,而不是被悄悄按宿主构建。
**路径。**
-1. [21 —— 目标三元组](21-the-target-triple.md) —— 目标怎么命名,哪台宿主服务哪个。
-2. [24 —— 基于 openkal 的交叉构建](24-openkal-cross.md) —— 机制。
-3. [22 —— 目标侧](22-target-side.md) —— manifest 必须按目标不同时。
+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/) —— 一个
-程序在任意宿主上为四个目标构建。
+**跑。** [`examples/06-openkal-cross`](../../examples/06-openkal-cross/)——
+一个程序在任意宿主上为四个目标构建。
-**最让人意外的一件事。** `--target` 不要求机器上**已经有**那条工具链。它要求的是
-这个目标能被这台宿主**服务**;[21](21-the-target-triple.md) 里的支持矩阵说明哪些
-可以。
+**最让人意外的一件事。** `--target` 不要求机器上**已经有**那条工具链。它要求
+的是这个目标能被这台宿主**服务**;[21](21-the-target-triple.md) 的支持矩阵
+说明哪些可以。
## 6. 没有操作系统的目标
-**处境。** 板子或单片机上的固件:没有 OS、默认没有 libc、有链接脚本与向量表。
+**处境。** 板子或单片机上的固件:没有 OS、默认没有 libc、有链接脚本与向量表。
-**mcpp 贡献了什么。** 板级支持包供给整个目标世界 —— 链接脚本、启动代码,以及
-**runner** —— 于是 `mcpp run` 在模拟器上与在真实硬件上是同一条命令。
+**mcpp 贡献了什么。** 板级支持包供给整个目标世界——链接脚本、启动代码,以及
+**runner**——于是 `mcpp run` 在模拟器上与在真实硬件上是同一条命令。
**路径。**
-1. [40 —— 裸机与 freestanding 目标](40-baremetal.md) —— 目标、档位、`std` 还剩什么。
-2. [41 —— 抵达一台设备](41-devices.md) —— runner、具名 runner。
-3. [08 —— 测试](08-testing.md) —— 跑在板子上的测试。
+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;开发者敲的那条命令不变。
+**最让人意外的一件事。** 模拟器与真实板子相差的是**一个 feature**,不是两个
+包。`mcpp run --features hardware` 移动的是默认 runner;开发者敲的那条命令
+不变。
## 7. GPU 或加速器上的计算
**处境。** 程序的一部分是由厂商编译器编译、再链接进普通二进制的 kernel。
-**mcpp 贡献了什么。** 设备工具包由规则包声明、由构建安装;加速器是**一条只写一次
-的轴**;产物记录下它携带了哪些设备的代码。
+**mcpp 贡献了什么。** 设备工具包由规则包声明、由构建安装;加速器是**一条只
+写一次的轴**;产物记录下它携带了哪些设备的代码。
**路径。**
-1. [42 —— 异构硬件构建](42-heterogeneous-builds.md) —— `accel`、岛、接缝。
-2. [30 —— 构建程序](30-build-mcpp.md) —— 规则怎样到达图。
-3. [06 —— Feature 与能力](06-features-and-capabilities.md) —— 选中一条 lane 的
- 那个 feature。
+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 = …)` 选择器。
+**最让人意外的一件事。** 不点名加速器的构建**一个字节都不下载**。多 GB 的
+工具包被取之前要开两道闸:选中规则的那个 feature,以及说明「这次构建确实为
+设备编译」的 `cfg(accelerator = …)` 选择器。
## 8. 图形渲染
-**处境。** 编译到 SPIR-V 的着色器、一条管线,以及必须正确的像素。
+**处境。** 编译到 SPIR-V 的着色器、一条管线,以及必须正确的像素。
-**mcpp 贡献了什么。** 着色器编译器是被声明的载荷;编译好的着色器以**模块**到达,
-而不是一个没人写过其名字的生成头;并且渲染结果可以在没有 GPU 的情况下被断言。
+**mcpp 贡献了什么。** 着色器编译器是被声明的载荷;编译好的着色器以
+**模块**到达,而不是一个没人写过其名字的生成头;渲染结果可以在没有 GPU 的
+情况下被断言。
**路径。**
-1. [42 —— 异构硬件构建](42-heterogeneous-builds.md) —— 着色器那条 lane。
-2. [30 —— 构建程序](30-build-mcpp.md) —— 要模块表面的那一行。
-3. [10 —— 发布打包](10-pack-and-release.md) —— 交付它。
+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 贡献了什么。** 这一步变成与其余一切同一张图上的边:被定序、进指纹、可增量,
-失败时按自己的名字被报出。它不是一个构建前脚本。
+**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) —— 声明这一步要跑的工具。
+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 一门引擎从未听说过的语言。
+**跑。** [`examples/08-build-rules`](../../examples/08-build-rules/) 是做检查
+与嵌入的规则;
+[`12-a-new-device-language`](../../examples/12-a-new-device-language/) 教会
+mcpp 一门引擎从未听说过的语言。
-**最让人意外的一件事。** 这一步调用的**那个工具本身是它的输入**。少了这一条,改动
-生成器会让每条边都是干净的,产物保留上一个生成器产生的字节 —— 一次覆盖在陈旧结果
-之上的绿色构建。
+**最让人意外的一件事。** 这一步调用的**那个工具本身是它的输入**。少了这一条,
+改动生成器会让每条边都是干净的,产物保留上一个生成器产生的字节——一次覆盖在
+陈旧结果之上的绿色构建。
## 10. 为别人打包一个工具、一个驱动或一块板子
-**处境。** 别的工程应当能够声明并得到的东西:一个编译器、一个着色器编译器、一个
-模拟器、一个宿主图形驱动、一块板子。
+**处境。** 别的工程应当能够声明并得到的东西:一个编译器、一个着色器编译器、
+一个模拟器、一个宿主图形驱动、一块板子。
-**mcpp 贡献了什么。** 消费者按名字声明它就能得到一个能用的程序 —— 载荷被安装、它的
-库进了产物的搜索路径,而档位决定「一次从不运行的构建要不要为它付代价」。
+**mcpp 贡献了什么。** 消费者按名字声明它就能得到一个能用的程序——载荷被
+安装,它的库进了产物的搜索路径,而档位决定「一次从不运行的构建要不要为它
+付代价」。
**路径。**
-1. [32 —— 编写一个载荷](32-authoring-a-payload.md) —— 由 mcpp 安装的工具或预编译库。
-2. [33 —— 编写运行时适配包](33-authoring-an-adapter.md) —— 当那个库属于宿主、
+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) —— 如果有一步构建工作要驱动
- 这个工具。
+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`。
+**跑。** 描述符本身:载荷看 `xim-pkgindex/pkgs/g/glslang.lua`,适配包看
+`mcpp-index/pkgs/c/compat.vulkan-runtime.lua`,板子看 `mcpplibs/cortex-m-rt`。
-**最让人意外的一件事。** 该用哪个答案,由**许可证与 ABI** 决定,不由偏好决定。
-用户态与内核模块处于锁步、且许可证禁止再分发的驱动**不能**是载荷;它是宿主能力,
-而适配包是产物够到它的方式。开源驱动就是载荷,用着它的机器完全不需要适配包。
+**最让人意外的一件事。** 该用哪个答案,由**许可证与 ABI** 决定,不由偏好
+决定。用户态与内核模块处于锁步、且许可证禁止再分发的驱动**不能**是载荷;
+它是宿主能力,而适配包是产物够到它的方式。开源驱动就是载荷,用着它的机器
+完全不需要适配包。
## 当前边界
-- 这里列出的场景,都是背后有**可运行的工程或已发布模板**的那些。mcpp 服务得了、
- 但本仓库没有任何东西演示的场景不在此列 —— 一条没有工程可跑的路径是一个主张,
- 不是一个场景。
-- 在既有构建系统内部采用 mcpp 不是这里的场景。mcpp 构建它自己拥有的工程;与另一个
- 构建系统的产物互操作既无文档也无示例覆盖。
+- 这里列出的场景,都是背后有**可运行的工程或已发布模板**的那些。mcpp 服务
+ 得了、但本仓库没有任何东西演示的场景不在此列——一条没有工程可跑的路径是
+ 一个主张,不是一个场景。
+- 在既有构建系统内部采用 mcpp 不是这里的场景。mcpp 构建它自己拥有的工程;
+ 与另一个构建系统的产物互操作既无文档也无示例覆盖。
diff --git a/docs/zh/03-examples.md b/docs/zh/03-examples.md
index ff9cc3549..9dffbd2b4 100644
--- a/docs/zh/03-examples.md
+++ b/docs/zh/03-examples.md
@@ -1,16 +1,16 @@
# 03 —— 示例项目
-**读者:**在挑一个起点,或者在找一个与自己形状相近的工程的人。
+**读者:**在挑一个起点,或者在找一个与自己形状相近的工程的人。
-**本章回答的那一个问题:**哪个示例教什么,以及它们以什么顺序相互叠加。
+**本章回答的那一个问题:**哪个示例教什么,以及它们以什么顺序相互叠加。
-**不在这里:**任何一个示例的内容 —— 每个示例自带 README,只解释它新增的部分。
-在此之前:[01 —— 快速开始](01-getting-started.md)。在此之后:
+**不在这里:**任何一个示例的内容 —— 每个示例自带 README,只解释它新增的部分。
+在此之前:[01 —— 快速开始](01-getting-started.md)。在此之后:
[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md)。
-[`examples/`](../../examples) 目录是一套课程。每个工程都可以单独跑起来,而且每个
-工程都教**一件更早的示例没有教过的事**。本章说明那件事是什么,于是你可以按自己
-需要的深度进入,而不必从头读起。
+[`examples/`](../../examples) 目录是一套课程。每个工程都可以单独跑起来,而且每个
+工程都教**一件更早的示例没有教过的事**。本章说明那件事是什么,于是你可以按自己
+需要的深度进入,而不必从头读起。
## 运行方式
@@ -20,8 +20,8 @@ cd mcpp/examples/01-hello
mcpp build && mcpp run
```
-每个示例自带 README,只解释它新增的部分。安装与工具链初始化在
-[01 —— 快速开始](01-getting-started.md),不在示例里重复。
+每个示例自带 README,只解释它新增的部分。安装与工具链初始化在
+[01 —— 快速开始](01-getting-started.md),不在示例里重复。
## 课程
@@ -32,64 +32,64 @@ mcpp build && mcpp run
| [`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` |
+| [`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++ 模块 |
+| [`05-lib-distribution`](../../examples/05-lib-distribution/) | 一个库的接口与它的预编译二进制;从同一份源产出 C 头文件与 C++ 模块 |
### C —— 环境
| 示例 | 首次引入的内容 |
|---|---|
-| [`07-project-subos`](../../examples/07-project-subos/) | `[xlings]`、`[xlings.workspace]`,以及 `PATH` 来自工程声明环境的构建程序 |
+| [`07-project-subos`](../../examples/07-project-subos/) | `[xlings]`、`[xlings.workspace]`,以及 `PATH` 来自工程声明环境的构建程序 |
### D —— 目标
| 示例 | 首次引入的内容 |
|---|---|
-| [`06-openkal-cross`](../../examples/06-openkal-cross/) | `--target`,同一份源在任意宿主上为四台机器构建 |
+| [`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/) | 第二个编译器,自带它自己的标准库 |
+| [`…/boundary`](../../examples/09-heterogeneous/boundary/) | 单独的岛边界:消费者 import 一个生成的模块,工程里没有接缝也没有头文件。不需要设备 |
+| [`…/cuda`](../../examples/09-heterogeneous/cuda/) | 设备编译器、生成边界之上的接缝、把驱动版本既陈述为事实也陈述为下限 |
+| [`…/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/) | 结果是像素的渲染管线,并与软件光栅器逐像素比对 |
+| [`…/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 = [...]` 为构建机构建出来的包 |
-| [`13-platform-targets`](../../examples/13-platform-targets/) | 一份源码、零个 `cfg`,只改 `--target` 就为 Linux、WebAssembly 和两个 Android ABI 构建;`min_api_level` 作为工程自己的决定,以及一条不能被覆盖的能力钉 |
+| [`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 = [...]` 为构建机构建出来的包 |
+| [`13-platform-targets`](../../examples/13-platform-targets/) | 一份源码、零个 `cfg`,只改 `--target` 就为 Linux、WebAssembly 和两个 Android ABI 构建;`min_api_level` 作为工程自己的决定,以及一条不能被覆盖的能力钉 |
[31 —— 编写规则包](31-authoring-a-rule-package.md) 是这两个示例所演示内容的参考。
## 以模板形式到达的课程
-一个包可以提供 `templates//`,由 `mcpp new --template` 实例化。那是与本目录
-和章节并列的第三个教学面;当被教的东西属于某个包而不属于 mcpp 时,课程就落在那里。
+一个包可以提供 `templates//`,由 `mcpp new --template` 实例化。那是与本目录
+和章节并列的第三个教学面;当被教的东西属于某个包而不属于 mcpp 时,课程就落在那里。
| 模板 | 课程 | 章节 |
|---|---|---|
| `riscv-virt-rt` | 一个裸机工程、它的板级支持与它的 runner | [40](40-baremetal.md) |
-| `riscv-virt-rt:nolibc` | 同上,但没有 C 库 | [40](40-baremetal.md) |
+| `riscv-virt-rt:nolibc` | 同上,但没有 C 库 | [40](40-baremetal.md) |
| `ocornut.imgui` | 一个带窗口与渲染栈的图形应用 | [20](20-toolchains.md) |
```bash
@@ -98,12 +98,12 @@ mcpp new blinky --template riscv-virt-rt
## 新增一个示例
-一个示例目录是 `mcpp.toml` + `src/` + `README.md`,编号接在最后一个之后。什么时候
-值得新增一个示例:当一个能力**改变工程的形状** —— 它包含的文件、它声明的 manifest、
-或者作者敲的命令。如果一个能力只是既有示例工程里的一行,它属于对应章节里的一个
-代码块;如果它只经由命令到达,它属于
+一个示例目录是 `mcpp.toml` + `src/` + `README.md`,编号接在最后一个之后。值得
+新增一个示例的判据:一个能力**改变了工程的形状** —— 它包含的文件、它声明的 manifest、
+或者作者敲的命令。如果一个能力只是既有示例工程里的一行,它属于对应章节里的一个
+代码块;如果它只经由命令到达,它属于
[09 —— 按场景选命令](09-commands-by-scenario.md)。
-README 要写明这个示例第一个教什么,以及判断它是否成立的判据。贡献流程见
+README 要写明这个示例第一个教什么,以及判断它是否成立的判据。贡献流程见
[90 —— 从源码构建 & 参与贡献](90-build-from-source.md)。
diff --git a/docs/zh/04-mcpp-toml.md b/docs/zh/04-mcpp-toml.md
index 37d9c4b93..a9f42ed6b 100644
--- a/docs/zh/04-mcpp-toml.md
+++ b/docs/zh/04-mcpp-toml.md
@@ -1,19 +1,22 @@
# 04 —— mcpp.toml 工程文件指南
-**读者:**正在写或正在读一份 manifest 的作者。
+**读者:**编写或阅读一份 manifest 的作者。
-**本章回答的那一个问题:**一份 `mcpp.toml` 可以说什么,逐字段地。
+**本章回答的那一个问题:**一份 `mcpp.toml` 可以逐字段写下什么。
-**不在这里:**四个主题的表虽然写在这个文件里,但本章不拥有它们 —— 依赖是
-[05](05-dependencies.md),feature 是 [06](06-features-and-capabilities.md),
-以目标为条件是 [22](22-target-side.md),工程的环境是
-[23](23-the-project-environment.md)。每一处都在它的表本该出现的位置点名。
+**不在这里:**本文件里的这些表分属四个主题,但都不归本章拥有——依赖属于
+[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` 会自动发现并读取它。
+`mcpp.toml` 是 mcpp 构建工具的工程配置文件,类似于 Cargo 的 `Cargo.toml` 或
+Node 的 `package.json`。把它放在工程根目录;`mcpp build` 会自动发现并读取它。
## 1. 最小化示例
-mcpp 的设计原则是 **约定优于配置** —— 大多数字段都有合理默认值,最简单的 `mcpp.toml` 只需几行:
+mcpp 按**约定优于配置**设计——大多数字段都有合理的默认值,因此最简单的
+`mcpp.toml` 只需要几行:
### 1.1 可执行程序(最简)
@@ -23,11 +26,11 @@ name = "hello"
version = "0.1.0"
```
-mcpp 自动推断:
-- 源文件: `src/**/*.{cppm,cpp,cc,c,S,s,asm}`
-- 入口: `src/main.cpp` → 生成 `hello` 二进制
-- 标准: C++23
-- 模块: 扫描 `export module ...` 声明自动建立依赖图
+mcpp 自动推断:
+- 源文件:`src/**/*.{cppm,cpp,cc,c,S,s,asm}`
+- 入口点:`src/main.cpp` → 产出 `hello` 二进制
+- 标准:C++23
+- 模块:扫描 `export module ...` 声明并自动构建依赖图
### 1.2 库项目(最简)
@@ -40,55 +43,72 @@ version = "0.1.0"
kind = "lib"
```
-lib-root 约定:主模块接口默认在 `src/mylib.cppm`(包名的最后一段)。
+库根约定:主模块接口默认是 `src/mylib.cppm`(包名的最后一段)。
## 2. 完整字段参考
-### 2.1 `[package]` — 包元数据
+### 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" # 仓库地址(可选)
+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` 是 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)。
-
-`[package.metadata.]`(mcpp 2026.9.16.1+)是引擎保留但不解释的表。它是包对自身的
-陈述,供读取它的工具使用,例如收集每个库贡献内容的框架;它通过 `mcpp::graph_file()` 到达根包
-的构建程序([30 —— build.mcpp](30-build-mcpp.md))。其中的路径由读取方相对于该包的清单目录
-解析。`[package]` 中 mcpp 不读取的其他键会被报告,与 `[build]` 一致:给出警告,在 `--strict`
-下报错(2026.9.16.1+)。
+`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 方言;这个选择会进入指纹与 std
+ BMI 的缓存键。
+- `c++latest`:解析为所用工具链支持的最新标准档位。适合本地实验,但不
+ 推荐用在要求可复现的发布包上。
+- `c++fly`:`c++latest` **加上所用工具链能启用的每一项实验性标准特性**
+ (语言特性加标准库)。在 GCC ≥ 16 上会打开 C++26 反射(`-freflection`)
+ 与 contracts;在 Clang/libc++ 上会加上 `-fexperimental-library`;工具链
+ 不支持的开关会被跳过并打印一份摘要。它刻意依赖具体工具链——这是最
+ 前沿的试验场模式,绝不用于已发布的包。
+
+两条值得了解的性质:
+
+- **标准是模块图全局的。** 根包的 `standard` 适用于构建中的每一个翻译
+ 单元,包括依赖——依赖自身的 `standard`,在它作为依赖被构建时不会被
+ 使用。这不是一种简化:BMI 在不同档位之间不兼容(GCC 会报
+ `language dialect differs` 拒绝它),所以一张图在物理上无法同时容纳
+ 两个档位。
+- **档位之间从不共享缓存。** 标准是指纹、`import std` BMI 身份与依赖
+ 构建缓存键的一部分,所以在 `c++20` 与 `c++23` 之间切换,会让每个档位
+ 拿到自己独立的 target 目录与自己独立的 std BMI,而不是一次损坏的命中。
+
+若源码在所用工具链未提供 `std` 模块的档位上写了 `import std;`,mcpp 会
+在编译之前失败,并同时点出工具链与工程档位两者。
+
+字段值的两种写法都被接受:`standard = "c++26"` 与 `standard = 26`。
+
+当**某个依赖声明的档位高于全图的档位**时,mcpp 会在编译之前说明这一点,
+而不是任由构建在那个依赖的源码内部某处失败。见
+[workspace §4.2](07-workspace.md)。
+
+`[package.metadata.]`(mcpp 2026.9.16.1+)是一张引擎保留、但不解读
+的表。它是这个包对自身的陈述,供读取它的工具使用——例如某个框架收集
+每个库贡献了什么——并通过 `mcpp::graph_file()` 到达根包的构建程序
+([30 —— build.mcpp](30-build-mcpp.md))。表中的路径由那个读取者相对包的
+manifest 目录解析。`[package]` 中 mcpp 不读取的任何其它键都会被报告,与
+`[build]` 中的处理一致:一条警告,在 `--strict` 下则是错误
+(2026.9.16.1+)。
```toml
[package.metadata.demo]
@@ -97,22 +117,23 @@ resources = "res"
#### 方言标志与 `import std` BMI
-有些标志会改变标准库头文件**声明出什么**,因此预编译的 `import std` BMI 也必须带着它们一起
-构建。这就是 `[build] dialect_cxxflags` 的用途:它会被施加到 std BMI 预编译、模块扫描
-**以及**图中每一个 TU(依赖也包括在内)。
+有些标志会改变标准库头文件的声明内容,所以预编译的 `import std` BMI 也
+必须用它们一起构建。这正是 `[build] dialect_cxxflags` 的用途:它会应用
+于 std BMI 的预构建、模块扫描,**以及**图中的每一个翻译单元,包括依赖。
```toml
[build]
dialect_cxxflags = ["-fno-exceptions"]
```
-其中少数几个标志,mcpp 在 `cxxflags` 里发现时会自动提升进这条通道
-(`-freflection`、`-fchar8_t`、`-D_GLIBCXX_USE_CXX11_ABI=…`)—— 混用这些标志的图本来就是
-病态的,任何依赖都不可能对它们持有另一种自洽的意见。
+当 mcpp 在 `cxxflags` 中发现某些标志时(`-freflection`、`-fchar8_t`、
+`-D_GLIBCXX_USE_CXX11_ABI=…`),会自动把它们提升进那个通道——一张混用
+了这些标志的图本来就是病态的,所以没有哪个依赖能对它们持不同意见。
-`-fno-exceptions` 与 `-fno-rtti` **不会**被自动提升,因为依赖可以合法地不同意:它们移除的是
-依赖可能正在使用的语言设施,而消费者无权替它做这个决定。留在 `cxxflags` 里,它们会到达每一个
-TU 却到不了预编译,于是构建不可能成功 —— mcpp 在编译前就拒绝,并指出该用哪个键:
+`-fno-exceptions` 与 `-fno-rtti` **不会**被提升,因为依赖可以合理地持
+不同意见:它们移除了依赖可能要用到的语言设施,而消费者无权替依赖做这个
+选择。若留在 `cxxflags` 里,它们会到达每一个 TU 却到不了预构建,导致
+构建无法成功——mcpp 会在编译之前拒绝,并点名这个键:
```
error: `-fno-exceptions` changes the language dialect, but the `import std` BMI is
@@ -124,43 +145,46 @@ error: `-fno-exceptions` changes the language dialect, but the `import std` BMI
dialect_cxxflags = ["-fno-exceptions"]
```
-这项检查读的是**生效后的**标志集合,所以同一个标志写在 `[profile.] cxxflags` 或
-`[target.…]` 块里同样会被抓到。而当图中根本没有 `import std` 时它不触发 —— 那里它就是一个
-正常工作的按 TU 选项。
+这项检查读取的是**生效**标志,所以同一个标志写在
+`[profile.] cxxflags` 或某个 `[target.…]` 块里也会触发。当图里
+没有任何单元 import `std` 时不会触发,此时该标志只是一个正常生效的按
+单元选项。
-### 2.2 `[targets.]` — 构建目标
+### 2.2 `[targets.]` —— 构建目标
```toml
-# 可执行程序(默认,有 src/main.cpp 时自动推断)
+# Executable (default; inferred automatically when src/main.cpp exists)
[targets.myapp]
kind = "bin"
-main = "src/main.cpp" # 可选,默认 src/main.cpp
+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" # 可选: Linux/ELF ABI 名称,运行时会生成同名 alias
+soname = "libmylib.so.1" # Optional: Linux/ELF ABI name; an alias of the same name is generated at runtime
```
-`soname` 用于共享库的 ABI 名称,类似 Autotools/CMake 中的
-`SOVERSION`/`SONAME`。在 Linux 上,mcpp 会向链接器传递
-`-Wl,-soname,`,并在输出目录生成 ` -> lib.so` alias,
-让下游程序可通过标准 ABI 名称 `DT_NEEDED` 或 `dlopen()` 加载该库。
-该字段只对 `kind = "shared"` 有效,值必须是文件名 basename。未声明 `soname`
-的 ELF 共享库以输出文件名作为 SONAME(2026.9.14.2+),这正是消费者已经记录在
-`DT_NEEDED` 中的名字;bionic 自 API level 23 起要求共享库带有 SONAME。
+`soname` 是共享库的 ABI 名,相当于 Autotools/CMake 里的
+`SOVERSION`/`SONAME`。在 Linux 上,mcpp 会向链接器传入
+`-Wl,-soname,`,并在输出目录生成一个 ` -> lib.so`
+的别名,使下游程序能通过 `DT_NEEDED` 或 `dlopen()`,按其标准 ABI 名加载
+这个库。这个字段只对 `kind = "shared"` 生效,取值必须是一个文件名
+(不含路径)。一个未声明 `soname` 的 ELF 共享库,会把自己的输出文件名
+记为自己的 SONAME(2026.9.14.2+)——这正是它的消费者已经记录在
+`DT_NEEDED` 里的那个名字;bionic 从 API level 23 起要求必须有这个名字。
-共享库目标在三种二进制格式上都可用。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`。
+共享库目标在全部三种二进制格式上都能工作。ELF 得到一个带 `soname` 和
+`$ORIGIN` 搜索路径的 `.so`;Mach-O 得到一个 install name 为
+`@rpath/` 的 `.dylib`,因此即使被移动位置也仍然有效;PE 同时得到
+loader 打开的 `.dll` 与链接器消费的导入库,导出列表从对象文件按 MSVC
+ABI 生成(不带 `__declspec(dllexport)` 或 `.def` 时什么都不导出)。见
+`tests/e2e/08`、`257` 与 `259`。
-#### `kind = "app"` —— 用户启动的那个东西(mcpp 2026.9.12.3+)
+#### `kind = "app"` —— 用户启动的那个东西(mcpp 2026.9.12.3+)
```toml
[targets.myapp]
@@ -168,92 +192,102 @@ kind = "app"
main = "src/main.cpp"
```
-`app` 在每一行上表达同一件事——用户启动的程序——而每一行为它提供各自的文件:
+`app` 在每一行上命名的是同一个事实——用户启动的那个程序——而每一行
+为它提供自己的文件形式:
-| 行 | `app` 的形态 | 文件 |
+| 行 | `app` 的形式 | 文件 |
|---|---|---|
-| ELF、PE、Mach-O 各行,`wasm32-emscripten` | 与 `bin` 相同 | `myapp`、`myapp.exe`、`myapp.js` |
+| ELF、PE、Mach-O 各行,`wasm32-emscripten` | 与 `bin` 相同 | `myapp`、`myapp.exe`、`myapp.js` |
| `*-linux-android` | 与 `shared` 相同 | `libmyapp.so` |
-在 `*-linux-android` 上,平台把应用程序作为共享库加载进一个 Java 进程
-(`System.loadLibrary("myapp")`、manifest 里的 `android:name`);这一行上不存在
-应用程序的可执行形态。在其余每一行上,`app` 的链接方式与 `bin` 完全相同,产物
-与 `bin` 目标逐字节相同。
+在 `*-linux-android` 上,平台把一个应用当作共享库加载进一个 Java 进程
+(`System.loadLibrary("myapp")`,manifest 里的 `android:name`);这一行上
+没有可执行文件形式的应用。在其余每一行上,`app` 的链接方式与 `bin`
+完全相同,产出的文件与 `bin` 目标的产物逐字节相同。
-`main` 在每一行上保持同一含义:它指出定义入口点的翻译单元。在 `app` 是可执行文件
-的那些行上,该入口就是 `main` 本身。在 `*-linux-android` 上,这个文件被编译为共享库
-的一个翻译单元,平台自己的入口(`ANativeActivity_onCreate`,或它声明的 JNI 导出)
-是平台的契约,不是 mcpp 指定的名字。`exports`(见上)对 `app` 目标的适用方式与对
-`shared` 完全相同;`windows_subsystem` / `windows_entry`(见下)接受 `app` 的方式
-与接受 `bin` 完全相同。
+`main` 在每一行上都保持同一个含义:它命名定义入口点的翻译单元。当
+`app` 是一个可执行文件时,那个入口就是 `main` 本身。在 `*-linux-android`
+上,这个文件被编译为共享库的一个翻译单元,平台自己的入口
+(`ANativeActivity_onCreate`,或它声明的 JNI 导出)是平台自己的契约,
+不是 mcpp 分配的名字。`exports`(见上)对 `app` 目标的适用方式与对
+`shared` 目标完全相同,`windows_subsystem` / `windows_entry`(见下)
+接受 `app` 的方式也与接受 `bin` 完全相同。
-在 `app` 的形态是共享库的那一行上,不带 `--format` 运行 `mcpp run` 会被拒绝,拒绝信息
-指出该旗标以及已解析图提供的格式集合。`mcpp pack --format apk` 把这个库放到闭包已经
-安放共享对象的位置。参见 [10 — 打包与发布](10-pack-and-release.md)里的 `mcpp run
---format`。
+在其形式为共享库的那一行上,`mcpp run` 一个 `app` 目标,若不带
+`--format` 会拒绝执行,并点名这个旗标与所解析出的图提供的那些格式。
+`mcpp pack --format apk` 会把这个库放到闭包已经放置共享对象的位置。见
+[10 —— 打包与发布](10-pack-and-release.md)中的 `mcpp run --format`。
-早于 2026.9.12.3 的引擎不认识这个取值,按名字拒绝,并列出它认识的三种:
+早于 2026.9.12.3 的引擎不认识这个取值,会拒绝并点名它认识的三种:
```
targets.myapp.kind must be 'bin', 'lib' or 'shared'; got 'app'
```
-#### `exports` —— 产物发布的符号集合(mcpp 2026.9.6.5+)
+#### `exports` —— 产物发布的符号集合(mcpp 2026.9.6.5+)
```toml
[targets.mydriver]
kind = "shared"
soname = "libmydriver.so.1"
-exports = "abi/mydriver.exports" # 或内联:exports = ["vk_icd*"]
+exports = "abi/mydriver.exports" # or inline: exports = ["vk_icd*"]
```
-**不写这个键就发布全部,而那正是两个平台今天的默认**——ELF 给符号默认可见性,PE 会
-自动生成列出全部符号的 `.def`。`exports` 把它收窄。
+**省略这个键会发布一切,而这恰好是两个平台本来就在做的事**——ELF 给
+符号默认可见性,PE 自动生成一份列出每个符号的 `.def`。`exports` 收窄
+这个范围。
-两类工程需要收窄。**有稳定 ABI 的运行时**只发布一份经过评审的集合,不在集合里的东西
-才保持可改。**与同类并存的插件**不能撞名:Vulkan loader 按名字找
-`vk_icdGetInstanceProcAddr`,一个把内部符号也导出的 ICD 会与 loader 以及同进程内另一个
-ICD 相撞。
+两类工程需要这种收窄。**带稳定 ABI 的运行时**只发布一份经过审查的
+集合,其余一概不发布,让不在集合里的东西保留自由变化的空间。**与同类
+插件并存加载的插件**不能相撞:一个 Vulkan ICD 是按名字被找到
+`vk_icdGetInstanceProcAddr` 的,若它同时导出自己的内部符号,就会与
+loader 以及进程中的其它 ICD 相撞。
-文件一行一条符号模式,`#` 起注释,`*` 是唯一的通配符。内联数组说的是同一件事,用于
-只有两三个入口、单开一个文件反而是仪式的场合。
+这个文件每行列一个符号模式,`#` 起一行注释,`*` 是唯一的通配符。内联
+数组说的是同一件事,用于只有两三个入口点、单独开一个文件显得多余的场合。
-一句话,三种渲染:
+一句陈述,三种渲染:
| 平台 | 渲染为 |
|---|---|
-| ELF | version script,`-Wl,--version-script=` |
-| Mach-O | `-Wl,-exported_symbols_list`(前导下划线由引擎补) |
-| PE | `.def`,取代自动生成的全导出版本 |
+| ELF | 一份 version script,`-Wl,--version-script=` |
+| Mach-O | `-Wl,-exported_symbols_list`(前导下划线由引擎补上) |
+| PE | 那份 `.def`,替换自动生成的、导出一切的那一份 |
-**它不改变编译期可见性,这是有意的。** 三种格式上收窄都是链接期属性,所以一个键只有
-一个效果。`-fvisibility=hidden` 仍可经 `[build] cxxflags` 使用以取得代码生成上的收益,
-而它是一个**单独**的决定,因为它同时改变本库各翻译单元之间如何看见彼此。
+**它不改变编译期可见性,这是刻意的。** 这种收窄在全部三种格式上都是
+链接期属性,所以一个键只有一种效果。`-fvisibility=hidden` 仍可通过
+`[build] cxxflags` 使用,以获取它带来的代码生成收益,它是一个独立的
+决定,因为它同时改变这个库自己的翻译单元之间彼此可见的方式。
-**符号版本化不是这个键。** `foo@@LIB_1.0` 与 `foo@LIB_0.9` 并存是 ELF 独有的能力,
-无法中立表达;需要它的包自己写 version script 经 `[build] ldflags` 传入,或者算出来后
-用 `mcpp:link-flag=` 发出(docs/07)。
+**符号版本化不是这个键管的事。** `foo@@LIB_1.0` 与 `foo@LIB_0.9` 并存,
+是一种只有 ELF 才有、无法中立表达的能力;需要它的包自己写 version
+script 并通过 `[build] ldflags` 传入,或者自行计算并发出
+`mcpp:link-flag=`(docs/07)。
-`soname` 对 `kind = "lib"` 同样有意义 —— 见下文的 `dependency_linkage`,
-库以何种形态出现是**消费者**的决定。
+`soname` 在 `kind = "lib"` 上同样有意义——见下文的
+[`dependency_linkage`](#dependency_linkage--静态还是动态由消费者决定),
+在那里,一个库采取的形式变成消费者的决定。
-#### `windows_subsystem` 与 `windows_entry` —— Windows GUI 可执行文件(mcpp 2026.9.12.2+)
+#### `windows_subsystem` 与 `windows_entry` —— Windows GUI 可执行文件(mcpp 2026.9.12.2+)
```toml
[targets.myapp]
kind = "bin"
main = "src/main.cpp"
-windows_subsystem = "windows" # "console"(默认)| "windows"
-windows_entry = "main" # "main"(默认)| "wmain" | "WinMain" | "wWinMain"
+windows_subsystem = "windows" # "console" (default) | "windows"
+windows_entry = "main" # "main" (default) | "wmain" | "WinMain" | "wWinMain"
```
-PE 可执行文件记录一个子系统。`"console"` 为程序附加控制台,`"windows"` 产生启动时不带控制台的
-GUI 程序。`windows_entry` 指程序定义的函数,而不是调用该函数的启动符号;它与子系统相互独立:控制台
-程序可以定义 `wmain`,GUI 程序也可以保留可移植的 `int main()`。
+一个 PE 可执行文件记录一个 subsystem。`"console"` 附带一个控制台,
+`"windows"` 产出一个启动时没有控制台的 GUI 程序。`windows_entry` 命名
+的是程序自己定义的函数,不是调用它的启动符号,它独立于 subsystem 之外:
+一个控制台程序可以定义 `wmain`,一个 GUI 程序也可以保留可移植的
+`int main()`。
-这两个键是字段而不是链接标志,原因是正确的标志取决于 ABI,而一条标志无法说明自己面向哪个 ABI:
+这两个键之所以是字段而不是链接旗标,是因为正确的旗标取决于 ABI,而一个
+旗标说不出它面向哪个 ABI:
-| `windows_subsystem` / `windows_entry` | MSVC ABI(cl、clang-cl、面向 `*-windows-msvc` 的 clang) | GNU ABI(MinGW gcc、面向 `*-windows-gnu` 的 clang) |
+| `windows_subsystem` / `windows_entry` | MSVC ABI(cl、clang-cl、目标为 `*-windows-msvc` 的 clang) | GNU ABI(MinGW gcc、目标为 `*-windows-gnu` 的 clang) |
|---|---|---|
| `"console"` / `"main"` | 无 | 无 |
| `"windows"` / `"main"` | `/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup` | `-mwindows` |
@@ -261,121 +295,137 @@ GUI 程序。`windows_entry` 指程序定义的函数,而不是调用该函数
| `"windows"` / `"wWinMain"` | `/SUBSYSTEM:WINDOWS /ENTRY:wWinMainCRTStartup` | `-mwindows -municode` |
| `"console"` / `"wmain"` | `/SUBSYSTEM:CONSOLE /ENTRY:wmainCRTStartup` | `-municode` |
-在 MSVC ABI 上,只要任一键偏离默认值,两条标志就都写出。原因是链接器在缺少其中一条时由另一条推断:
-单独的 GUI 子系统会选择 `WinMainCRTStartup`,而可移植的 `int main()` 无法满足它;`/ENTRY:main`
-则会跳过 CRT 初始化,静态构造也随之被跳过。GNU 风格的驱动收到的 MSVC ABI 标志形如
-`-Wl,/SUBSYSTEM:...`。
+在 MSVC ABI 上,只要两个键中任意一个偏离其默认值,两个旗标就都会被
+写出,因为链接器在其中一个缺席时会从另一个推断:单独的 GUI subsystem
+会选中 `WinMainCRTStartup`,而一个可移植的 `int main()` 满足不了它;
+`/ENTRY:main` 会跳过 CRT 初始化,包括静态构造函数。GNU 风格的驱动程序
+以 `-Wl,/SUBSYSTEM:...` 的形式接收 MSVC-ABI 旗标。
-这两个键只到达声明它们的目标的链接。同一包的其他可执行文件、`mcpp test` 的测试二进制以及该包的消费者
-都保持控制台子系统;这也是这些标志不应写进 `[build] ldflags` 的原因:该通道到达图中的每一次链接。
-在 ELF、Mach-O 与 WebAssembly 上,这两个键不产生任何标志,产物与未声明它们时逐字节相同,因此跨平台的
-manifest 不需要 `cfg` 块。库目标声明任一键会被拒绝,拒绝信息指出目标与键名。
+这两个键只到达声明它们的目标的链接,不会传播出去。第二个可执行文件、
+`mcpp test` 的二进制,以及这个包的消费者,都保持控制台 subsystem——这
+正是为什么这些旗标不应放进 `[build] ldflags`:那条通道会到达图中的每
+一次链接。在 ELF、Mach-O 与 WebAssembly 上,这两个键不渲染出任何东西,
+产物与未写它们时逐字节相同,所以一份跨平台 manifest 不需要 `cfg` 块。
+一个库目标若声明这两个键中的任意一个都会被拒绝,拒绝信息点名该目标与
+该键。
-构建程序通过 `mcpp::windows_subsystem("", "windows")` 与
-`mcpp::windows_entry("", "wmain")` 为本包的可执行文件设置同样的字段
-([build.mcpp](30-build-mcpp.md))。
+一个构建程序可以为自己包里的某个可执行文件设置同样的字段,用
+`mcpp::windows_subsystem("", "windows")` 与
+`mcpp::windows_entry("", "wmain")`([build.mcpp](30-build-mcpp.md))。
-应用程序包、应用程序清单与 DPI 感知不属于这两个键,它们归属于打包格式与 `[resources]`。
+应用程序包(bundle)、应用程序 manifest 与 DPI 感知不属于这两个键管的
+范围;它们属于打包格式与 `[resources]`。
-#### 按目标的键(per-target keys)
+#### 按目标的键(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 标志
+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"] # 仅当 feature `gui` 激活时才构建
+required_features = ["gui"] # only built when feature `gui` is active
```
| 键 | 含义 |
|---|---|
-| `defines` | 预处理宏(`name` 或 `name=value`),脱糖为 `-D`,作用于该目标入口的 C 与 C++ 编译。 |
-| `cxxflags` / `cflags` | 该目标的额外编译标志。**不要**放 `-std=...`——用 `[package].standard`。 |
-| `required_features` | 仅当列出的 feature **全部**激活时才生成该目标,否则静默跳过。只是门禁——不激活 feature(用 `--features` / `[features].default`)。 |
-| `windows_subsystem` *(2026.9.12.2+)* | 可执行文件的 PE 子系统:`"console"`(默认)或 `"windows"`(启动时不带控制台的 GUI 程序)。只到达该目标的链接,在非 PE 目标上不产生任何标志。见上一节。 |
-| `windows_entry` *(2026.9.12.2+)* | 程序定义的入口函数:`"main"`(默认)、`"wmain"`、`"WinMain"` 或 `"wWinMain"`。见上一节。 |
-| `linkage` *(2026.9.15.2+)* | 库目标的**默认**链接形态,`"static"` 或 `"shared"`:不写 `linkage` 的消费者得到的形态。它不同于 `kind = "shared"`,不是约束,因此消费者的显式陈述会被遵从。与 `kind = "shared"` 同写或写在程序目标上会被拒绝。见 [`dependency_linkage`](#dependency_linkage--静态还是动态由消费者决定)。 |
-
-> **作用域(重要):** 目标上的 `defines` / `cxxflags` / `cflags` **只作用于该目标独占的入口源**
-> (它的 `main`)——**绝不**作用于共享的模块/实现对象(那些只编译一次、被每个目标链接,即 mcpp 的
-> compile-once 模型)。当标志只需影响某个二进制(或测试)**自己的入口**时,这正是合适的工具 ——
-> 例如某个测试的 `main` 里触发契约违规、需要按测试设置契约求值语义
-> (`-fcontract-evaluation-semantic=observe`),或入口独享的 feature 宏、局部告警抑制。
-> 若标志必须穿透**共享**代码,就不该放在这里 —— 改用 [workspace](07-workspace.md) member 或
-> `[features]`;若是整次构建的模式,用 `[profile.*]`(`mcpp test --profile ` 会让包括被测
-> 代码在内的整个测试镜像都在该 profile 下编译)。
+| `defines` | 预处理宏(`name` 或 `name=value`);在 C 与 C++ 两种入口编译上都脱糖为 `-D`。 |
+| `cxxflags` / `cflags` | 这个目标专用的额外编译旗标。**不要**把 `-std=...` 写在这里——用 `[package].standard`。 |
+| `required_features` | 只有当构建中**每一个**列出的 feature 都被激活时,这个目标才会被产出;否则被静默跳过。它只是一道闸——不会激活 feature(用 `--features` / `[features].default`)。**一个例外,但它不是第二条规则:** 当这个目标作为 host 工具被请求时(`tools = [...]`,§2.14),这个目标就是被**请求**的那一个,于是它的 `required_features` 变成子构建的**输入**。同一个字段、同一个含义——只是解析的方向反过来了。 |
+| `windows_subsystem` *(2026.9.12.2+)* | 可执行文件的 PE subsystem:`"console"`(默认)或 `"windows"`,一个启动时没有控制台的 GUI 程序。只到达这个目标的链接,别处不受影响,在非 PE 的目标上不渲染任何东西。见上一节。 |
+| `windows_entry` *(2026.9.12.2+)* | 程序定义的入口函数:`"main"`(默认)、`"wmain"`、`"WinMain"` 或 `"wWinMain"`。见上一节。 |
+| `linkage` *(2026.9.15.2+)* | 一个库目标的**默认**链接形态,`"static"` 或 `"shared"`:不写 `linkage` 的消费者得到的形态。与 `kind = "shared"` 不同,它不是约束,因此消费者的显式陈述会被遵从。与 `kind = "shared"` 同写,或写在程序目标上,都会被拒绝。见[`dependency_linkage`](#dependency_linkage--静态还是动态由消费者决定)。 |
+
+> **范围(重要):** 目标上的 `defines` / `cxxflags` / `cflags` **只**
+> 应用于该目标专属的入口源文件(它的 `main`)——绝不应用于共享的
+> 模块/实现对象,后者只编译一次,并链接进每一个目标(mcpp 的一次编译
+> 模型)。当一个旗标只需要影响单个二进制(或测试)自己的入口时,这两个
+> 键是正确的工具——例如某个测试的 `main` 专门触发违规,需要按测试设置
+> contract 求值语义(`-fcontract-evaluation-semantic=observe`);或者
+> 只有入口会读到的一个 feature 宏;或者一次局部的警告抑制。如果一个
+> 旗标必须到达**共享**代码,就不属于这里——要么拆成一个
+> [workspace](07-workspace.md) 成员,要么用 `[features]`;若是整个构建
+> 范围的模式,用 `[profile.*]`(`mcpp test --profile ` 会在那个
+> profile 下构建整份测试镜像,包括被测代码)。
>
-> `[targets.]` 下的不支持键会产生 warning(`--strict` 下为 error)。
+> `[targets.]` 下不受支持的键会被报告为警告(`--strict` 下为
+> 错误)。
-**构建配置该放哪** —— 当多个二进制需要不同配置时:
+**构建配置该放在哪里**——当不止一个二进制必须有所不同时:
-| 目标 | 使用 |
+| 目的 | 做法 |
|---|---|
-| 某二进制**自己入口**上的不同宏/标志 | 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`。
+| 二进制**自己入口**上不同的宏/旗标 | 按目标的 `defines` / `cxxflags`(见上) |
+| 两个产物在**共享**代码上有差异 | 拆成 [workspace](07-workspace.md) 成员,各自在共享的 `lib` 之上有自己的 `[build]` 旗标 |
+| **选择一个变体**的共享库(例如某个后端) | 用该库上的 `[features]`(§2.8)——是增量的,到达库自己的编译 |
+| **整个构建范围**的模式(sanitizer、contract 语义、优化档位) | `[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 各阶段、HLSL、OpenCL C 与
+> Metal,完整列表见[42 —— 异构构建](42-heterogeneous-builds.md)——从不
+> 由引擎编译;它们以 `MCPP_DEVICE_SOURCES` 的形式到达构建程序,工程
+> import 的规则包把每一个都变成一个 `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(见下节)
+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)
```
#### 编译 flag 的写法 *(mcpp 2026.9.17.1+)*
-`cflags`、`cxxflags` 与 `asmflags` 的一个元素代表一个或多个编译器参数(下称「词」)。
-写法在每个宿主上相同,也不论列表写在哪里:`[build]`、`[targets.]`、`flags` 的
-glob 条目、feature、`[target..build]`、xpkg 描述符,以及构建程序的
+`cflags`、`cxxflags` 或 `asmflags` 里的一个元素代表一个或多个编译器
+参数(「词」)。这套语法在每个宿主上都相同,无论写在哪张表里:
+`[build]`、`[targets.]`、`flags` glob 条目、feature、
+`[target..build]` 小节、xpkg 描述符,以及构建程序的
`mcpp:cflag=` / `mcpp:cxxflag=` 指令。
| 写法 | 编译器收到的词 |
@@ -383,361 +433,411 @@ glob 条目、feature、`[target..build]`、xpkg 描述符,以及构
| `"-O2 -g"` | `-O2`、`-g` |
| `"-include config.h"` | `-include`、`config.h` |
| `"'-DNAME=a b'"` 或 `"-I\"my dir\""` | `-DNAME=a b`、`-Imy dir` |
-| `"-DNAME=long long"` | `-DNAME=long long`(见下) |
-| `"-DNAME=\\\"text\\\""` | `-DNAME="text"`(字符串字面量) |
+| `"-DNAME=long long"` | `-DNAME=long long`(见下) |
+| `"-DNAME=\\\"text\\\""` | `-DNAME="text"`(一个字符串字面量) |
| `"-I/opt/my\\ dir/include"` | `-I/opt/my dir/include` |
| `"-IC:\\sdk\\include"` | `-IC:\sdk\include` |
| `"-DNAME=a$b"` | `-DNAME=a$b` |
-规则陈述在元素的文本上(TOML 或 Lua 先去掉它们自己的转义):
+以下规则施加于元素的文本上(TOML 或 Lua 已经去掉了它自己的转义之后):
-- 未加引号的空格与制表符分隔词;
-- `'...'` 按字面取到下一个 `'`;
-- `"..."` 按字面取,只有 `\"` 与 `\\` 分别代表 `"` 与 `\`;
-- 引号之外,反斜杠后跟空格、制表符、`"`、`'` 或 `\` 时代表该字符,其余反斜杠按字面;
-- 相邻的带引号与不带引号的片段组成一个词;
-- `$`、`*`、`;`、`|` 等 shell 运算符没有特殊含义;
-- 以 `-D` 或 `/D` 开头且含空格的元素是一个词,按原样取,与此前各版本相同。
+- 不带引号的空格与制表符分隔词;
+- `'...'` 是字面量,直到下一个 `'`;
+- `"..."` 是字面量,只有 `\"` 与 `\\` 分别代表 `"` 与 `\`;
+- 在引号之外,反斜杠之后紧跟空格、制表符、`"`、`'` 或 `\`,代表那个
+ 字符本身;其它反斜杠都是字面量;
+- 相邻接触的带引号与不带引号的片段合并成一个词;
+- `$`、`*`、`;`、`|` 以及其它 shell 操作符没有特殊含义;
+- 一个以 `-D` 或 `/D` 开头并含有空格的元素,视为一个词,按字面接受,
+ 与以往每一个版本相同。
-`defines` 的一个条目是一个值,不按此写法读取:`defines = ["NAME=\"text\""]` 传入的是
-一个词 `-DNAME="text"`。`ldflags`、`dialect_cxxflags` 与 `std-module-flags` 不在本节范围内。
+一条 `defines` 条目是一个值,不受这套语法解析:`defines =
+["NAME=\"text\""]` 传出单独一个词 `-DNAME="text"`。`ldflags`、
+`dialect_cxxflags` 与 `std-module-flags` 不受本节约束。
-`compile_commands.json` 与 `mcpp emit build-database` 在 `arguments` 中列出同样的词,
-不经 shell 即可执行。
+`compile_commands.json` 与 `mcpp emit build-database` 在 `arguments`
+里列出同样的词,可以不经 shell 直接执行。
-一个工程的首次规划中,若某个元素的词与 2026.9.17.1 之前的版本在同一宿主上传入的参数不同,
-mcpp 以 `build/flag-words` 警告并给出两者。重复同一规划的构建不再重复该警告。
+在一个工程的首次 plan 时,若某个元素的词与 2026.9.17.1 之前的版本在
+同一宿主上传出的参数不同,mcpp 会在 `build/flag-words` 下发出警告,并
+点名两者。重复同一份 plan 的构建不会重复这条警告。
#### `dependency_linkage` —— 静态还是动态由消费者决定
```toml
[build]
-dependency_linkage = "shared" # 全图默认;缺省即 "static"
+dependency_linkage = "shared" # whole-graph default; "static" is the default default
[profile.dev]
-dependency_linkage = "shared" # 按 profile 覆盖
+dependency_linkage = "shared" # per profile
[dependencies]
-"compat.zlib" = { version = "1.3.2", linkage = "shared" } # 单个包
+"compat.zlib" = { version = "1.3.2", linkage = "shared" } # one package
```
-在 mcpp 2026.8.28.2 之前,一个依赖只有一种形态,而且由**包作者**定死:
-`kind = "lib"` 把它的对象并进每个消费者的链接,`kind = "shared"` 产出真正的
-共享库。这个决定放错了位置。一个库在运行期该不该是独立文件,是**被构建的那个
-程序**的性质 —— 它怎么分发、多久重链一次、进程里是不是已经有人提供了这个库。
+在 mcpp 2026.8.28.2 之前,一个依赖恰好只有一种形态,并且由**包作者**
+选择:`kind = "lib"` 把它的对象合并进每个消费者的链接,`kind = "shared"`
+产出一个真正的共享库。这个决定的归属者选错了。一个库在运行时是否应该
+是一个独立文件,是**正在被构建的那个程序**的属性——它怎样被发布、多久
+被重新链接一次、进程里是否已经有别的东西提供了这个库。
-- **`static`**(默认)—— 依赖的对象并进使用它的映像。与 mcpp 一直以来的行为
- 逐字节相同;不写这个键的工程构建结果不变。
-- **`shared`** —— mcpp 把依赖构建成产物旁边的共享库并链接它,由 `$ORIGIN`
- (ELF)/ `@loader_path`(Mach-O)/ 可执行文件自身目录(PE)保证构建目录
- 移动后仍能找到它。
+- **`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"` 会被
-拒绝,并说明原因。
+**这不是 `[target.].linkage`**(§2.7.1)。那个键回答的是一个
+听起来相似、但关于 **C 库**的问题(musl 的 `-static` 链接、MSVC
+的 `/MT`)。这两者不是独立的,而且方向很重要:一个完全静态的镜像没有
+解释器,因此根本无法加载共享对象。在一个 C 库以静态方式链接的目标
+上——这是**musl 的默认行为**——`dependency_linkage = "shared"` 会被
+拒绝,并说明原因。
-**包可以声明它必须是某一种形态**,而且只在确有理由时:
+**一个包可以声明它必须是某一种形态**,但只能出于真实的理由:
-| 包写了 | mcpp 读作 |
+| 包写的内容 | mcpp 的解读 |
|---|---|
-| `[targets.] kind = "shared"` | *必须* shared —— 进程里会有别人 `dlopen` 它,因此只能有一份(X11、Vulkan loader) |
-| `[target..targets.] kind = "shared"` *(2026.9.14.2+)* | 在选择器命中的行上*必须* shared,其余行两种形态都可以([22 —— 目标侧](22-target-side.md)) |
-| `ldflags` 里含 `-L` | *必须* static —— 包携带了 mcpp 没有编译的预构建归档,放不进 mcpp 自己构建的共享对象 |
-| 分发包(`mcpp pack`) | 它实际随包的那些腿,取自 `[[runtime.artifacts]] role` |
-| 其他 | 两种形态都可以 |
+| `[targets.] kind = "shared"` | **必须**是共享的——进程里另有别的东西会 `dlopen` 它,所以只能有一份拷贝(X11、Vulkan loader) |
+| `[target..targets.] kind = "shared"` *(2026.9.14.2+)* | 在选择器匹配的那些行上**必须**是共享的,在别处两种形态都可以([22 —— 目标侧](22-target-side.md)) |
+| `ldflags` 中含 `-L` | **必须**是静态的——包发布了 mcpp 没有编译、也无法放进自己构建的共享对象里的预构建归档文件 |
+| 一个已打包的库(`mcpp pack`) | 它实际发布的那几条腿,来自 `[[runtime.artifacts]] role` |
+| 其它任何情况 | 两种形态都可以 |
-`kind = "lib"` **不是**约束:它是默认值,大多数包写下它并没有做任何选择。
-**没有陈述不等于一条陈述。**
+`kind = "lib"` **不是**一种约束:它是默认值,大多数包写它时并没有在做
+选择。没有陈述不等于一种陈述。
-**包还可以陈述一个默认值** *(2026.9.15.2+)*,它不是约束:
+**一个包也可以声明一个默认值**(2026.9.15.2+),这不是约束:
```toml
[targets.fw]
kind = "lib"
-linkage = "shared" # 不写 linkage 的消费者得到的形态
+linkage = "shared" # the form a silent consumer receives
[target.'cfg(env = "android")'.targets.fw]
-linkage = "shared" # 同上,只在选择器命中的行上
+linkage = "shared" # the same, on the rows the selector matches
```
-- 形态按陈述从具体到一般的顺序决定:根工程在该依赖边上的 `linkage`,根工程
- 写下的 `dependency_linkage`(`[build]` 或当前 profile),包的 `linkage`,
- 最后是 `static`。
-- 与包的默认值不同的显式陈述会被遵从。它不是降级,`--strict` 接受它,并由一条
- 信息行(`Linkage`)同时点名两条陈述。
-- 同一张表里同时写 `kind = "shared"` 与 `linkage` 会被拒绝,因为约束没有默认值可言;
- 一行只陈述 `kind` 与 `linkage` 之一,后命中的陈述替换先前的陈述,因此某行的
- `linkage = "static"` 会把无条件表约束为 `shared` 的包恢复为由消费者选择的形态。
-- 目标无法遵从的默认值(完全静态的映像、freestanding 目标)静默回落,因为没有人
- 要求它。
-- 2026.9.15.2 之前的引擎把 `[targets.] linkage` 报为不支持的键(对依赖静默)
- 并静态链接该包;2026.9.14.2 起的引擎拒绝不含 `kind` 的行表。依赖此键的包应陈述
- 这一引擎下限。
-
-根工程的构建程序通过 `mcpp::dep_linkage("name")` 读取每个依赖在本次构建中的形态
-([30 —— build.mcpp](30-build-mcpp.md)),生成的加载入口或导入声明因此跟随同一个决定。
-
-约束拒绝的请求按包允许的形态链接,并给出一条点名包的陈述的警告
-(`its manifest states [targets.fw] kind = "shared", ...`);`--strict` 下该警告
-成为错误。`mcpp why deps` 报告每个依赖的形态及其原因:`default`、
-`package-default`(2026.9.15.2+)、`requested`、`package-kind`、`row-kind`、
-`packaged`、`no-sources`、`prebuilt-inputs`、`no-loader` 或 `static-libc`(2026.9.14.2+)。
-
-依赖边上的 `linkage` 只在**根工程**的 `[dependencies]` 里生效。依赖图深处的包
-无权决定最终程序的布局;真正必须只有一份共享副本的包,应当在自己的 target 上
-声明。
+- 形态的决定按以下顺序、以最具体的陈述优先:根包在这条依赖边上写的
+ `linkage`,根包写出的 `dependency_linkage`(在 `[build]` 或当前生效
+ 的 profile 里),包自己的 `linkage`,以及 `static`。
+- 一处与包默认值不同的显式陈述会被遵从。这不算一种降级,所以
+ `--strict` 接受它,并有一行信息(`Linkage`)点名两处陈述。
+- 在同一张表里同时写 `kind = "shared"` 与 `linkage` 会被拒绝,因为一个
+ 约束不会留下默认值可陈述;一行只陈述 `kind` 与 `linkage` 二者之一,
+ 后一条匹配的陈述会替换前一条,所以一行上的 `linkage = "static"`,
+ 能把某张无条件的表约束为 `shared` 的包,交还给它的消费者去选择形态。
+- 一个目标无法满足的默认值(一个完全静态的镜像、一个 freestanding
+ 目标)会无声地回落,因为根本没有人要求过它。
+- 早于 2026.9.15.2 的引擎会把 `[targets.] linkage` 报告为不受支持
+ 的键(对依赖是静默的),并把包按静态链接;从 2026.9.14.2 起的引擎会
+ 拒绝一张没有 `kind` 的行表。依赖这个键的包应声明这个引擎下限。
+
+根工程的构建程序可以通过 `mcpp::dep_linkage("name")`
+([30 —— build.mcpp](30-build-mcpp.md))读到每个依赖在这次构建中采取的
+形态,使生成的 loader 条目或 import 声明遵循同一个决定。
+
+被约束拒绝的一个请求,会按包允许的形态链接,并给出一条点名包的陈述的
+警告(`its manifest states [targets.fw] kind = "shared", ...`);
+`--strict` 把这条警告变成错误。`mcpp why deps` 报告每个依赖的形态及其
+原因:`default`、`package-default`(2026.9.15.2+)、`requested`、
+`package-kind`、`row-kind`、`packaged`、`no-sources`、
+`prebuilt-inputs`、`no-loader` 或 `static-libc`(2026.9.14.2+)。
+
+按依赖设置的 `linkage`,**只**在根工程自己的 `[dependencies]` 里被
+遵从。图深处的某个包无权决定最终程序如何布局;真正必须是单一共享拷贝
+的包,应该在自己的目标上这样声明。
#### 共享库之下的静态包 *(mcpp 2026.9.16.1+)*
-共享库与它到达的静态包链接在一起。一次构建中的每个共享映像都有一个**静态闭包**:
-从它的包出发、不穿过另一个共享包所能到达的静态包。一个静态包若恰好只在一个闭包里,
-而根工程自己并不到达它,它就链接进那个映像,而不进程序。
+一个共享库与它所触达的静态包一起被链接。一次构建里,每一份共享镜像都
+有一个**静态闭包**:从它的包出发、不跨越另一个共享包所能到达的所有
+静态包。一个只处在**一个**闭包里、根工程自己又碰不到的静态包,会被
+链接进那份镜像,而不是进程序本身。
-2026.9.16.1 之前,这样的包进的是程序,共享库在运行期绑定到程序里的那份。这只在
-ELF 上、且只对那个程序成立:该库通不过 `-Wl,-z,defs`,没有链接这个包的宿主加载不了它
-(`undefined symbol`),Mach-O 与 PE 在链接期就解析每一个引用,Android 则先于任何能
-提供这个包的东西加载应用的共享库。
+在 2026.9.16.1 之前,这样的包会被链接进程序,库在运行时绑定的是程序里
+的那份拷贝。这只在 ELF 上有效,而且只对那一个程序有效:这个库会拒绝
+`-Wl,-z,defs`,一个没有链接该包的宿主无法加载它(`undefined symbol`),
+Mach-O 与 PE 在链接期就解析每一处引用,而 Android 会在任何能提供该包
+的东西之前先加载应用的共享库。
-被**多个**映像到达的静态包(两个共享库,或一个共享库加程序)没有唯一可放的映像:
+一个被**多份**镜像触达的静态包(两个共享库,或一个共享库加程序)没有
+单一的镜像可以栖身:
-- 在 Mach-O、PE 与 Android 应用行上,构建在编译之前被拒绝,原因为
- `static-package-in-two-images`([50](50-machine-output.md));
-- 在其他 ELF 行上,这个包照旧留在程序里,构建会报告它(`build/static-placement`),
- `--strict` 下为错误。
+- 在 Mach-O、PE 与 Android 应用这一行上,构建会在编译之前被拒绝,理由
+ 是 `static-package-in-two-images`([50](50-machine-output.md));
+- 在其它 ELF 行上,包仍像以前一样留在程序里,构建会报告这一点
+ (`build/static-placement`),`--strict` 把它变成错误。
-消息会写出这个包、到达它的映像,以及出路:让这个包取共享形态,使每个映像加载同一份。
+这条消息点名这个包、触达它的那些镜像,以及补救办法:给这个包共享的
+形态,让每份镜像各加载一份拷贝。
```toml
[dependencies]
-x = { path = "../x", linkage = "shared" } # 写在根工程的依赖边上
+x = { path = "../x", linkage = "shared" } # on the root's edge
-# 或作为这个包自己的默认值,写在它的 manifest 里
+# or as the package's own default, in its manifest
[targets.x]
linkage = "shared"
```
-提供目标层的包(`provides = ["mcpp:..."]`,C 库或 C++ 运行时)不在这条规则之内:
-它的对象放在哪里由运行时契约决定([20](20-toolchains.md))。
+一个提供目标层(`provides = ["mcpp:..."]`、一个 C 库或一个 C++ 运行时)
+的包不受这条规则约束:它的对象去往何处,由运行时契约决定
+([20](20-toolchains.md))。
#### library 目标上的 `soname`
-`soname`(§2.2)在 `kind = "lib"` 上同样可以声明。它是一个库被**找到**时用的
-名字,也是 mcpp 构建的那份与第三方携带的同一个库能解析到**同一个文件**的唯一
-途径 —— 而如果声明它就意味着这个包不能再作为静态库被消费,包就无法陈述这件事。
+`soname`(§2.2)也可以声明在 `kind = "lib"` 上,不只是
+`kind = "shared"`。它是一个库被**查找**时使用的名字,也是 mcpp 构建出
+的某个包与第三方的同一份库能解析到**同一个文件**而不是两个文件的
+唯一途径——如果声明它就意味着这个包不能再作为静态库被消费,包就没法
+陈述这一点。
-在非 shared 目标上写 `soname` 的描述符,**无法被 2026.8.28.2 之前的 mcpp 读取**
-—— 失败的是整份 manifest,不只是这个键。因此把它发布进索引要等下限抬上去。
+一份在非共享目标上写了 `soname` 的描述符,无法被 2026.8.28.2 之前的
+mcpp 发布版读取——整份 manifest 都会加载失败,而不只是这个键。因此把
+这样一份描述符发布到索引,要等到那个引擎下限被移动之后。
#### 符号提供者检查
-链接之后,mcpp 会问:映像里的每个符号是不是**恰好有一个**提供者。在 ELF 上
-可执行文件排在最前,因此被静态并进程序的库,会在它与旁边加载的共享库共有的
-每个符号上获胜 —— 共享的那份永远不会被调用,而那个库里的代码跑在一份它并非
-针对其链接的构建上。链接器和加载器都不会为此报任何一句话。
-
-这项检查是**测量**而不是声明:读产物的动态符号表,去掉 copy relocation,只报告
-产物自身闭包里**也**有定义的那些。进程里只有一份副本的安排保持静默。有三类共同定义
-只计数、不报告 *(后两类自 2026.9.16.1 起)*:加载器按设计统一的 vague linkage
-(`STB_WEAK`,以及 GCC 用于内联实体静态数据的 `STB_GNU_UNIQUE`);构建从**同一个
-目标文件**链接进两个映像的定义,例如每个导入 `std` 的 C++ 映像里的 `std` 模块初始化
-函数;以及同一个初始化函数与工具链自身 C++ 运行时之间的重复,GCC 16 起该运行时也导出
-它。形状相同、定义在其他地方的名字仍然会被报告。判定记录在
-`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` 当普通标识符用的消费者会编不过,且看不出原因。
+链接之后,mcpp 会检查镜像里的每一个符号是否恰好只有**一个**提供者。
+在 ELF 上,可执行文件先被搜索,所以一个静态合并进程序的库,会在它与
+旁边加载的共享库共有的每一个符号上获胜——那份共享拷贝从未被真正
+调用,那个库内部的代码运行时面对的是一份它没有链接过的构建。对此,
+链接器与加载器都没有任何诊断。
+
+这项检查是一次测量,不是一条声明:它读取产出镜像的动态符号表,去掉
+属于拷贝重定位的条目,只报告产物自身闭包里**另有**某个库同样定义的
+那些符号。一份进程中只有一处拷贝的安排是无声的。三类共享定义会被计入
+但不报告(最后两类为 2026.9.16.1+):vague linkage,加载器按设计统一
+处理这类符号(`STB_WEAK`,以及 GCC 用于内联实体静态数据的
+`STB_GNU_UNIQUE`);构建从**一个对象**同时链接进两份镜像的定义,例如
+每一个 import `std` 的 C++ 镜像里 `std` 模块的初始化器;以及那同一个
+初始化器相对工具链自身 C++ 运行时的情形,GCC 16 起会导出它。任何其它
+地方定义的同名同形符号,仍算一处发现。判定结果被记录在
+`target///resolution.json` 的 `runtime.symbol_provision`
+下,带计数与分母,CI 无需 `readelf` 即可读取。
+
+它默认是警告,在 `--strict` 下是错误。解决办法按顺序排列,顺序很重要:
+
+1. **让其中一方停止提供它**——通常是某个包自带了图里已经在构建的某个
+ 库的拷贝。总是正确的做法。
+2. **让两者解析到同一个文件**,做法是在那个库的目标上声明它真实的
+ `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` 的**子集**而不是第二个列表的原因 ——
-# 两个数组表达不了一个顺序。
+# 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"]
```
-条目支持与 `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`)
+条目遵循与 `include_dirs` 相同的 `*` glob 约定,并且是在展开**之后**
+匹配的——所以一条 glob 可以正好命中它展开出的那些目录。一个不在这个
+包的 `include_dirs` 里的条目不会隐藏任何东西,并且会被如实报告,而
+不是悄悄放过。
+
+**在较旧的引擎上,这个键会被忽略,而不会致命。** 在 2026.8.26.2 上
+实测:在一个依赖的 manifest 里,它被静默接受;在一份根 manifest 里,
+它会警告——
+`[build] has unsupported key 'private_include_dirs' (ignored)`——
+而构建继续进行。所以一个包可以先采用这个键,不必等待
+它的消费者升级;那些还在旧引擎上的消费者,只会继续像以前一样拿到那个
+目录。唯一的例外,是一份已发布的 `xim` 描述符的 `target_cfg` 块——
+那里,一个无法识别的子键是硬错误,会让整份 manifest 加载失败——在
+索引下限点名一个认识这个键的引擎之前,不要把这个键放进那里。
+
+`include_dirs_after`(#249)列出在工具链的系统目录**之后**才被搜索的
+头文件目录(在 GCC/Clang 上渲染为 `-idirafter`,在 MSVC 方言下渲染为
+追加在末尾的 `/I`,在 NASM 汇编单元上渲染为普通的 `-I`——后两者都没有
+对应机制,也都没有需要保护的系统头文件链)。当某个目录是一个解出来的
+源码压缩包根目录、其中的文件名与标准头文件相撞时,用它代替
+`include_dirs`——例如,在大小写不敏感的 macOS 文件系统上,如果把
+ffmpeg 的压缩包根目录放上 `-I`,它顶层的 `VERSION` 文件会遮蔽 libc++
+的 ``。用 `include_dirs_after`,系统头文件总是获胜,同时这个
+包真正的头文件(``)仍然可以被找到。条目支持与
+`include_dirs` 相同的 `*` glob 约定,并沿着相同的边向依赖它的包
+传播——消费者收到的是「之后」目录,永远不会被升级为 `-I`。
+
+`macos_deployment_target` 设定产物的 Mach-O 头(`LC_BUILD_VERSION
+minos`)里记录的最低系统版本,也就是这个二进制能运行的最旧 macOS
+版本。优先级遵循生态惯例:`MACOSX_DEPLOYMENT_TARGET` 环境变量(一次
+调用的显式覆盖,cargo/rustc、cc 等同样这样处理)> 这个字段(工程默认
+值,类似 SwiftPM 的 `platforms:`)> **内置默认值 `14.0`**(rustc
+风格——每个目标都有一个基线,而 14.0 正是 LLVM 官方静态库自身的下限)。
+这个值进入 BMI 指纹,所以切换目标会自动重建模块缓存。
+
+### 构建并发(`jobs`)与模块调度(`bmi_schedule`)
```toml
[build]
-jobs = "auto" # 或正整数;--jobs / MCPP_JOBS 覆盖它
-bmi_schedule = "off" # auto(默认,= 关)| on | off
+jobs = "auto" # or a positive number; --jobs / MCPP_JOBS override it
+bmi_schedule = "off" # auto (default, = off) | on | off
```
-`jobs` 是同时跑几个编译。`"auto"` **在构建这台机器上现算**,绝不冻进 manifest:
-异构 CPU 上取物理核数(13900K 是 8 P-core + 16 E-core,它的 32 个线程不是 32 个
-等价的工人),再按可用内存夹一次 —— 单个模块接口编译峰值 0.5–1.0 GB。
-写错的值会被
-**明确报出来,绝不静默当成默认值** —— 一个悄悄退回默认的拼写错误,表现是
-「构建莫名其妙比我要求的慢」。
+`jobs` 是同时运行多少个编译。`"auto"` 是**相对正在执行构建的这台机器**
+解析的,绝不会被冻结进 manifest:它取一颗异构 CPU 的物理核心数(一颗
+13900K 是 8 个 P-core + 16 个 E-core,所以它的 32 个线程不是 32 个
+等价的工作者),并按空闲内存夹紧这个数字,因为单次模块接口编译峰值
+占用 0.5–1.0 GB。一个畸形的取值会**被报告,绝不会被静默当作默认值
+处理**——一个悄悄恢复默认值的拼写错误,会让构建比要求的更慢,却没有
+任何迹象说明原因。
-优先级,每一级描述的是不同的东西:
+优先级如下,每一层描述的是不同的东西:
-| 级别 | 作用域 |
+| 层级 | 作用域 |
|---|---|
-| `--jobs` / `MCPP_JOBS` | 这一次调用 |
-| `[build] jobs`(这个键) | 这个工程 |
-| `~/.mcpp/config.toml` 里的 `[build] default_jobs` | **这台机器** |
-| 缺省,或 `0` | 什么都不说,交给后端自己的默认值 |
+| `--jobs` / `MCPP_JOBS` | 本次调用 |
+| `[build] jobs`(这个键) | 本工程 |
+| `~/.mcpp/config.toml` 里的 `[build] default_jobs` | **本机** |
+| 缺失,或 `0` | 什么都不说,交给后端自己的默认值 |
-三者里只有按机器的那个键能承载机器事实。`--jobs` 每次调用都要重说一遍;这个键
-是按包的,而 `[workspace.build]` 不继承它,所以一个七成员的 workspace 会把同一个
-数字写七遍,并把某位开发者的内存上限提交进仓库。`default_jobs = 0` 是 mcpp 写进
-新配置的值,含义是缺省。
+三者之中,只有按机器设置的那个键能承载一个机器事实。`--jobs` 必须在
+每次调用时重复传入;这个键是按包的,`[workspace.build]` 不会继承它,
+所以一个七个成员的 workspace 会把这个数字重复七遍,并把某个开发者
+自己的内存上限提交进仓库。`default_jobs = 0` 是 mcpp 写进一份全新
+配置文件时的取值,含义是「未设置」。
-`default_jobs` **同时约束 `mcpp test` 的并发**,而在那里缺省时的回落是整台机器
-而不是某个后端的默认值。十个并发测试进程与十个并发编译的内存形状一样,所以按机器
-设的数字对两者都生效。这句话写出来是因为:一个键有两种行为,必须明说。
+`default_jobs` **也约束 `mcpp test` 的并发度**,它缺失时的回落值是
+整台机器,而不是某个后端的默认值。一次十个并发进程的测试运行,与一次
+十个并发的编译,内存形状相同,所以一个按机器设置的数字对两者都适用。
+之所以写出这一点,是因为一个键有两种行为,就必须写清楚。
-`bmi_schedule` 决定**导入方什么时候被解锁**。
+`bmi_schedule` 决定 importer 何时被解除阻塞。
-| 值 | |
+| 取值 | |
|---|---|
-| `"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` —— 所以接口用别的扩展名的工程自己声明:
+| `"auto"` | **默认值,目前意味着关闭** |
+| `"on"` | 拆分这条模块边:importer 在 BMI 发布时就开始,而不是等编译器退出 |
+| `"off"` | 一个模块对应一条边 |
+
+只接受这三种拼法。`"ON"`、`"true"` 与 `"yes"` 会**带诊断信息被
+拒绝**,而不是被悄悄当作关闭——它们也不是无害的笔误:这个值会进入
+构建指纹,所以一个被拒绝的拼法,过去会选中一个不同的构建目录(一次
+完整重建),而调度本身却什么都没变。
+
+**为什么 `auto` 是关闭的。** 一次模块接口编译里,86% 的时间花在没有
+任何 importer 会读取的代码生成上,所以提前发布 BMI 很值——在 mcpp
+自身上实测,`cold` 从 86.7s 降到 35.7s,`edit-body` 从 80.9s 降到
+29.8s。但一次调度上的错误是**无声地**错的:一条被漏掉的依赖不会让
+构建失败,只会让某样东西不再被重建。它保持默认关闭,直到在每个平台的
+CI 上都跑通过。
+
+**它帮不上忙的地方。** 在 mcpp 已经跳过级联的地方——`touch-hub`、
+`edit-comment`——没有欠下的工作需要移出关键路径,这个键什么都买不到。
+见[基准测试](../../README.md#benchmark)。
+
+**机制**因编译器而异,并自动选择:gcc 用 `rename()` 发布它的 BMI,
+所以代码生成与之分离,这条边在发布时就返回;clang 得到的是两条普通的
+边,因为它以 `O_TRUNC` 把 BMI 写到最终路径,读者可能会看到一个写了
+一半的文件。MSVC 被留在一边不动——`/ifcOnly` 的开销与 `.ifc` 的
+原子性都未经测量,两者任何一个猜错都是无声的。
+
+### 模块接口扩展名(`module_extensions`)
+
+mcpp 把 `.cppm` 当作一个模块接口单元。C++ 生态还没有在这件事上收敛到
+一种拼法——Clang 还认得 `.ccm` 与 `.cxxm`,MSVC 用 `.ixx`——所以一个
+接口用了别的扩展名的工程需要声明它:
```toml
[build]
module_extensions = [".ixx", ".ccm"]
```
-这个列表是**追加**的:`.cppm` 永远是模块接口,不能删。要让某个文件不参与构建,
-用 `sources` 的 `!` 前缀 —— 那才是 `sources` 的职责。
+这份列表是**只增**的:`.cppm` 永远是模块接口,无法被移除。要阻止某个
+具体文件被构建,在 `sources` 里用 `!` 排除它;这正是 `sources` 的用途。
-声明一个扩展名会同时做三件事,这正是「一个键而不是几个键」的理由:
+声明一个扩展名会同时做三件事,这也是设一个键而不是设几个键的意义
+所在:
-1. `sources` 的约定默认值跟着变宽,文件才**能被找到**(`src/**/*.ixx` 自动进入默认 glob);
-2. 这些单元用**模块**规则编译 —— 产出 BMI,其 `.o` 无条件进入链接;
-3. 新鲜度快路径会扫描它们,所以给其中一个加 `import` 会让构建图作废,
- 而不是静默复用一张过期的图。
+1. `sources` 的约定默认值随之扩大,使这些文件能被**找到**
+ (`src/**/*.ixx` 加入默认 glob);
+2. 这些单元按**模块**规则编译——它们发出 BMI,它们的对象无条件被
+ 链接;
+3. 新鲜度快路径会盯住它们,所以给某个文件加一条 `import` 会让构建图
+ 失效,而不是静默复用一份陈旧的图。
-**任何扩展名都接受**,唯独拒绝那些已经代表其他角色的
-(`.cpp` `.cc` `.cxx` `.c` `.m` `.mm` `.h` `.hpp` `.hh` `.hxx` `.S` `.s` `.asm`)——
-这是 manifest **错误**而不是警告,因为它会把(比如)C 文件送进 C++ 模块规则,
-最终失败在一个既不提文件也不提这个键的地方。
+除了那些已经命名了某种非模块角色的扩展名(`.cpp` `.cc` `.cxx` `.c`
+`.m` `.mm` `.h` `.hpp` `.hh` `.hxx` `.S` `.s` `.asm`)之外,任何扩展名
+都会被接受;声明其中之一是 manifest 错误而不是警告,因为那会把(比如
+说)C 文件路由进 C++ 模块规则,并在一个既不点名文件也不点名这个键的
+地方失败。
-扩展名**按字面匹配,不做大小写折叠** —— 在这个领域里 `.S` 和 `.s` 是两种不同的语言,
-所以大小写从不被忽略。
+扩展名是**按字面匹配、不做大小写折叠**的——`.S` 与 `.s` 在这个领域
+是两种不同的语言,大小写永远不会被忽略。
-mcpp 每次都会**显式告诉编译器**这个单元是模块接口(Clang 用 `-x c++-module`,
-GCC 用 `-x c++`,MSVC 用 `/interface /TP`),所以即使编译器驱动从没听说过这个扩展名
-也能工作。这也是为什么任何扩展名都被允许:mcpp 不需要编译器认识它。
+mcpp 总是明确告诉编译器某个模块接口单元就是模块接口单元(Clang 上是
+`-x c++-module`,GCC 上是 `-x c++`,MSVC 上是 `/interface /TP`),所以
+编译器驱动程序从未听说过的扩展名照样能工作。这正是任何扩展名都被
+允许的原因:mcpp 不需要编译器认识它。
-> **发布须知**:旧版 mcpp 不认识这个键 —— 它会警告、忽略,然后把那些文件当作普通
-> 翻译单元编译,得到一个**错误的构建**而不是一次干净的失败。发布一个用了
-> `module_extensions` 的包,请在它的索引描述符里声明 mcpp 版本下限。
+> **发布注意事项。** 较旧的 mcpp 不认识这个键:它会警告、忽略它,
+> 然后把那些文件当作普通翻译单元编译——这是一次错误的构建,而不是
+> 一次干净的失败。一个使用了 `module_extensions` 的已发布包,应在其
+> 索引描述符里声明一个 mcpp 版本下限。
-### 构建程序超时(`build_program_timeout`)
+### 构建程序超时(`build_program_timeout`)
-`build.mcpp` 默认有 **600 秒**,超时后 mcpp 杀掉它并让构建失败、点名是哪个包。
-构建程序确实需要跑更久的工程(大规模代码生成)自己抬高上限:
+一个 `build.mcpp` 默认获得 **600 秒**,超时后 mcpp 会杀掉它并使构建
+失败,同时点名这个包。一个构建程序确实需要更长时间运行的工程(比如
+一个大型的代码生成步骤),可以抬高自己的上限:
```toml
[build]
-build_program_timeout = 1800 # 秒;0 = 不限
+build_program_timeout = 1800 # seconds; 0 = no limit
```
-这个值读的是**拥有该 `build.mcpp` 的那个包**的 manifest —— 依赖的生成器由依赖自己的
-声明来限制,因为只有它的作者知道要跑多久。优先级与 `macos_deployment_target` 同构:
+这个值取自**拥有这个 `build.mcpp` 的那个包自己的 manifest**——一个
+依赖的生成器,由依赖自己的声明约束,因为知道它要跑多久的是它的作者。
+优先级与 `macos_deployment_target` 遵循相同的形状:
```
-MCPP_BUILD_PROGRAM_TIMEOUT=<秒> 本次调用(最高)
- > [build] build_program_timeout 该包自己的 manifest
- > 600 内置默认
+MCPP_BUILD_PROGRAM_TIMEOUT= (this invocation; highest)
+ > [build] build_program_timeout (that package's manifest)
+ > 600 (built-in default)
```
-**不写这个键**与**写 `0`** 不是一回事:不写表示「用默认上限」,`0` 表示「完全不设上限」。
+省略这个键与把它设为 `0` 不是一回事:不设置意味着「用默认上限」,`0`
+意味着「完全没有上限」。
-这个值刻意**不进构建指纹** —— 它不改变图里的任何一条边,而把它折进指纹会让
-「抬高超时」触发全量重建,这恰好与抬高超时的人想要的相反。
+这个值刻意**不是**构建指纹的一部分——它不改变图里的任何一条边,把它
+折进指纹会意味着抬高超时会重建整个工程,这与抬高超时的人想要的正好
+相反。
-只有构建**程序**受限,**编译**不受限。原因见
-[30-build-mcpp.md](30-build-mcpp.md)。
-### C++ 运行时契约(`cxx_runtime`)
+**编译**阶段不受这个上限约束,只有构建**程序**受约束。这种不对称是
+刻意为之,原因见 [30-build-mcpp.md](30-build-mcpp.md)。
-已移入 [20 —— 工具链管理](20-toolchains.md)。
+### C++ 运行时契约(`cxx_runtime`)
+已移至 [20 —— 工具链管理](20-toolchains.md)。
### 宿主代码页之外的文件名
-glob 是窄字符串,编译命令和 `build.ninja` 也是。在 Windows 上这些字符串由进程的
-**ANSI 代码页**产生,因此一个名字在该代码页里无法拼写的文件,既匹配不了 glob,也
-写不进编译命令或构建文件。
+glob 是窄字符串,编译命令与 `build.ninja` 也是。在 Windows 上,这些
+字符串以进程的 **ANSI 代码页**产生,所以一个文件名在那个代码页里没有
+拼法的文件,无法被 glob 匹配,无法出现在编译命令里,也无法写进构建
+文件。
-这类条目会被跳过,并按目录报告一次:
+这样的条目会被跳过,跳过信息按目录汇报一次:
```text
warning: 'C:/.../pkg/test/www' contains names this system's active code page cannot represent
@@ -745,41 +845,48 @@ warning: 'C:/.../pkg/test/www' contains names this system's active code page can
hint: Windows only: this is the process ANSI code page, which `chcp` does not change. ...
```
-报告里给的是**最近一个代码页拼得出的祖先目录**,用通用(`/`)写法。拼不出的那个名字本身
-永远不会被打印:渲染它会抛出这条消息正在报告的同一个异常。
+报出的路径是最近的、其名字**能**被该代码页拼出的祖先目录,采用通用
+(`/`)拼法。出问题的名字本身永远不会被打印:渲染它会抛出与这条消息
+正在报告的同一个异常。
-`chcp` 改的是**控制台**代码页,对此无效。若这些名字只是测试数据或文档,跳过是无害
-的——上游 tarball 里带一个日文夹具目录,在 en-US 宿主上照样构建。源文件则不然:需要
-改名,或换一台代码页覆盖得了的机器。
+`chcp` 设置的是**控制台**代码页,在这里没有作用。只是测试数据或文档
+的文件名是无害的——一个携带日语命名测试夹具目录的上游压缩包,在
+en-US 宿主上照常能构建。源文件则不然:它们需要改名,或者需要一个
+代码页能覆盖它们的宿主。
-Linux 与 macOS 不做这种转换,因此那里不会跳过任何名字。一个包在一边能构建、在另一
-边报 `internal: unhandled exception` 并指向代码页,就是 mcpp#516。
+Linux 与 macOS 不做这种转换,所以那里没有任何东西被跳过。一个能在
+其中一个上构建、在另一个上不能、并报出一条来自代码页消息的
+`internal: unhandled exception` 的包,就是 mcpp#516。
-### 2.3.1 `[build] accel` — 本次构建面向的加速器
+### 2.3.1 `[build] accel` —— 本次构建面向的加速器
```toml
[build]
accel = "cuda12.8+{sm_80,sm_90f} ptx>=90"
```
-本次构建为哪些设备后端与架构编译。单次构建可用 `--accel` 覆盖 ——
-这与 `--target` 对 `[toolchain]` 的关系相同;`--no-accel` 是显式请求「不要加速器」,
-也就是在一个同时发布了设备构建的包中选中 CPU-only 变体的方式。
+这次构建面向哪些设备后端与体系结构。可被 `--accel` 为单次构建覆盖,
+它与 `[toolchain]` 的关系和 `--target` 相同;`--no-accel` 显式请求不
+要任何加速器,这正是从一个同时发布设备构建的包中选出纯 CPU 变体的
+方式。
-该取值会与构建所消费的任何预建产物的 `accel` 字段比较,而请求为空的构建被任何产物满足。
-见 [42 — 异构硬件构建](42-heterogeneous-builds.md)。
+这个值会与所消费的任何预构建产物的 `accel` 字段比较,一次不要求任何
+加速器的构建,会被任何产物满足。见
+[42 —— 异构构建](42-heterogeneous-builds.md)。
-### 2.4 `[lib]` — 库根模块约定
+### 2.4 `[lib]` —— 库根模块约定
```toml
[lib]
-path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置
+path = "src/capi/lua.cppm" # Override the default lib-root location
```
-默认约定:`src/<包名最后一段>.cppm`(如包名 `mcpplibs.cmdline` → `src/cmdline.cppm`)。
+默认约定:`src/<包名的最后一段>.cppm`(例如包名 `mcpplibs.cmdline` →
+`src/cmdline.cppm`)。
+
### 2.5 `[dependencies]`、`[dev-dependencies]`、`[build-dependencies]`
-已移入 [05 —— 依赖与解析](05-dependencies.md)。
+已移至 [05 —— 依赖与解析](05-dependencies.md)。
### 2.7 `[toolchain]` —— 工具链配置
@@ -787,15 +894,15 @@ path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置
[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.*]` —— 平台条件依赖与 flag
-已移入 [22 —— 目标侧](22-target-side.md)。
+### 2.7.1 `[target.*]` —— 平台条件依赖与 flag
+已移至 [22 —— 目标侧](22-target-side.md)。
### 2.7.3 `min_api_level` —— 产物必须能跑在多老的 OS 上
@@ -804,74 +911,74 @@ linkage = "static"
min_api_level = 24
```
-Android 自己的用词是 **API level**,而这里要的是它的**最小值** —— NDK 的 CMake
-toolchain 把 `ANDROID_PLATFORM` 记载为「the minimum API level supported by the
-application or library」,并说明它对应 Gradle 的 `minSdk`。
+Android 自己的术语是 **API level**,这里指的是其中的最小值——NDK 的
+CMake 工具链文档里把 `ANDROID_PLATFORM` 记录为承载的那个量(「应用或
+库支持的最低 API level」),对应 Gradle 的 `minSdk`。
-**这是工程的决定,不是工具链的属性。** 一个 NDK 服务一个级别区间,所以写
-`android-ndk@` 并不钉住某一个级别。
+**这是一个工程决定,不是工具链的属性。** 一个 NDK 服务于一段范围内的
+level,所以命名 `android-ndk@` 并不能钉住其中一个。
-**它到达编译器,不进入身份。** 规范 triple 仍然是 `aarch64-linux-android` ——
-输出目录、`cfg(env = "android")` 和打包的 ABI tag 都由它命名;级别只拼进交给
-编译器的那个 triple:
+**它到达的是编译器,而不是身份。** 规范三元组始终是
+`aarch64-linux-android`,它命名输出目录、`cfg(env = "android")` 与
+打包后的 ABI 标签;level 被融合进传给编译器的那个三元组:
| | |
|---|---|
-| 规范 triple | `aarch64-linux-android` |
-| clang 实际收到 | `aarch64-unknown-linux-android24` |
-| 构建指纹 | 含级别 |
+| 规范三元组 | `aarch64-linux-android` |
+| clang 收到的 | `aarch64-unknown-linux-android24` |
+| 构建指纹 | 携带这个 level |
-指纹不是可选项:级别决定哪些 bionic 符号可见,所以两个级别就是两个 ABI,绝不可
-共用一个构建目录。
+指纹里带上它不是可选的:level 决定哪些 bionic 符号可见,所以两个
+level 是两种 ABI,绝不能共享一个构建目录。
-不设也合法,含义是 NDK 自己的默认级别 —— 那正是
-`clang -target aarch64-linux-android` 规范化出的形式。
+不设置是合法的,意味着使用 NDK 自己的默认值,也就是 `clang -target
+aarch64-linux-android` 归一化后得到的那个值。
-这与 `macos_deployment_target`(见上文)是同一套机制,而两者都用各自平台的词汇
-命名,而不是一个共享抽象。它们回答同一个问题:产物必须能跑在多老的 OS 发布版上。
+这与 `macos_deployment_target`(见上文)用的是同一套机制,两者各自用
+自己平台的说法命名,而不是共用一个抽象。两者回答的是同一个问题:产物
+必须能运行的最旧 OS 版本是哪个。
+### 2.7.2 裸机(`os = none`)—— freestanding 目标
-### 2.7.2 裸机(`os = none`)—— freestanding target
+`riscv64-none-elf` 与 `riscv32-none-elf` 是底下没有操作系统的目标。
+它们不需要按宿主区分的交叉工具链:clang 与 lld 本就是交叉编译器,所以
+任何能安装 llvm 载荷的宿主都能产出它们。
-`riscv64-none-elf` 与 `riscv32-none-elf` 是底下没有操作系统的 target。它们不需要
-逐宿主的交叉工具链:clang 与 lld 天生是交叉编译器,任何能装 llvm 载荷的宿主都能
-产出它们。
-
-本节是清单参考。示例部分 —— 生成工程、运行、在目标上测试、freestanding 标准库
-子集,以及编写板级支持包 —— 在
-[40 — 裸机与 freestanding 目标](40-baremetal.md)。
+本节是 manifest 参考。实战示例——脚手架搭建、运行、在目标上测试、
+freestanding 标准库子集,以及编写一个板级支持包——都在
+[40 —— 裸机与 freestanding 目标](40-baremetal.md)里。
```bash
mcpp build --target riscv64-none-elf
-mcpp run --target riscv64-none-elf # 经 [target.].runner
+mcpp run --target riscv64-none-elf # via [target.].runner
```
-**从板级支持包起步**
+**从一个板子包开始**
-下面这些几乎都不需要手写。板级支持包(BSP)自带 C 库、启动代码、内存布局和模拟器,
-所以跑起一个镜像的最短路径是:
+下面这些内容几乎不需要手写。一个板级支持包携带了 C 库、启动代码、
+内存布局与模拟器,所以到达一个能启动的镜像的最短路径是:
```bash
mcpp new blinky --template riscv-virt-rt
cd blinky && mcpp run
```
-生成的 manifest 里没有链接脚本、没有加载地址、没有 libc、没有模拟器 —— 连
-`[target.*]` 段都没有。本节余下的内容讲的是**这样一个包提供了什么**,也就是要给
-一块还没有 BSP 的板子写一个时该照着做什么。
+生成的 manifest 不命名任何链接脚本、加载地址、libc 或模拟器——它完全
+没有 `[target.*]` 小节。本节剩下的部分描述这样一个包供给了什么,这
+正是在为一块没有这些的板子编写包时应当参照的内容。
-**freestanding target 上有什么不同**
+**freestanding 目标上有哪些不同**
| | |
|---|---|
-| 链接线 | `-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` 的那个文件。 |
+| 链接行 | `-nostdlib -nostartfiles -static`,不含任何 hosted 的东西——没有 crt 文件、没有动态链接器、没有 C++ 运行时。链接器以**绝对路径**寻址(`-fuse-ld=/bin/ld.lld`),因为 `-fuse-ld=lld` 经 `PATH` 解析,在任何前面装了 binutils 的机器上都会找到 GNU ld。 |
+| ISA 旗标 | `-march` / `-mabi` / `-mcmodel` 取自目标表,所以仅凭 `--target ` 就足以产出正确的对象文件。 |
+| C 库 | **目标自己的那一份**,由 mcpp 从目标自己的行解析,方式与解析编译器完全相同——一个裸机工程不声明任何 libc,正如一个 hosted 工程不声明 glibc。它的头文件到达每一个翻译单元,它的目录在链接搜索路径上,所以一个板子包用裸名字(`-lc`、`-lcrt0-semihost`)从中选取。**哪些**对象、**哪份**链接脚本,仍是板子自己的决定。 |
+| 异常与 RTTI | **关闭**,在包括依赖在内的每一个翻译单元上。这里没有展开器,也没有 `libc++abi`,所以没有任何东西能抛出;否则单是 `std::optional::value()` 就会拉进 `__cxa_throw` 以及另外三个未定义符号。它属于目标而不是工程的 `cxxflags`,因为 BMI 会记录它——一个带异常编译的依赖,无法被一个不带异常的单元 import。 |
+| `import std` | **不可用。** `std` 是覆盖整个标准库的一个模块——包括线程、文件系统与 iostreams——所以没有一个子集能在没有操作系统的情况下构建出来。两个普通依赖取代了它:**板子包**包装目标的 C 库,**`std-freestanding`** 携带标准库里不需要操作系统的那些部分(实测为 libc++ 110 个头文件中的 103 个)。 |
+| 入口点 | 只要有东西提供了 `crt0`,`int main()` 就能工作——一个板子包通常提供,这样固件的入口点就是一个普通的 `main`,它的返回值经由 semihosting 到达宿主。只有零 libc 的板子才需要一个显式目标,其 `main` 指向携带 `_start` 的文件。 |
-**一个最小固件**
+**一份最小固件**
```toml
[package]
@@ -883,70 +990,79 @@ ldflags = ["-T", "/abs/path/to/link.ld"]
[targets.firmware]
kind = "bin"
-main = "src/start.S" # 入口在汇编里,不在 main()
+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` —— `mcpp run` 如何执行本机跑不了的东西**
+**`runner`——`mcpp run` 如何执行一个这台机器本身跑不了的东西**
-裸机镜像的 ISA 不对、没有 loader、且期望独占整个地址空间;直接 exec 它得到的是
-"Exec format error"。`runner` 就是挡在它前面的 argv 模板。产物路径会被**追加**,
-或者在模板含 `{}` 时替换进去。
+一个裸机镜像的 ISA 不对,没有加载器,并期望独占整个地址空间;直接
+执行它会得到「Exec format error」。`runner` 是站在它前面的那份 argv
+模板。产物路径会被**追加**在后面,或者在模板包含 `{}` 时替换掉它。
-mcpp **刻意不提供默认 runner**。用哪个模拟器、哪个机器型号、哪种固件模式都是板级
-事实 —— 同一 ISA 的两块板需要不同 argv(OpenSBI 启动用 `-bios default`,picolibc
-镜像用 `-bios none -semihosting`)—— 引擎一旦猜一个,另一块板就得跟它打架。板级
-支持包通常会提供它。
+mcpp 刻意**不附带任何默认 runner**。用哪个模拟器、哪个机器型号、哪种
+固件模式,都是板子自己的事实——同一 ISA 上的两块板子可能需要不同的
+argv(OpenSBI 启动用 `-bios default`,picolibc 镜像用 `-bios none
+-semihosting`)——一个替某一块板子猜一个值的引擎,就是另一块板子必须
+与之对抗的引擎。一个板级支持包通常会提供它。
-### 2.7.3 hosted 目标上的 `runner`(2026.9.2.1+)
+### 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 执行产物,要么尝试直接执行并
-报告内核的回答。
+`[target.].runner` 适用于每一个精确三元组,不只是裸机。一个
+hosted 的交叉产物——在 x86_64 机器上构建的 `aarch64-linux-musl`——在
+一些宿主上可执行(注册了 qemu-user 的 binfmt_misc),在另一些宿主上会
+被拒绝并报 `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):
+对 `mcpp run` 与 `mcpp test` 都适用的规则:
+
+- **一个已声明的 runner 会被使用。** 它的第一个元素由 mcpp 定位:先在
+ `[xlings.workspace]`(§2.13)下每个已声明载荷的 `bin/` 目录里找,再
+ 到 `PATH` 上找。`PATH` 上的一个裸名字会解析到一个 xvm shim,它回答
+ 的是当前 SubOS,而不是这个包;正是载荷查找,让一个 runner 能够命名
+ 工程自己声明的程序。
+- **一个已声明但找不到或启动不了的 runner 是一个错误**,报告程序、
+ 搜索过的目录与 errno。这里没有回落到直接执行:用不同的解释器、不同
+ 的参数去运行这个产物,正是这个键存在要防止的那种失败。
+- **没有 runner,内核拒绝这个产物:** `mcpp run` 报告这次拒绝与需要
+ 写的那个键,退出码 2。`mcpp test` 把每个测试都报告为未运行,理由
+ 只报一次,退出码 2(§2.7.3.1)。
+- **`--no-runner`** 直接执行产物,忽略已声明的 runner。它陈述的是
+ 关于这台宿主的一个事实——这个三元组在这里是原生的——而 manifest
+ 没有轴可以承载这一点;一个 runner 为 x86_64 开发者写的工程,在
+ aarch64 机器上依然可读。
+
+通过 `[xlings.workspace]` 配置模拟器,是给 CI 作业、或构建在单一宿主
+类别上的工程用的形式。索引里的 `qemu-user-aarch64` 只为 x86_64
+Linux 构建,而这张表会在每个构建这个工程的宿主上配置,所以这个条目
+要按平台分别写(§2.13):
```toml
[xlings.workspace]
-"xim:qemu-user-aarch64" = { linux = "" } # Linux 上存在即可,版本不限
+"xim:qemu-user-aarch64" = { linux = "" } # present on Linux, any version
[target.aarch64-linux-musl]
runner = ["qemu-aarch64-static"]
```
-宿主装不了的包是硬构建错误,所以不带平台形式的条目会让工程在 macOS 与 Windows 上
-无法构建。同样没有这个包的 Linux/aarch64 宿主传 `--no-runner`。
+一个宿主装不上的包是一个硬构建错误,所以一个没有按平台区分的条目,
+会让这个工程在 macOS 与 Windows 上完全无法构建。而在这个包同样不
+存在的 Linux/aarch64 宿主上,要传 `--no-runner`。
#### 2.7.3.1 `mcpp test` 与未运行的测试
-产物在本机无法执行的测试既没有通过也没有失败。`mcpp test` 把它报告为**未运行**,
-在确立原因时打印一次,在汇总里重复原因的第一行,退出码 2:
+一个这台宿主无法执行其产物的测试,既没有通过,也没有失败。`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
@@ -954,37 +1070,39 @@ 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
+退出码 1 保留原有含义——一个测试运行过并失败;0 意味着每个测试都
+运行过并通过。`--message-format json` 在每条记录上携带
+`"status":"not_run"` 与一个 `reason`,摘要记录上则是 `not_run` /
+`not_run_reason`(见[50 —— 机器可读输出](50-machine-output.md))。
-已移入 [06 —— Feature 与能力](06-features-and-capabilities.md),
-连同 `provides` / `requires` 与 `[feature-deps.]`。
+### 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 集合已知且稳定时,
-用声明取代扫描:
+默认的模块扫描器是一趟文本层面的处理,它(刻意)拒绝条件预处理块内的
+`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"] # 每个单元至多提供一个模块
+provides = ["fmt"] # at most one provided module per unit
imports = ["std"]
```
-被 glob 命中的文件跳过文本扫描,声明的单元直接进入模块图。该声明**每次构建都被审计**:
-编译器自己对该文件的 P1689 扫描结果(`.ddi` dyndep 输入)会与之比对,任何分歧都会让
-那条编译边失败并打印双方 —— 陈旧的声明无法静默污染模块图。未命中任何源文件的
-override glob 是错误。
+被这条 glob 匹配到的文件会跳过文本扫描;声明的单元直接进入模块图。
+这条声明会在**每次构建时被审计**:编译器自己对这个文件做的 P1689
+扫描(`.ddi` dyndep 输入)会与它比对,任何分歧都会让那条编译边失败,
+并同时打印双方的结果——一条陈旧的声明无法悄悄污染这张图。一条不匹配
+任何源文件的 override glob 是一个错误。
-同一个键在 xpkg 描述符(索引包)中同样存在:
+xpkg 描述符(索引包)里也有同一个键:
```lua
mcpp = {
@@ -996,119 +1114,135 @@ mcpp = {
}
```
-要把 plan 与 ddi 的比对审计扩展到**每一个**模块单元(而不只是 override),
-在生成构建时设置 `MCPP_VERIFY_MODGRAPH=1`。
+要把这套「plan 与 ddi 对照」的审计扩展到**每一个**模块单元(不只是
+override),在生成构建时设置 `MCPP_VERIFY_MODGRAPH=1`。
### 2.8.4 默认扫描器读到的东西
-扫描器对每个文件只回答两个问题 —— 这个单元提供什么、需要什么 —— 并且没有别的
-东西决定它们。
+扫描器为每个文件回答两个问题——这个单元提供什么、这个单元需要
+什么——没有别的东西决定这两个答案。
-**三种模块声明,它们是不同的产生式。**
+**三种模块声明,它们是不同的产生式。**
```cpp
-module M; // 实现单元: 需要 M,不提供任何东西
-module M:part; // 实现分区: 提供 M:part
-module : private; // 私有模块片段:两者都不声明
+module M; // implementation unit: requires M, provides nothing
+module M:part; // implementation partition: provides M:part
+module : private; // private module fragment: declares neither
```
-第三种不是一个名字以冒号开头的分区。它在两个方向上都不产生边,其后的内容仍属于
-同一个单元。编译器是否实现它由编译器回答:GCC 16.1 报
-`sorry, unimplemented: private module fragment`,mcpp 不在其上追加任何说法。
+第三种不是一个名字以冒号开头的分区。它在任何方向上都不贡献边,它之后
+的内容仍然属于同一个单元。编译器是否实现了它,答案由编译器给出:
+GCC 16.1 报告 `sorry, unimplemented: private module fragment`,mcpp
+对此不做任何额外处理。
-**模块扩展名的文件不必提供模块。** 实现单元是 `.cppm` 的合法居民,而
-`module_extensions` 说的是**扫描**哪些文件,不是每个文件**是**什么。编译方式跟随
-扫描结果:提供模块的单元按接口编译并获得写 BMI 的位置;不提供的按普通翻译单元
-编译。因此两个编译器对同一个文件收到相同的指令 —— 在 mcpp 2026.9.9.1 之前并非
-如此:Clang 从扩展名推断出 `c++-module` 并拒绝该文件,而 GCC 能构建它。
+**一个模块扩展名文件不必提供一个模块。** 一个实现单元是 `.cppm` 里
+合法的居民,`module_extensions` 说的是要**扫描**哪些文件,而不是每个
+文件**是**什么。编译模式跟随扫描结果:一个提供模块的单元被当作接口
+编译,并给它一个写出 BMI 的地方;一个不提供模块的单元被当作普通翻译
+单元编译。因此两个编译器对同一个文件收到的是同一条指令——这在 mcpp
+2026.9.9.1 之前并非如此:Clang 会从扩展名推断出 `c++-module` 并拒绝
+这个文件,而 GCC 则会构建它。
-**源码按 UTF-8 读取。** UTF-8 字节序标记会被消耗,不属于正文 —— 这正是每个编译器
-对它的处理,也是 MSVC 默认写出的东西。UTF-16 或 UTF-32 标记会被具名拒绝,而不是
-被误读。同一规则适用于 `mcpp.toml`。
+**源码按 UTF-8 读取。** 一个 UTF-8 字节顺序标记会被消费掉,不算作
+正文的一部分,这与每个编译器的处理方式相同,也是 MSVC 默认写出的
+形式。一个 UTF-16 或 UTF-32 标记会被点名拒绝,而不是被误读。同一条
+规则也适用于 `mcpp.toml`。
-**任何源码都不可能声明出来的名字会被拒绝。** 模块身份是以点分隔的标识符序列,
-其后可选地跟一个 `:` 和另一个这样的序列。此外的形式在扫描阶段失败,而不是进入
-构建图 —— 在那里它会变成一条没有任何东西会报告的 BMI 路径。
+**一个源码不可能声明出来的名字会被拒绝。** 一个模块身份是一串由点
+分隔的标识符,后面可以再跟一个 `:` 与另一串这样的序列。任何其它形式
+都会在扫描阶段失败,而不会进入构建图——否则它会变成一个没有任何
+东西会报告的 BMI 路径。
-### 2.9 `[profile.]` — 构建档案
+### 2.9 `[profile.]` —— 构建档案
```toml
[profile.dist]
-opt = 3 # -O 级别(数字或 "s"/"z" 字符串)
+opt = 3 # -O level (a number, or the string "s"/"z")
debug = false # -g
-lto = true # -flto(注意:部分打包 gcc 未启用 LTO 插件)
-strip = true # 链接期 -s
-# passthrough 逃生口(固定键、开放值):
+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 = []
```
-- 选择与默认:裸 `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、依赖版本相同的两个工程复用同一条目。
+- 选择与默认值:一条裸的 `mcpp build` 使用 **`dev`** profile
+ (`-O0 -g`)——这是主流约定(参见 Cargo/Meson/CMake/Zig/Bazel)。
+ **Release 需要显式选择:**`mcpp build --release`(简写)或
+ `--profile release`。`--dev` 是 dev 的显式简写。`mcpp test
+ --profile ` 同样适用(在该 profile 下构建被测代码及测试
+ 二进制)。
+- **按工程的默认值**——`[build].default-profile = ""`(别名:
+ `profile`)在没有传入旗标时设置这个工程自己的默认值。典型用法是一个
+ 默认就该优化构建的工具或库:`[build] default-profile = "release"`。
+ 优先级:`--profile`/`--release`/`--dev` 旗标 **>**
+ `[build].default-profile` **>** 全局默认 `dev`。(一个默认走 dev
+ 的工程,在产出可分发物时应传 `--release`。)
+- 内置 profile:`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"
+cache = "global" # "global" (default) | "local" | "off"
```
-| 模式 | 读缓存 | 写缓存 | 先清构建目录 |
+| 模式 | 读缓存 | 写缓存 | 先清空构建目录 |
|---|---|---|---|
-| `global`(默认) | 是 | 是 | 否 |
+| `global`(默认) | 是 | 是 | 否 |
| `local` | 否 | 否 | 否 |
| `off` | 否 | 否 | 是 |
-`local` 把所有依赖都编在本工程 `target/` 内 —— 排障时一次性排除"是不是缓存的问题",
-也给 CI 一个无共享的可复现基线。`off` 额外清掉本次的 `target///` 做冷构建;
-`--no-cache` 是它的兼容别名。
+`local` 让每个依赖都在这个工程自己的 `target/` 里构建——用于在排查
+某个问题时排除缓存因素,也用于给 CI 一条不共享的基线。`off` 还会额外
+清空这次构建的 `target///` 以获得一次冷构建;
+`--no-cache` 是它的一个已废弃别名。
-优先级:`--cache ` **>** `MCPP_BUILD_CACHE` **>** `[build] cache` **>** `global`。
-无法识别的值会被报出来(`--strict` 下为错误),而不是静默回落到 `global`。
+优先级:`--cache ` **>** `MCPP_BUILD_CACHE` **>**
+`[build] cache` **>** `global`。一个无法识别的取值会被报告(`--strict`
+下是错误),而不是静默回落到 `global`。
-**不进缓存的**:`path` 与 `git` 依赖(任意深度)以及 workspace 成员。它们的源码可以在
-`name@version` 不变的情况下改变,任何基于该身份的键都看不见这种变化。
+**不会**被缓存的:`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 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]
```
-条目的磁盘布局是带版本的。改动布局的 mcpp 版本会**一次性作废全部旧条目**,
-所以升级后的第一次构建会重编依赖并重新填充 —— 不需要手工清理。
-2026.8.3.4 就是这样一次:条目里对象的地址现在相对**包**自身,
-而不再相对"最先填充这个条目的那个工程"的构建目录。
-`mcpp cache verify` 另外会报告任何逃出条目的记录地址,
-使这条不变量可以离线审计。
+磁盘上的条目布局是版本化的。一个改变了它的 mcpp 发布版,会一次性
+淘汰每一个旧条目,所以这样一次升级之后的第一次构建,会重建它的依赖
+并重新填充缓存——不需要手动清理任何东西。2026.8.3.4 做的正是这件事:
+一个条目的对象路径现在相对**包**寻址,而不再相对最先填充这个条目的
+那个工程的构建目录。`mcpp cache verify` 还会额外报告任何记录的地址
+逃出了自身范围的条目,使这类复发情况可以离线审计。
-### 2.11 `[runtime]` — provider-neutral 运行时契约
+### 2.11 `[runtime]` —— provider-neutral 运行时契约
```toml
[runtime]
requirements = [
- { kind = "capability", value = "display.present", phase = "run", required = true },
+ { 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"]
@@ -1116,7 +1250,7 @@ artifacts = [
{ role = "library", path = "runtime/libwidget.so.1", provenance = "payload", abi = "elf-x86_64", digest = "sha256:...", host_fingerprint = "host-1" },
]
-# 平台无关 LinkIntent;路径相对本包根目录。
+# Platform-neutral LinkIntent. Paths are relative to this package root.
libraries = ["widget"]
link_library_dirs = ["lib"]
transitive_needed_dirs = ["runtime/closure"]
@@ -1125,19 +1259,19 @@ frameworks = ["WindowKit"]
deploy_files = ["bin/widget.dll"]
deploy = [ { from = "share/vulkan/icd.d/widget_icd.json", to = "vulkan/icd.d" } ]
-# 多 provider 时使用精确 canonical identity。
+# Use an exact canonical identity when multiple providers exist.
[runtime."display.present"]
provider = "acme.widget-runtime@2.0.0"
```
-本表中不受支持的键会被**报出并忽略**,消息里列出它比对用的那份键表。
-`[runtime.]` 子表是 provider 覆盖而不是键,因此不在清扫范围内。
-同一规则适用于 `[target..runtime]`,其词汇表是 `libraries`、
-`link_library_dirs` 与 `frameworks`(mcpp 2026.9.12.3+)
-(见[22 —— 目标侧](22-target-side.md))。该表上的 `frameworks` 追加在顶层列表
-之后,只在 Mach-O 各行渲染为 `-framework `,其余各行不产生任何标志——
-当某个 framework 存在于 iOS 而不存在于 macOS(或相反)时,manifest 用这个键
-表达:
+这张表里一个不受支持的键会**被报告并被忽略**,消息会列出它核对过的
+那些键。一个 `[runtime.]` 子表是一处提供者覆盖,不是一个
+键,所以不会被扫入。同样的规则适用于 `[target..runtime]`,
+它的词汇是 `libraries`、`link_library_dirs` 与 `frameworks`
+(mcpp 2026.9.12.3+)([22 —— 目标侧](22-target-side.md))。那张表上的
+`frameworks` 会追加在顶层列表之后,只在 Mach-O 各行上渲染
+`-framework `,在其它行上什么都不渲染——这是一份 manifest 在
+某个 framework 只存在于 iOS 而不存在于 macOS(或反过来)时要用的键:
```toml
[runtime]
@@ -1150,158 +1284,187 @@ frameworks = ["AppKit"]
frameworks = ["UIKit"]
```
-`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 把不同发现阶段分开:
+`requirements` 记录一个非空的 `kind`/`value`,一个 `link` 或 `run`
+阶段,以及这条要求是否强制(`required` 默认 `true`)。
+
+`discovery` 是可选的,说明**加载器如何找到**满足这条要求的东西——
+例如 `rpath-of-dispatch`、`json-dir`、`glvnd-dispatch`。它是**被
+声明的,从不被推断**:一个能力用哪种机制是提供者的属性,会在 mcpp
+不知情的情况下改变,所以 mcpp 只是携带这个值,把一个未声明的情形
+报告为 `unknown`,而不是去猜。之所以专门设一个字段,是因为这些机制
+彼此不可互换——一种可能是烘焙进某个 dispatch 库里的搜索路径,另一种
+可能是一个持有*绝对*路径的 JSON 文件,「把这个目录整体拷过去」能
+满足一种,满足不了另一种。`mcpp pack` 把它写进 bundle 的
+`HOST-REQUIREMENTS`,`mcpp publish` 把它投影进描述符,两者出自同一份
+推导。可选要求仍然保留可见的来历信息,但不会成为硬性的 ABI 或 doctor
+输入。一条显式写成相对文件路径的 `libraries` 条目,相对声明它的包根
+目录解析;一个裸的逻辑名字仍然是一个按平台拼写的库名。`artifacts`
+要求 `role`、`path` 与 `provenance`;`abi`、`digest` 与
+`host_fingerprint` 是可选的佐证。是解析器而不是描述符,给每一条要求
+打上确切的请求方 PackageId,给每一个产物打上确切的声明提供方
+PackageId,包括命名空间、版本与来源/索引出处。因此一份描述符无法
+冒充另一个包,`alpha.backend` 永远不会被折叠进 `beta.backend`。
+
+只有 `provides` 会创建一个由描述符拥有的提供者事实。仅仅要求一项
+能力,永远不会让请求方成为它自己的提供者。一个显式的
+`[runtime.] provider=` 覆盖接受一个规范的
+`namespace.name@version`(或一个无歧义的兼容拼法);缺失、或短名字
+有歧义的提供者是硬错误。已被 xlings SubOS 选定的提供者/产物事实,
+优先于描述符的回落值。xlings/xim 拥有图形栈、驱动、ICD、WSL 与宿主
+出处的选择权;mcpp 只记录并消费那个通用结果,从不探测 GPU 硬件。
+
+Link intent 把各个发现阶段分开处理:
| 字段 | 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 |
-| `deploy` *(2026.9.12.2+)* | copy edge,复制到 `bin//` | copy edge,复制到 `bin//` | copy edge,复制到 `bin//`;绝不成为 linker flag |
-
-`deploy` 把文件放进相对可执行文件的目录;`deploy_files` 表达不了这一点,因为它把每一项都放在可执行
-文件旁。读取固定子目录的加载器需要它:macOS 上的 Vulkan loader 从 `<可执行文件目录>/vulkan/icd.d`
-读取驱动清单。每一项是恰好含两个字符串的表:`from` 相对声明它的包的根目录,`to` 相对可执行文件所在
-目录,`"."` 表示该目录本身。两者在所有宿主上都以 `/` 分隔,不得是绝对路径、不得指定盘符,也不得含
-空分量、`.` 或 `..` 分量;违反的项被拒绝,拒绝信息指出该项的序号。同一目标位置的两个来源被拒绝并指出
-目标位置,同名文件放进两个不同目录则不构成冲突。`deploy` 是独立的键而不是 `deploy_files` 的表形式:
-早于它的描述文件读取器在 `deploy_files` 中遇到 `{` 时不会终止,而对不认识的 `runtime` 键会跳过。`mcpp pack`
-把两个键的文件放到打包后可执行文件旁的同一相对位置。
-
-一个兼容发布周期内仍读取旧字段:`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` 判定:
+| `transitive_needed_dirs` | `-Wl,-rpath-link` | 无旗标 | 无旗标 |
+| `runtime_search_dirs` | 仅 RUNPATH/rpath,从不是 `-L` | 仅 rpath | 无旗标 |
+| `frameworks` | 无旗标 | `-framework` | 无旗标 |
+| `deploy_files` | 拷贝边 | 拷贝边 | 拷贝到输出旁边;从不是链接器旗标 |
+| `deploy` *(2026.9.12.2+)* | 拷贝边,进 `bin//` | 拷贝边,进 `bin//` | 拷贝边,进 `bin//`;从不是链接器旗标 |
+
+`deploy` 把一个文件放进相对可执行文件的某个目录,而 `deploy_files`
+表达不了这一点,因为它把每一条都放在可执行文件旁边。一个读取固定
+子目录的加载器需要它:macOS 上的 Vulkan loader 从
+`<可执行文件目录>/vulkan/icd.d` 读取驱动 manifest。每一条都是恰好
+两个字符串组成的表。`from` 相对声明它的包根目录,`to` 相对可执行
+文件所在目录,`"."` 意味着那个目录本身。两者在每个宿主上都以 `/`
+分隔,都不能是绝对路径、不能命名一个驱动器、不能含有空、`.` 或 `..`
+组成部分;不满足的条目会被拒绝,拒绝信息点名它的索引。两个来源指向
+同一个目的地会被拒绝并点名那个目的地,而一个文件名出现在两个不同
+目录下不算冲突。`deploy` 是一个独立的键,而不是 `deploy_files` 的
+表格形式,因为一个早于它出现的描述符读取器,遇到 `deploy_files` 里的
+`{` 会无法终止,而它会跳过一个不认识的 `runtime` 键。`mcpp pack` 把
+这两个键指向的文件,以打包出的可执行文件为参照,拷贝到同样的相对
+路径。
+
+对于一批兼容性字段,`library_dirs` 只映射到运行时搜索,`dlopen_libs`
+映射到必需的运行期 soname 要求,`capabilities` 映射到必需的运行期
+能力要求。这些遗留字段都不创建提供者。
+
+`target///resolution.json` schema 2 存储 RuntimeBinding、
+规范化后的要求/提供者/产物、LinkIntent、平台发现机制与链接后判定。
+`mcpp why runtime` 是对最新存储文件的一个纯粹解读器:它既不会重新
+解析 manifest,也不会启动一次图形/硬件探测。当被选中的宿主提供者
+自身需要重新诊断时,用 `xlings doctor`。
+
+每个产物还携带一个仅从路径计算出的 `identity` 判定:
| `identity` | 含义 |
|---|---|
-| `ok` | 声明的路径(穿过符号链接后)落在声明的那个版本里 |
-| `mismatch` | 它解析到了别处 —— **该 binding 已陈旧**,后来的某次安装把它重新指向了别的地方 |
-| `missing` | 声明了,但那个路径上什么都没有 |
-| `unverified` | 声明时没有可供比对的版本 |
+| `ok` | 声明的路径(经符号链接)解析到声明的版本 |
+| `mismatch` | 它解析到了别处——**绑定已过期**,某次更晚的安装重新指向了它 |
+| `missing` | 已声明,但那个路径上什么都没有 |
+| `unverified` | 声明时没有给出可供核对的版本 |
-这就是 mcpp 早已施加于私有 libc 的那条规则的推广(`glibc@2.44` 解析到那一份载荷;
-陈旧或缺失是错误,而绝不是「已安装版本里哪个看起来能用就用哪个」)。它不需要知道
-该 artifact 做什么。`unverified` **有意**不等于 `ok`:一个解析到了却没有 artifact
-在其背后的 provider 并未被核验过,因此 `mcpp why runtime` 打印
-`(not declared by the environment — nothing to verify)` 而不是 `(none)`。
+这正是 mcpp 已经在对私有 libc 应用的那条规则(`glibc@2.44` 解析到
+那一份载荷;过期或缺失是错误,绝不是「随便一个装着的、看起来能用的
+版本」)的推广。它不需要知道这个产物是做什么的。`unverified` 刻意
+不等于 `ok`:一个已解析、背后却没有产物的提供者尚未被核对过,
+`mcpp why runtime` 会说
+`(not declared by the environment — nothing to verify)`,
+而不是 `(none)`。
-能力名使用分层小写 `domain.sub.role`(如 `display.present`)和前缀类
-`abi:`(如 `abi:glibc`,参与工具链 ABI 强制)。
+能力名使用分层的小写 `domain.sub.role`(例如 `display.present`)与
+前缀式的 `abi:`(例如参与工具链 ABI 强制检查的 `abi:glibc`)。
-### 2.12 `[package] platforms` — 平台声明
+### 2.12 `[package] platforms` —— 平台声明
```toml
[package]
platforms = ["linux", "macos", "windows", "ios", "android", "emscripten"]
```
-声明包支持的平台(CI 矩阵提示,经 `mcpp why` 展示)。词表由 mcpp 固定
-(它拥有 target/triple 体系):`linux | macos | windows | ios | android |
-emscripten`(mcpp 2026.9.12.3+;`ios`、`android`、`emscripten` 是新加入的——
-此前的词表是 `linux | macos | windows`);未知值 warning,`--strict` 下报错。
+声明这个包支持的平台(一条 CI 矩阵提示,经 `mcpp why` 展示)。词汇由
+mcpp(拥有 target/triple 体系的一方)固定:
+`linux | macos | windows | ios | android | emscripten`
+(mcpp 2026.9.12.3+;`ios`、`android` 与 `emscripten` 是这套此前只有
+`linux | macos | windows` 的词汇新增的成员);未知取值会产生警告,
+`--strict` 下是错误。
-一个平台名就是目标三元组的 `os`,除非某个 `env` 自己命名了一个平台。Android
-各行的 `os = "linux"`、`env = "android"`,所以这份列表里的 `linux` 不覆盖
-它们——服务 Android 的包要另写 `android`。Web 行保留自己的 `os` 单词
-`emscripten`,与 `cfg(...)` 选择器语法用的是同一个词;不存在 `web` 这种拼法。
+一个平台名就是目标三元组的 `os`,除非一个自己命名了平台的 `env`
+优先。Android 各行的 `os = "linux"`、`env = "android"`,所以这份
+列表里的 `linux` 不覆盖它们——一个服务 Android 的包要额外写出
+`android`。Web 这一行保留自己的 `os` 单词 `emscripten`,与
+`cfg(...)` 选择器语法用的是同一个词;没有 `web` 这种拼法。
-对库目标执行 `mcpp pack` 时,会拿这条声明与**实际产出的腿**核对 —— 那是第一个
-有证据可核的时刻:
+`mcpp pack` 在一个库目标上,会拿这条声明与它实际产出的那些腿核对,
+因为这是第一个有证据可供核对的时刻:
-| 情况 | 结果 |
+| 情形 | 结果 |
|---|---|
-| 某条腿的平台不在此列 | warning —— manifest 否认了一个包明明能服务的平台 |
-| 声明了某平台却没有对应的腿,**且本宿主本来就能构建它** | warning —— 该平台的消费者会解析到这个包却找不到产物 |
-| 声明了某平台却没有对应的腿,而本宿主根本构建不了它 | **不说话** |
+| 某条腿为一个这里没列出的平台打了包 | 警告——manifest 否认了一个这个包明显在服务的平台 |
+| 一个列出的平台没有对应的腿,**并且这台宿主本可以构建出一份** | 警告——那里的消费者会解析到这个包,却找不到产物 |
+| 一个列出的平台没有对应的腿,但这台宿主构建不出那个平台的产物 | **无声** |
-第三行才是这个检查可用的原因。正常的发布流程是 CI 上每平台各跑一次
-`mcpp pack`,于是 Linux runner 永远不会产出 macOS 腿 —— 为此告警会在每个跨平台
-包的每一次运行中触发,而**永远触发的告警会把真正该看的那条盖掉**。「本宿主能不能
-构建」与 `--target` 回答的是同一个问题(docs/08 §7.4)。
+第三行正是这项检查之所以可用的原因。正常的发布流程是在 CI 里为每个
+平台各跑一次 `mcpp pack`,所以一台 Linux runner 从不会产出一条
+macOS 的腿——如果对此发出警告,会在每个跨平台包的每一次运行上触发,
+而一条总是触发的警告会掩盖真正要紧的那一条。「这台宿主本可以构建」
+指的是与 `--target` 回答的同一个问题(docs/08 §7.4)。
-两者都只是 warning,绝不报错:覆盖度属于发布纪律,而能作判断的人看的是发布,
-不是这一次构建。
+两者都只是警告,从不是错误:覆盖度是发布纪律的一部分,能判断它的人
+是在看发布本身,而不是在看这一次构建。
-### 2.12b `[package] accelerators` — 加速器声明
+### 2.12b `[package] accelerators` —— 加速器声明
```toml
[package]
accelerators = ["cuda", "rocm"]
```
-声明该包支持的加速器后端。与 `platforms` 同形:一个意图声明与 CI 矩阵提示,
-由 `mcpp why` 展示,**不是门**。
+声明这个包支持的加速器后端。与 `platforms` 对称:一条意图陈述与一条
+CI 矩阵提示,经 `mcpp why` 展示,从不是一道闸。
+
+它与产物的 `accel` 字段刻意区分开。声明是手写的,可以是愿景;`accel`
+是从产出某个二进制的那次构建里实测得到的,也是消费者被拒绝时所对照
+的那个值。见[42 —— 异构构建](42-heterogeneous-builds.md)。
-与产物的 `accel` 字段刻意不同。声明由人手写、可以是期望值;`accel` 是从产生该二进制的
-那次构建测量出来的,并且是消费者被拒绝时所依据的东西。见
-[42 — 异构硬件构建](42-heterogeneous-builds.md)。
### 2.13 `[xlings]` —— 工程的环境
-已移入 [23 —— 项目环境](23-the-project-environment.md)。
+已移至 [23 —— 工程的环境](23-the-project-environment.md)。
### 2.14 依赖产出的 host 工具
-已移入 [30 —— build.mcpp](30-build-mcpp.md)。
-
+已移至 [30 —— build.mcpp](30-build-mcpp.md)。
-### 2.15 `[resources]` —— 编译进产物的元数据与资产(2026.8.7.1+)
+### 2.15 `[resources]` —— 编译进产物的元数据与资产(2026.8.7.1+)
-exe 图标,以及 Windows 在文件「属性」里显示的版本信息,就是 `mcpp.toml` 里的一个路径:
+一个 exe 图标,以及 Windows 在文件属性对话框里展示的版本元数据,在
+`mcpp.toml` 里不过是一条路径,别无其它:
```toml
[resources]
icon = "assets/app.ico"
```
-常见场景到此为止。`FILEVERSION`、`ProductName`、`FileDescription`、`CompanyName`、
-`LegalCopyright` 全部从 `[package]` 取默认值,资源脚本由 mcpp 生成。
+这就是常见情形的全部。`FILEVERSION`、`ProductName`、
+`FileDescription`、`CompanyName` 与 `LegalCopyright` 全都从
+`[package]` 取默认值,资源脚本由 mcpp 自动生成。
| 键 | 类型 | 含义 |
|---|---|---|
-| `icon` | 路径 | 作为应用图标嵌入(资源序号 1) |
-| `files` | 路径列表 | 工程自带的 `.rc` 脚本,mcpp 编译并**跟踪**为构建输入 |
-| `extra-inputs` | 路径列表 | `.rc` 扫描器看不见的输入(见下) |
-| `version-info` | 布尔 | `false` 表示不要生成版本资源 |
+| `icon` | 路径 | 作为应用图标嵌入(资源序号 1) |
+| `files` | 路径列表 | 自行编写的 `.rc` 脚本,会被编译并**追踪**为构建输入 |
+| `extra-inputs` | 路径列表 | `.rc` 扫描器看不见的输入(见下) |
+| `version-info` | 布尔值 | `false` 退出自动生成版本资源 |
| `[resources.version-info]` | 表 | `company`、`product`、`description`、`copyright`、`original-filename`、`internal-name` |
-**只有 PE 目标会*编译*这一节。** 在 Linux/macOS 上它**不适用**:不产资源单元、
-不出诊断、构建逐字节不变。**无需**(也不能)加 `cfg(windows)` 谓词 ——
-无条件写一次即可。
+**只有 PE 目标会*编译*它。** 在 Linux 与 macOS 上,这一节*不适用*:
+没有资源单元、没有诊断、构建逐字节相同。**不需要**(也不能用)一个
+`cfg(windows)` 谓词——写一次,无条件生效即可。
-**声明了却不存在的文件会让构建失败 —— 在每个目标上都是。** 资源和源码一样是
-构建输入;mcpp 不会悄悄产出一个缺了它的二进制。校验刻意**不**按 PE 设门:
-路径是否存在是关于工作树的事实,不是关于目标的事实,所以 `icon = "assets/app.ico"`
-里的拼写错误由 Linux/macOS 构建(以及对应的 CI job)当场抓住,而不是等
-Windows 那条。不想要图标,把那一行删掉。
+**一个声明了却不存在的文件会让构建失败——在每一个目标上都一样。**
+一份资源和一个源文件一样是构建输入;mcpp 不会悄悄发布一个缺了它的
+二进制。校验刻意**不**只针对 PE:一个路径是否存在,是工作树的一个
+事实,与目标无关,所以 `icon = "assets/app.ico"` 里的一个笔误,会被
+Linux 或 macOS 的构建(以及它们的 CI 作业)捕获,而不必等到 Windows
+那一份。要省略图标,删掉这一行即可。
-**版本字段。** `FILEVERSION` 取 `[package].version` 的四段数值,每段必须放得进
-16 位;字符串字段保留版本原文,所以数值字段装不下的形态(`1.0.0-rc1`)在属性
-对话框里照样看得到。
+**版本字段。** `FILEVERSION` 取 `[package].version` 的四个数字段,
+每一段都必须落在 16 位以内;字符串字段按原样保留版本号,所以一个
+数字字段容纳不了的形式(`1.0.0-rc1`),仍然会原样出现在属性对话框里。
#### 自写 `.rc`
@@ -1310,37 +1473,41 @@ Windows 那条。不想要图标,把那一行删掉。
files = ["res/app.rc"]
```
-写了 `files`,mcpp 就不再生成版本资源 —— 资源 ID 空间由工程自行支配。两者都需要时同时写
-`version-info = true`(注意冲突:序号 1 的 `RT_VERSION` 只能有一个)。
+设置了 `files` 之后,mcpp 停止生成版本资源,资源 ID 空间归工程所有。
+若两者都要,连同它一起设 `version-info = true`(注意会相撞:序号 1
+上只能有一个 `RT_VERSION`)。
-想从生成的脚本起步而不是从空文件起步:把它从构建目录里拷出来
-(`target///res/.mcpp.rc`)填进 `files`。结果**字节相同**,
-所以从「生成」走到「手写」不会改变产物。
+要从生成的脚本出发,而不是从一个空文件开始,把它从构建目录里拷出来
+(`target///res/.mcpp.rc`)并列进 `files`。结果
+逐字节相同,所以从生成切换到手写,不会改变实际发布的内容。
-> **`VS_VERSION_INFO` 需要 ``。** 手写脚本里如果写
-> `VS_VERSION_INFO VERSIONINFO` 而没有 `#include `,版本资源会被存成
-> **字符串名**而不是序号 1。所有工具依然报告 `Type: VERSIONINFO`,但
-> `GetFileVersionInfo` 查的是序号,于是 PowerShell 的 `FileVersionInfo` 里每个字段
-> 都是空的。要么 include ``,要么直接写 `1 VERSIONINFO`。mcpp 见到这个
-> 形状会警告;它自己生成的脚本用的是字面 `1`。
+> **`VS_VERSION_INFO` 需要 ``。** 在一份手写脚本里,
+> `VS_VERSION_INFO VERSIONINFO` 若没有 `#include `,会把
+> 版本资源归档到一个*字符串*名字下,而不是序号 1。每个工具仍会报告
+> `Type: VERSIONINFO`,但 `GetFileVersionInfo` 查的是序号,所以
+> PowerShell 的 `FileVersionInfo` 会显示每个字段都是空的。要么包含
+> ``,要么写 `1 VERSIONINFO`。mcpp 看到这种写法时会警告;
+> 它自己生成的脚本用的是字面量 `1`。
#### 被跟踪的输入
-mcpp 会读 `.rc`,把引号形式的 `#include` 和资源语句(`ICON`、`RCDATA`、
-`MANIFEST` …)点名的文件都变成构建输入,所以改图标会重链。尖括号形式
-(``)属于工具链,由工具链 fingerprint 覆盖。
+mcpp 读取 `.rc` 里带引号的 `#include`,以及资源语句(`ICON`、
+`RCDATA`、`MANIFEST` 等)命名的文件,并把它们变成构建输入,所以修改
+图标会触发重新链接。尖括号 include(``)属于工具链,由
+工具链指纹覆盖,而不是这项扫描。
-通过宏间接引用的文件名(`1 ICON APP_ICON`)扫描看不见。mcpp 会**指名**它没能解析
-的东西,并要求显式声明:
+一个经由宏到达的文件名(`1 ICON APP_ICON`)对这项扫描是不可见的。
+mcpp 会点名它无法解析的部分,并要求显式声明:
```toml
extra-inputs = ["assets/app.ico"]
```
-#### 其余一切:`role = "object"`
+#### 其余一切:`role = "object"`
-不是资源脚本的输入 —— `objcopy` 嵌入的 blob、生成的 `.def`、预编译对象 ——
-可以由构建程序声明一个产出接到链接的图节点:
+对于不是资源脚本的输入——一段用 `objcopy` 嵌入的二进制数据、一份
+生成的 `.def`、一个预构建的对象文件——构建程序可以声明一个构建图
+节点,把它的输出并入链接:
```cpp
mcpp::action o;
@@ -1348,46 +1515,49 @@ 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") // 省略:接到每个镜像,含测试二进制
+ .target("myapp") // omit: every image, test binaries included
.submit();
```
-见 [30 — build.mcpp](30-build-mcpp.md)。把这类文件写进 `[build].ldflags` 也「能用」,
-但 ldflags 是链接命令里的一串字符:没有任何东西跟踪它,改了它得到的是
+见 [30 —— build.mcpp](30-build-mcpp.md)。把这样一个文件名写进
+`[build].ldflags` 也「能用」,但 ldflags 在链接命令里是一段扁平
+字符串:没有任何东西追踪它,修改这个文件会得到
`ninja: no work to do`。
+
### 2.16 `[hooks]` —— 项目构建生命周期命令
-已移入 [09 —— 按场景选命令](09-commands-by-scenario.md)。
+已移至 [09 —— 按场景选命令](09-commands-by-scenario.md)。
### 2.17 `[test]` —— 测试程序的位置
```toml
[test]
-discover = ["tests/**/*.cpp"] # 默认值
+discover = ["tests/**/*.cpp"] # the default
```
| 键 | 类型 | 含义 |
|---|---|---|
-| `discover` | glob 数组 | glob 匹配到的每个文件都是一个测试程序;以 `!` 开头的 glob 去掉它匹配到的文件;`[]` 不发现任何测试 |
-
-glob 使用 `[build] sources` 的词汇。测试的名字是它相对于第一个匹配它的 glob 的固定
-目录的路径,去掉扩展名。同名的两个文件会被拒绝,并点名两者。值不是非空字符串数组时
-报错;`[test]` 中的其他键给出警告,在 `--strict` 下为错误。测试模型见
-[08 —— 测试](08-testing.md)。
+| `discover` | glob 数组 | 每个被某条 glob 匹配到的文件都是一个测试程序;以 `!` 开头的 glob 会移除它匹配到的文件;`[]` 不发现任何测试 |
+这些 glob 使用与 `[build] sources` 相同的词汇。一个测试的名字,是它
+相对第一条匹配到它的 glob 所在的固定目录的路径,去掉扩展名。两个
+同名文件会被拒绝,并点名两者。一个不是「非空字符串数组」的取值是
+一个错误;`[test]` 里的其它任何键都是警告,`--strict` 下是错误。
+测试模型见[08 —— 测试](08-testing.md)。
## 3. 实战示例
-其中四个是**可运行的工程**而不是片段,而工程是更好的答案:它能构建,而且由 CI 检查。
+其中四个是可运行的工程,而不是片段,工程是更好的答案:它能构建,并且
+由 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/) |
+| 一个带测试的模块库 | [`examples/11-features`](../../examples/11-features/) |
+| 一个带依赖的应用 | [`examples/02-with-deps`](../../examples/02-with-deps/) |
+| 一次交叉编译的静态发布 | [`examples/03-pack-static`](../../examples/03-pack-static/) |
-还有两种形态暂时没有对应示例,以 manifest 的形式留在这里。
+有两种形态目前还没有示例,以 manifest 的形式留在这里。
### 3.4 纯 C 库
@@ -1405,7 +1575,7 @@ sources = ["src/**/*.c"]
kind = "lib"
```
-### 3.5 混合 C / C++23 模块项目
+### 3.5 混合 C / C++23 模块工程
```toml
[package]
@@ -1417,34 +1587,34 @@ include_dirs = ["include"]
c_standard = "c11"
[dependencies]
-lua = "5.4.7" # 纯 C 库,mcpp 自动用 C 编译器编译 .c 文件
+lua = "5.4.7" # Pure C library; mcpp compiles .c files with the C compiler automatically
[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` 自动发现;`[test] discover` 替换这个集合 |
-| 依赖命名空间 | `mcpplibs`(默认) | 裸 selector 只表示该精确 ns |
+| 入口点 | `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` 自动发现;`[test] discover` 替换这个集合 |
+| 依赖命名空间 | `mcpplibs`(默认) | 一个裸选择器只匹配这一个精确命名空间 |
### 4.1 旧 `[language]` 兼容层
-旧配置仍可读取:
+旧的配置仍然可以被读取:
```toml
[language]
standard = "c++26"
```
-新项目请使用 `[package].standard`。如果两个位置都出现,`[package].standard` 是权威配置。
+新工程应使用 `[package].standard`。若两处都写了,以
+`[package].standard` 为准。
diff --git a/docs/zh/05-dependencies.md b/docs/zh/05-dependencies.md
index 6b7111def..654923815 100644
--- a/docs/zh/05-dependencies.md
+++ b/docs/zh/05-dependencies.md
@@ -1,22 +1,22 @@
# 05 —— 依赖与解析
-**读者:**构建里已经不只有自己代码的作者。
+**读者:** 构建里已经不只有自己代码的作者。
-**本章回答的那一个问题:**一个依赖从哪里来,版本约束是什么意思,以及两个约束
-不一致时会发生什么。
+**本章回答的那一个问题:** 一个依赖从哪里来,一条版本约束是什么意思,以及两条
+约束不一致时会发生什么。
-**不在这里:**什么使两个包成为同一个包 —— 那是
-[SPEC-001](../specs/package-identity.md),本章施用它而不复述它;以及怎么发布一个包,
-那是 [11 —— 发布一个库](11-publishing-a-library.md)。
+**不在这里:** 什么使两个包成为同一个包 —— 那是
+[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) 讲一个依赖
-怎么变成可选的。
+在此之前:[04 —— mcpp.toml 工程文件指南](04-mcpp-toml.md) 是这些表与其余表同处
+的地方。在此之后:[06 —— Feature 与能力](06-features-and-capabilities.md) 讲
+一个依赖怎样变成可选的。
-## `[dependencies]` — 运行时依赖
+## `[dependencies]` —— 运行时依赖
-**推荐写法是带精确版本的 dotted selector。** dotted 形式点名的是**一个身份** ——
-最后一段是包名,之前所有段都是 namespace —— 因此解析到什么,不取决于当前配置了
+**推荐写法是带精确版本的点式选择器。** 点式形式点名的是**一个身份** —— 最后
+一段是包名,之前所有段都是 namespace —— 因此解析到什么,不取决于当前配置了
哪些 namespace。
```toml
@@ -27,19 +27,19 @@ imgui.backend.glfw_opengl3 = "0.0.1"
mcpplibs.capi.lua = "0.0.3"
```
-裸名也被接受,它经由默认 namespace(`mcpplibs`)解析 —— 在只用那一个 namespace 的
-工程里方便,在不是的工程里有歧义:
+裸名也被接受,它经由默认 namespace(`mcpplibs`)解析 —— 在只用那一个
+namespace 的工程里方便,在不是的工程里有歧义:
```toml
[dependencies]
-cmdline = "0.0.2" # 解析为 mcpplibs.cmdline
+cmdline = "0.0.2" # resolves to mcpplibs.cmdline
```
-等价拼法:namespace 子表
+等价拼法:namespace 子表
-把同一 namespace 下的条目归组。它解析到的身份与 dotted 形式完全相同,在许多依赖
-共享一个 namespace 时值得用。
+把同一 namespace 下的条目归组。它解析到的身份与点式形式完全相同,在许多依赖
+共享一个 namespace 时值得使用。
```toml
[dependencies.mcpplibs]
@@ -48,59 +48,61 @@ tinyhttps = "0.2.2"
llmapi = "0.2.5"
[dependencies.compat]
-glfw = "3.4" # 显式 namespace,不做回退搜索
+glfw = "3.4" # Explicit namespace; no fallback search
```
```toml
-# 路径依赖(本地开发)
+# Path dependency (local development)
[dependencies]
mylib = { path = "../mylib" }
```
```toml
-# Git 依赖 —— tag / branch / rev 三选一
+# 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
-# 长式 dep spec:features 与 backend 旋钮
+# Long-form dep spec: features and backend knobs
[dependencies]
-imgui = { version = "0.0.3", features = ["docking"] } # 请求该依赖的 feature
-widget = { version = "1.0", backend = "glfw_opengl3" } # 糖:= features=["backend-glfw_opengl3"]
+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 = ""` 是**通用约定糖**:1:1 脱糖为请求该依赖的 `backend-`
-feature(库若支持该旋钮,应在自己的 `[features]` 中声明 `backend-*` 系列)。
-若目标包声明了 `[features]` 但不含所请求的 feature(含 backend 脱糖结果),
-默认给出 warning,`mcpp build --strict` 下报错。
+`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` 或换一台机器
-都不会静默切换到更新的分支头。需要新的分支头时,必须显式指定:
+**Git 依赖与 `mcpp.lock`**:`tag` 和 `rev` 已经指向历史中的一个固定点,而
+`branch` 是会移动的。首次构建把分支解析成一个 commit 并写进 `mcpp.lock`,
+此后每次构建都重新构建**那个** commit —— lock 是权威来源而不是缓存提示,
+所以删掉 `~/.mcpp/git` 或换一台机器,都不会静默切换到更新的分支头。要用新的
+分支头,必须显式请求:
```bash
-mcpp update mylib # 丢掉记录的 commit,下次构建重新解析
-mcpp update # 同上,对所有依赖
+mcpp update mylib # drop the recorded commit; the next build re-resolves it
+mcpp update # same, for every dependency
```
-既然记录的 commit 已经足以决定构建什么,那么在 `~/.mcpp/git` 里已有克隆的情况下,
-重新构建完全不发网络请求,`--offline` 下照常工作。只有两件事需要网络:解析一个在
-lock 里没有 commit 的分支,以及克隆一个尚未缓存的 commit。`git =` 若指向本地目录
-(或 `file://` URL),这两件事都不需要网络,因此离线下也绝不会被拒绝。
+既然记录下来的 commit 已经足以决定构建什么内容,那么在 `~/.mcpp/git` 里已有
+克隆的情况下,重新构建完全不发出网络请求,在 `--offline` 下照常工作。只有两件
+事需要网络:解析一个在 lock 里没有 commit 的分支,以及克隆一个尚未缓存的
+commit。`git =` 指向本地目录(或 `file://` URL)时,这两件事都不需要,因此
+离线状态下从不会被拒绝。
**SemVer 约束**:
```toml
[dependencies]
-foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret,默认)
+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" # 精确匹配
-qux = ">=1.0, <2.0" # 范围组合
+baz = "=1.2.3" # Exact match
+qux = ">=1.0, <2.0" # Range combination
```
### `visibility` —— 一个依赖的用法是否跨越本包自己的边界
@@ -110,57 +112,60 @@ qux = ">=1.0, <2.0" # 范围组合
sdk = { version = "1.0", visibility = "private" }
```
-每条依赖边都带一个 `visibility`,默认 `public`。它决定该依赖对消费方提出的要求
-——`provides`/`requires` 之外,头文件目录、宏定义与 flag——只到达本包自己的
-翻译单元,还是同时到达本包的**消费方**。
+每条依赖边都带一个 `visibility`,默认 `public`。它决定该依赖对消费方提出的
+要求 ——`provides`/`requires` 之外,头文件目录、宏定义与 flag —— 只到达本包
+自己的翻译单元,还是同时到达本包的**消费方**。
| 取值 | 本包自己的翻译单元 | 本包的消费方 |
|---|---|---|
-| `public`(默认) | 是 | 是 |
+| `public`(默认) | 是 | 是 |
| `private` | 是 | **否** |
| `interface` | 否 | 是 |
-`private` 是实现细节的常规情形:一个被 vendor 进来的库,或者一个包为实现某个
-功能而需要、但自己的接口并不暴露的平台 SDK。`interface` 是更少见的反向情形——
-本包自己的头文件会 `#include` 这个依赖,但本包从不编译链接它的目标文件。
-`public` 是大多数依赖想要的:依赖的某个类型出现在本包自己的公开头文件里,
-消费方就需要同一条头文件搜索路径才能用到它们。
+`private` 是实现细节的常规情形:一个被 vendor 进来的库,或者一个包为实现
+某项功能而需要、但自己的接口并不暴露的平台 SDK。`interface` 是更少见的反向
+情形 —— 本包自己的头文件会 `#include` 这个依赖,但本包从不编译链接它的目标
+文件。`public` 是大多数依赖想要的:依赖的某个类型出现在本包自己的公开头文件
+里,消费方就需要同一条头文件搜索路径才能用到它们。
-在某一个方向上写错是静默的(不必要的 `public` 把没人要的头文件广播出去),
-在另一个方向上则是构建失败——在第一个需要 `private` 所隐藏之物的消费方那里
-——这是更安全的失败方式,也是为什么 `[feature-deps.]`
-(见 06 —— "平台 SDK 依赖保持私有")要显式写出 `private`,而不是依赖一个
-恰好在没有消费方之前都能用的默认值。
+在其中一个方向上写错是静默的(不必要的 `public` 把没人要的头文件广播
+出去),在另一个方向上则是构建失败 —— 发生在第一个需要 `private` 所隐藏之物
+的消费方那里 —— 这是更安全的失败方式,也是为什么 `[feature-deps.]`
+(见 [06 ——「平台 SDK 依赖保持私有」](06-features-and-capabilities.md#平台-sdk-依赖保持私有))
+要显式写出 `private`,而不是依赖一个恰好能用、直到有消费方加入才会露馅的
+默认值。
### 同一依赖两条声明冲突的处理
依赖图里的两条边可能指向同一个身份 —— 同一个 `(namespace, name)` 二元组 ——
-却来自图中两个不同的位置:根 manifest 与某个依赖自己的 `[dependencies]`,或者
-两个互不相干的依赖。一条声明有一个**种类**(`version`、`git` 或 `path`)和该种
-类下的一个**引用**(一条 SemVer 约束、一个 `git` URL 加 `rev`/`tag`/`branch`,
-或一个文件系统路径)。mcpp 只解析这个身份一次;每个请求方的边都会被记录,而
-其中一个请求方的声明决定了同一身份下所有其他请求方拿到的是什么。
+却来自图中两个不同的位置:根 manifest 与某个依赖自己的 `[dependencies]`,
+或者两个互不相干的依赖。一条声明有一个**种类**(`version`、`git` 或
+`path`)和该种类下的一个**引用**(一条 SemVer 约束、一个 `git` URL 加
+`rev`/`tag`/`branch`,或一个文件系统路径)。mcpp 只解析这个身份一次;每个
+请求方的边都会被记录,其中一个请求方的声明决定了同一身份下所有其他请求方
+拿到的是什么。
| 第一条声明 | 第二条声明 | 结果 |
|---|---|---|
-| 任意 | 同种类、同引用 | 不变:第二条声明成为指向已解析身份的一条边,不报告任何信息。 |
-| `version` | `version`,不同的约束 | 按上文所述用 SemVer 对两个约束做 AND 合并;无法满足的一对被拒绝,并点名两个约束与两个请求方。 |
-| `git` | `git`,不同的 `rev`/`tag`/`branch` | 若根是两个请求方之一,根的声明胜出;否则先解析出来的声明胜出。`dependency/source-override` 警告点名两个请求方与两个引用,说明哪一个被采用、原因是什么,以及如何取用另一个。这个警告绝不会被吞掉。 |
-| `path` | `path`,不同的目录 | 规则与警告同上一行,只是比较的是规范化后的绝对目录而不是 git 引用。 |
-| 根的 `path` 或 `git` 声明 | 某个依赖的 `git` 或 `version` 声明(种类冲突) | 根的声明胜出,警告同上。若败下阵的声明是一条 `version` 需求,它会被拿去与根已解析出的那份 checkout 的 `[package] version` 核对;需求被违反就拒绝,并点名那个 pin、请求方与需求本身。 |
-| 某个依赖的 `path`/`git` 声明 | 另一个依赖的不同种类的声明(种类冲突,且双方都不是根) | 拒绝:"requested as both a … dep … and a … dep …. Pick one."。消息多出一句:在根里声明该身份即可解决。 |
-
-这里根所拥有的特权,与 `linkage` 只在根 manifest 自己的边上生效(见上文
-`[dependencies]`)是同一条边界:一个身份最终解析到*哪一份 checkout*,是一个
-只有构件自己的 manifest 才能替某个依赖悄悄做出的整图级决定。两个依赖互相冲
-突、且都不是根的情形,绝不会靠猜哪个先被声明来解决 —— 那正是本节要替换掉的
-"队列顺序的意外"。
-
-### `path` 与 `git` 依赖的身份(mcpp 2026.9.14.2+)
-
-`path` 或 `git` 依赖就是它的清单所声明的那个包,与指向它的键无关。一个键规范化后的
-身份若不同于清单 `[package]` 的 `namespace` 与 `name`,就采用清单声明的身份;mcpp
-对每条声明边告警一次,点名请求方、键、键所指的身份与清单声明的身份:
+| 任意 | 同种类、同引用 | 不变:第二条声明成为指向已解析身份的一条边,不报告任何信息。 |
+| `version` | `version`,不同的约束 | 按上文所述,用 SemVer 对两个约束做 AND 合并;无法满足的一对被拒绝,并点名两个约束与两个请求方。 |
+| `git` | `git`,不同的 `rev`/`tag`/`branch` | 根是两个请求方之一时,根的声明胜出;否则先解析出来的声明胜出。`dependency/source-override` 警告点名两个请求方与两个引用,说明哪一个被采用、原因是什么,以及如何取用另一个。这个警告绝不会被吞掉。 |
+| `path` | `path`,不同的目录 | 规则与警告同上一行,只是比较的是规范化后的绝对目录,而不是 git 引用。 |
+| 根的 `path` 或 `git` 声明 | 某个依赖的 `git` 或 `version` 声明(种类冲突) | 根的声明胜出,警告同上。若败下阵的声明是一条 `version` 需求,它会被拿去与根已解析出的那份 checkout 的 `[package] version` 核对;需求被违反就拒绝,并点名那个 pin、请求方与需求本身。 |
+| 某个依赖的 `path`/`git` 声明 | 另一个依赖的不同种类的声明(种类冲突,且双方都不是根) | 拒绝:"requested as both a … dep … and a … dep …. Pick one."。消息多出一句:在根里声明该身份即可解决。 |
+
+这里根所拥有的特权,与 `linkage` 只在根 manifest 自己的边上生效(见上文
+`[dependencies]`)是同一条边界:一个身份最终解析到*哪一份 checkout*,是一个
+只有构件自己的 manifest 才能替某个依赖悄悄做出的整图级决定。两个依赖互相
+冲突、且都不是根的情形,绝不会靠猜哪个先被声明来解决 —— 那正是本节要替换掉
+的"队列顺序的意外"。
+
+### `path` 或 `git` 依赖的身份(mcpp 2026.9.14.2+)
+
+`path` 或 `git` 依赖就是它的 manifest 所声明的那个包,与指向它的键无关。一个
+键规范化后的身份,若不同于 manifest `[package]` 的 `namespace` 与 `name`,
+就采用 manifest 声明的身份;mcpp 对每条声明边告警一次,点名请求方、键、键所指
+的身份,以及 manifest 声明的身份:
```toml
# comp/mcpp.toml; fw/mcpp.toml declares namespace = "huxdemo"
@@ -173,67 +178,73 @@ warning: 'huxdemo.comp@path' declares the dependency 'fw', which names mcpplibs.
hint: write 'huxdemo.fw' in 'huxdemo.comp@path' to state the identity the manifest declares.
```
-因此同一目录上分别写作 `fw` 与 `huxdemo.fw` 的两条边是同一个包,只编译一次,
-`mcpp why deps` 在它下面列出两个键。未声明命名空间的清单取键的命名空间,于是在这样
-一个目录上用两个不同命名空间的键,就是同一来源上的两个身份:构建在扫描之前被拒绝,
-点名二者;修正方式是在该清单中声明 `namespace`,或两处写同一个键。`version` 依赖
-不受影响,它的身份就是键。
+因此,同一目录上分别写作 `fw` 与 `huxdemo.fw` 的两条边是同一个包,只编译
+一次,`mcpp why deps` 在它下面列出两个键。未声明命名空间的 manifest 取键的
+命名空间,于是在这样一个目录上使用两个不同命名空间的键,就是同一来源上的两个
+身份:构建在扫描之前被拒绝,点名二者;修正方式是在该 manifest 中声明
+`namespace`,或两处写同一个键。`version` 依赖不受影响,它的身份就是键。
-### git 仓库中的一个包(mcpp 2026.9.16.1+)
+### git 仓库中的一个包(mcpp 2026.9.16.1+)
-`git` 依赖指向一个仓库,键说明指的是仓库里的哪一个包。根清单的包是其一;根清单
-`[workspace] members` 的每一项是另一个。键的身份不是根包时,选中清单声明该身份的那个
-member,提交相同:
+`git` 依赖指向一个仓库,键说明指的是仓库里的哪一个包。根 manifest 的包是
+其一;根 manifest `[workspace] members` 的每一项是另一个。当键的身份不是
+根包时,选中 manifest 声明该身份的那个 member,取同一个提交:
```toml
-# repo/mcpp.toml 声明 spike.fw 与 [workspace] members = ["tool"];
-# repo/tool/mcpp.toml 声明 spike.fw-installer
+# repo/mcpp.toml declares spike.fw and [workspace] members = ["tool"];
+# repo/tool/mcpp.toml declares spike.fw-installer
[dependencies]
spike.fw = { git = "https://example.org/fw.git", rev = "cc3c74c5" }
spike.fw-installer = { git = "https://example.org/fw.git", rev = "cc3c74c5", tools = ["fw-installer"] }
```
-- member 继承仓库的 `[workspace.package]`,与从仓库自己的检出构建时相同。
-- member 中留在克隆目录之内的 `path` 边(例如 `spike.fw = { path = ".." }`)指向同一
- git 源的同一提交,因此它就是根的键解析到的那个包,而不是第二条以 path 为源的声明。
-- 既不指根包也不指任何 member 的键沿用上一节的规则:使用根清单的身份,并给出警告。
+- member 继承仓库的 `[workspace.package]`,与从仓库自己的检出构建时相同。
+- member 中留在克隆目录之内的 `path` 边(例如 `spike.fw = { path = ".." }`)
+ 指向同一 git 源的同一提交,因此它就是根的键解析到的那个包,而不是第二条
+ 以 path 为源的声明。
+- 既不指根包、也不指任何 member 的键,沿用上一节的规则:使用根 manifest 的
+ 身份,并给出警告。
-键按身份选择,不存在 `subdir` 键:较旧的客户端会忽略这样的键,并不声不响地构建根包。
+键按身份选择,不存在 `subdir` 键:较旧的客户端会忽略这样的键,并不声不响地
+构建根包。
### 命名空间解析规则
-每个包的身份是**命名空间 + 名字**二元组。每个 selector 都只规范化成一个身份:
+每个包的身份是**命名空间 + 名字**二元组。每个选择器都只规范化成一个身份:
-- `cmdline` → `(mcpplibs, cmdline)`;省略 namespace 只表示默认 `mcpplibs`。
+- `cmdline` → `(mcpplibs, cmdline)`;省略 namespace 只表示默认 `mcpplibs`,
+ 不表示别的。
- `compat.gtest` → `(compat, gtest)`。
- `mcpplibs.capi.lua` → `(mcpplibs.capi, lua)`。
-不存在有序回退或按短名的全索引模糊搜索:
+不存在有序回退,也不存在按短名的全索引模糊搜索:
```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"
-# 错误 —— 裸名永远到不了 chriskohlhoff 命名空间
+# Wrong — a bare name never reaches the `chriskohlhoff` namespace
[dependencies]
asio = "1.38.1"
```
-第三种写法会明确报错,指出实际尝试的 `(mcpplibs, asio)`;若该短名存在于别处,错误信息会给出可直接复制的显式 selector。
+第三种写法会明确报错,指出实际尝试过的 `(mcpplibs, asio)` 身份;该短名若存在
+于别处,错误信息会给出一个可直接复制的显式选择器。
-#### 裸名过渡期(`2026.8.10.1` 起,`2026.9` 移除)
+#### 裸名过渡期(`2026.8.10.1` 起,`2026.9` 移除)
-索引里已发布的 `compat.*` 包与既有 manifest **全部**写成裸名(`gtest = "1.15.2"`)。
-升级后直接失败,等于让一次程序发布把**已经发布、且无法追溯修改**的数据作废,
-所以有一个版本的过渡期:裸名在 `mcpplibs` 未命中时仍可到达 `compat.`,
-不声明 namespace 的 descriptor 也仍可被裸名解析。
+索引里每一个已发布的 `compat.*` 包,以及精确身份出现之前写下的每一份
+manifest,都把它的依赖写成裸名 —— `gtest = "1.15.2"`。升级时让它们直接失败,
+等于让一次程序发布,把已经发布、且无法追溯修改的数据作废,所以有一个版本的
+过渡期:一个在 `mcpplibs` 未命中的裸名,仍可到达 `compat.`,一个完全不
+声明 namespace 的描述符,也仍可响应它的裸名。
-但它不再静默:
+但它不再是静默的:
```
warning: dependency 'gtest' resolved to 'compat.gtest' through the deprecated
@@ -244,70 +255,82 @@ 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 的是**规范身份**,因此歧义的拼法只存在
+于一个地方 —— manifest 本身 —— 直到它被改写为止。`mcpp add gtest@1.15.2`
+执行的正是这次改写。
-过渡期**不适用于**写明 namespace 的 selector(`mcpplibs.gtest` 未命中就是未命中),
-裸名也**仍然**到不了第三方 namespace。
+这个过渡期**不适用于**写明了 namespace 的选择器(`mcpplibs.gtest` 未命中就
+保持未命中),裸名也**仍然**永远到不了第三方 namespace。
-**为什么只允许一个身份?** 因为依赖解析必须可复现。候选搜索会让同短名包受索引状态影响,新增索引还可能悄悄重定向既有依赖。
+**为什么只允许一个身份?** 依赖解析必须可复现。候选搜索会让两个短名相同的
+namespace,由索引当时的状态来裁决,而新增一个索引就可能悄悄改变一个既有依赖
+指向的目标。
-**给 xpkg 作者:** 索引描述符里,身份是 `(package.namespace, package.name)` 二元组。命名空间是点分路径,**`name` 是单一原子段**:
+**给 xpkg 作者:** 在一个索引描述符里,身份是 `(package.namespace,
+package.name)` 这一对。namespace 是点分路径,**`name` 是单一的原子段**:
```lua
package = {
namespace = "chriskohlhoff",
- name = "asio", -- 单一段;不是 "chriskohlhoff.asio"
+ name = "asio", -- one segment; NOT "chriskohlhoff.asio"
}
package = {
- namespace = "mcpplibs.capi", -- 层级放这里
+ namespace = "mcpplibs.capi", -- depth belongs here
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` 且只有一个模板,
-该单模板自动成为默认。多个未标默认的模板会报错,绝不按目录顺序选择。规范表见
+文件名只是一个提示 —— 描述符按它声明的身份被发现,所以
+`pkgs/c/chriskohlhoff.asio.lua` 与 `pkgs/z/anything.lua` 解析结果完全相同。
+`.lua` 或 `..lua` 是推荐写法(它们命中 mcpp 的快
+路径),但不是强制要求。
+
+较旧的完全限定拼法(`name = "chriskohlhoff.asio"`)仍被接受,因此已发布的
+描述符不需要改动。`mcpp xpkg parse` 会校验这条规则,索引 CI 里应当运行它。
+描述符身份要求 mcpp >= 0.0.106,精确选择器要求 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 约束在本地已知版本里无解。只要所有依赖都能在本地解析出来,
-无论本地索引多旧,构建都不会发起任何网络请求。
+`mcpp build` / `run` / `test` **只在依赖无法用本地副本解析时**刷新包索引,
+绝不会仅仅因为时间流逝就刷新。具体地说:本地根本没有索引、依赖的描述符不在
+其中,或一条 SemVer 约束在本地已知版本里无解,这三种情况会触发刷新。只要
+所有依赖都能在本地解析出来,不论本地索引多旧,构建都不会发出任何网络请求。
-由此带来的一个需要知道的语义:`^1.2` 这类约束是对**本地索引已知的版本**求解的。
-若上游在上次刷新之后发布了 `1.3.0`,需要主动获取:
+由此带来一个值得知道的推论:`^1.2` 这类约束,是对**本地索引已知的那些版本**
+求解的。上游在上一次刷新之后发布的 `1.3.0`,在被主动取回之前不可见:
```bash
-mcpp index update # 同步索引
-mcpp update # 同步索引,并重新解析依赖
-mcpp index status # 看本地现状:状态、年龄、修订号
+mcpp index update # sync the index
+mcpp update # sync, then re-resolve dependencies
+mcpp index status # local state: state, age and revision
```
-三个开关,优先级从高到低:
+三个开关,按优先级从高到低:
-| 开关 | 作用 |
+| 开关 | 效果 |
|---|---|
-| `--offline`(任意命令) | 完全不碰网络——不刷索引、不下载、不自动装工具链,也不发 `git ls-remote`/`clone`。已安装的东西照常构建,包括 commit 已在 `mcpp.lock`、克隆已在缓存里的 git 依赖 |
-| `MCPP_OFFLINE=1` | 同上,作用于整个 shell 会话或 CI job |
-| `~/.mcpp/config.toml` 里 `[index] auto_refresh = false` | 永不隐式刷新索引:依赖未命中时不刷新,安装本地索引缺少的包之前不刷新,工程自定义索引的首次同步也不做(该次构建停止并指出 `mcpp index update`)。下载仍然可用 |
+| `--offline`(任意命令) | 完全不碰网络 —— 不刷索引、不下载、不自动安装工具链,也不发出 `git ls-remote`/`clone`。已安装的东西照常构建,包括 commit 已在 `mcpp.lock`、克隆已在缓存里的 git 依赖 |
+| `MCPP_OFFLINE=1` | 同上,作用于整个 shell 会话或一次 CI job |
+| `~/.mcpp/config.toml` 中的 `[index] auto_refresh = false` | 永不隐式刷新索引:依赖未命中时不刷新,安装本地索引没有的包之前不刷新,工程自定义索引的首次同步也不做(该次构建停止,并指出 `mcpp index update`)。下载仍然可用 |
-`MCPP_NO_AUTO_INSTALL=1` 作为 `--offline` 的旧式窄化拼写仍然有效(它只管工具链的
-自动安装)。
+`MCPP_NO_AUTO_INSTALL=1` 作为 `--offline` 更早、更窄的拼法,仍然被接受
+(它只约束工具链的自动安装)。
-刷新有期限。`[index] refresh_timeout`(秒,默认 120)是一次刷新最长可用的时间;超过
-即被终止,一条警告指出该设置,构建与任何一次刷新失败之后一样,继续使用本地索引。经由
-xlings 的安装在 xlings 连续 300 秒没有任何输出(包括心跳)时被终止。结束 mcpp 会一并
-结束它启动的 xlings 进程。
+一次刷新有时限。`[index] refresh_timeout`(单位秒,默认 120)是一次刷新
+可用的最长时间;超过它就被终止,一条警告指出这个设置,构建像在任何一次刷新
+失败之后一样,继续使用本地索引。经由 xlings 的安装,在 xlings 连续 300 秒
+没有任何输出(包括心跳)时被终止。终止 mcpp 会一并终止它启动的 xlings 进程。
-任意命令加 `-v` 可以看到每个依赖的判定结果与原因。
+对任意命令加 `-v`,可以看到每个依赖的判定结果与原因。
## `[dev-dependencies]` —— 测试依赖
@@ -316,61 +339,67 @@ xlings 的安装在 xlings 连续 300 秒没有任何输出(包括心跳)时被
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` 分别提供过滤与机器可读输出。
+`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+)
+## `[build-dependencies]` —— 构建期依赖(mcpp 2026.8.29.1+)
```toml
[build-dependencies]
protobuf = { version = "35.1", tools = ["protoc"] }
```
-段与边上的请求回答的是**两个不同的问题**,把它们混为一谈是建模上的错误,不是写法之争。
+段与边上的请求回答的是**两个不同的问题**,把它们混为一谈是建模上的错误,
+不是写法之争。
-- **段**回答:这个包本身进不进目标。`[dependencies]` 进;`[build-dependencies]` 永不进,
- 只能经由它到达的东西也一样。
-- **边上的请求**回答:要它的哪一种构建期产物。`tools = [...]` 要一个宿主可执行文件,
- `host-module = true` 要一个构建程序可以 import 的模块。
+- **段**回答:这个包本身进不进目标。`[dependencies]` 表示进;
+ `[build-dependencies]` 表示永不进,只能经由它到达的东西同样不进。
+- **边上的请求**回答:想要它的哪一种构建期产物。`tools = [...]` 要一个宿主
+ 可执行文件,`host-module = true` 要一个构建程序可以 import 的模块。
-一个包可以同时在两个轴上取值,而 protobuf 正是证明这两个轴必须分开的例子:工程既链接
-`libprotobuf`,构建期又需要 `protoc`。它只写一次,写在 `[dependencies]` 里:
+一个包可以同时在这两个轴上取值,而 protobuf 正是证明这两个轴必须分开的
+例子:一个工程既链接 `libprotobuf`,构建期又需要 `protoc`。它只写一次,
+写在 `[dependencies]` 里:
```toml
[dependencies]
protobuf = { version = "35.1", tools = ["protoc"] }
```
-`[build-dependencies]` 用于第一个轴无法表达的那种组合 —— 某个包的库不得进入目标,
-而它的工具或规则仍然需要。同一个包同时出现在两张表里不是错误:普通声明胜出,因为
-一行 `[build-dependencies]` 不应该悄悄拿掉目标真正需要的库;两条声明各自的请求
-(`tools`、`features`、`host-module`、`reexport`)都作用于这一条边(mcpp 2026.9.16.1+;
-此前第二条声明的请求被丢弃)。
+`[build-dependencies]` 用于第一个轴无法表达的那种组合 —— 某个包的库不得
+进入目标,而它的工具或规则仍然需要。同一个包同时出现在两张表里不是错误:
+普通声明胜出,因为一行 `[build-dependencies]` 不应该悄悄拿掉目标真正需要
+的库;两条声明各自请求的内容(`tools`、`features`、`host-module`、
+`reexport`)都作用在同一条边上(mcpp 2026.9.16.1+;该版本之前,第二条声明
+的请求会被丢弃)。
-**只含程序的包只贡献它的程序(mcpp 2026.9.16.1+)。** 声明的 `[targets]` 全部是程序
-(`bin`、`app`、`test`)的依赖没有可链接的东西。它的工具由工具子构建构建,子构建把这个包
-当作自己的根来解析;在消费方的图里它只提供工具与目录:它自己的依赖不在那里解析,它的源码
-不在那里编译,它的 `ldflags` 不进入消费方的链接。因此一个程序可以依赖请求它的那个包,
-SDK 正是这样提供一个针对自身构建的程序。没有 `[targets]` 表的包不受影响,即使
-`src/main.cpp` 为它推断出一个程序。
+**只含程序的包,只贡献它的程序(mcpp 2026.9.16.1+)。** 一个声明的
+`[targets]` 全部是程序(`bin`、`app`、`test`)的依赖,没有可链接的东西。
+它的工具由工具子构建构建出来,子构建把这个包当作自己的根来解析;在消费方的
+图里,它只提供它的工具与它的目录,别无其他:它自己的依赖不在那里解析,它的
+源码不在那里编译,它的 `ldflags` 不进入消费方的链接。因此,一个程序可以
+依赖请求它的那个包,SDK 正是这样提供一个针对自身构建出来的程序。没有
+`[targets]` 表的包不受影响,即使某个 `src/main.cpp` 为它推断出一个程序。
-包之间的环在解析依赖图的地方被拒绝,并列出环上的边,与缓存模式无关;工具子构建再次请求
-正在构建的同一个工具时,在第一次重复处被拒绝,并给出请求链。
+包之间的环,在解析依赖图的地方就被拒绝,并列出环上的边,与缓存模式无关;
+一个工具子构建再次请求正在构建的同一个工具时,在第一次重复处被拒绝,并给出
+请求链。
-与 `[dev-dependencies]` 不同,这些依赖**会**被传递遍历:一个构建期依赖自己的依赖正是
-让它能工作的东西,并且继承它「只服务构建」的性质。
+与 `[dev-dependencies]` 不同,这些依赖**会**被传递遍历:一个构建期依赖自己
+的依赖,正是让它能工作的东西,并且继承它"只服务构建"的性质。
-feature 可以为构建期请求划定范围而无需第二个声明处。`[feature-deps.]` 的条目可以
-以相同的源重述一条已经无条件声明的依赖,并为它追加 `tools`,所以「按需才要」不需要另开
-一张表:
+一个 feature 可以为构建期请求划定范围,而无需另开一个声明处。
+`[feature-deps.]` 的条目可以用相同的源,重述一条已经无条件声明的
+依赖,并为它追加 `tools`,所以"只在需要时才要"不需要另开一张表:
```toml
[dependencies]
@@ -380,20 +409,21 @@ spike.installer = { path = "../installer" }
spike.installer = { path = "../installer", tools = ["installer"] }
```
-重述要写出源,因为每张依赖表都如此:没有 `path`、`git`、`version` 或 `workspace` 的条目
-被当作命名空间表读取并被拒绝,拒绝消息会说明要重述源。重述中的 `tools`、`features`、
-`host-module` 与 `reexport` 加到该行生效的声明上。重述写了另一个源时被拒绝,并列出两个源
-(mcpp 2026.9.16.1+);此前它被忽略。
+这次重述要写出它的源,因为每一张依赖表都是如此:一个没有 `path`、`git`、
+`version` 或 `workspace` 的条目会被当作命名空间表读取并被拒绝,拒绝消息会
+说明要重述源。重述中的 `tools`、`features`、`host-module` 与 `reexport`
+会加到这一行当前生效的声明上。重述若写了另一个源,会被拒绝,并列出两个源
+(mcpp 2026.9.16.1+);该版本之前它会被忽略。
-> 这个段很早就能被解析,而直到 2026.8.29.1 之前没有任何做决定的代码读它:写下它得到的是
-> 一份能加载的清单、零诊断、零效果。
+> 这个段很早就能被解析,而直到 2026.8.29.1,没有任何做决定的代码读过它:
+> 写下它得到的是一份能加载的 manifest、零诊断、零效果。
## 当前边界
-- **只有两件事需要网络,也只有这两件:**解析一个在锁里没有 commit 的分支,以及克隆
- 一个尚未缓存的 commit。指向本地目录或 `file://` URL 的 `git =` 两者都不需要,因此
- 离线时从不会被拒绝。
-- 索引刷新窗口**不适用于写明了 namespace 的选择器**。`mcpplibs.gtest` 一旦未命中就
- 保持未命中,直到下一次刷新。
-- `mcpp.lock` 记录并核验一次解析,但不约束解析。见
+- **只有两件事需要网络,而且只有这两件:** 解析一个在 lock 里没有 commit 的
+ 分支,以及克隆一个尚未缓存的 commit。指向本地目录或 `file://` URL 的
+ `git =`,两者都不需要,因此离线时从不会被拒绝。
+- 索引刷新窗口**不适用于写明了 namespace 的选择器**。`mcpplibs.gtest` 一旦
+ 未命中,就保持未命中,直到下一次刷新。
+- `mcpp.lock` 记录并核验一次解析,但不约束一次解析。见
[51 —— 受支持的版本与兼容性](51-supported-versions.md)。
diff --git a/docs/zh/06-features-and-capabilities.md b/docs/zh/06-features-and-capabilities.md
index 7e50e4b94..53aca4014 100644
--- a/docs/zh/06-features-and-capabilities.md
+++ b/docs/zh/06-features-and-capabilities.md
@@ -1,110 +1,119 @@
# 06 —— Feature 与能力
-**读者:**手上有可选内容的作者 —— 一份额外的源、一个额外的依赖,或者在多个后端
+**读者:** 手上有可选内容的作者 —— 一份额外的源、一个额外的依赖,或者在多个后端
之间做选择。
-**本章回答的那一个问题:**一个包怎样提供可选内容,消费者又怎样要它。
+**本章回答的那一个问题:** 一个包怎样提供可选内容,消费方又怎样请求它。
-**不在这里:**一次构建面向哪些设备后端 —— 那看起来像 feature 而不是 feature,
-它是 [42 —— 异构硬件构建](42-heterogeneous-builds.md)。在此之前:
-[05 —— 依赖与解析](05-dependencies.md)。在此之后:[07 —— 工作空间](07-workspace.md)。
+**不在这里:** 一次构建面向哪些设备后端 —— 那看起来像 feature,却不是 —— 它是
+[42 —— 异构硬件构建](42-heterogeneous-builds.md)。在此之前:
+[05 —— 依赖与解析](05-dependencies.md)。在此之后:[07 —— 工作空间](07-workspace.md)。
-Feature 是一个包提供可选内容的方式:一个编译宏、一份额外的源文件、一个额外的
-依赖,或者在多个后端之间做选择。本章是声明与消费 feature 的参考。
+Feature 是一个包提供可选内容的方式:一个编译宏、一份额外的源文件、一个额外的
+依赖,或者在多个后端之间的一次选择。本章是声明与消费 feature 的参考。
-相关文档:[04 —— mcpp.toml](04-mcpp-toml.md) 是 manifest 其余部分的字段参考;
+相关文档:[04 —— mcpp.toml](04-mcpp-toml.md) 是 manifest 其余部分的字段参考;
[`examples/11-features`](../../examples/11-features/) 是一个把三种形态都声明了
-一遍、并且用 dev-dependency 写测试的包;[42 —— 异构硬件构建](42-heterogeneous-builds.md)
-是这套机制最大的消费者,因为每条加速器 lane 都是一个 feature。
+一遍、并且用 dev-dependency 写了测试的包;[42 —— 异构硬件构建](42-heterogeneous-builds.md)
+是这套机制最大的消费方,因为每一条加速器 lane 都是一个 feature。
-## `[features]` —— Feature(Cargo 风格,可加性)
+## `[features]` —— Feature(Cargo 风格,可加性)
```toml
[features]
-default = ["base"] # 默认激活集合
+default = ["base"] # Default activation set
base = []
-docking = ["extra"] # 激活 docking 即隐含激活 extra(传递闭包)
+docking = ["extra"] # Activating docking implies activating extra (transitive closure)
extra = []
```
-- 激活来源:包自己的 `default` 集合 ∪ 显式请求(根包经由
- `mcpp build --features a,b`;依赖经由长形式依赖 spec 的 `features = [...]`
- 与 `backend = "..."` 糖)。
-- 每个被激活的 feature 在该包编译时得到宏 `-DMCPP_FEATURE_`(名字大写,
- 非字母数字变 `_`,例如 `backend-a` → `MCPP_FEATURE_BACKEND_A`)。
-- **严格校验**:目标包声明了 `[features]` 表时,请求一个未声明的 feature 产生
- 警告,在 `--strict` 下是错误。不声明 `[features]` 的包接受任意请求(纯宏用法)。
+- 激活来源:包自己的 `default` 集合,并上显式请求(根包经由
+ `mcpp build --features a,b`;依赖经由长形式依赖 spec 的 `features = [...]`
+ 与 `backend = "..."` 糖衣)。
+- 每个被激活的 feature,在该包编译时得到宏 `-DMCPP_FEATURE_`(名字转大写,
+ 非字母数字字符变 `_`,例如 `backend-a` → `MCPP_FEATURE_BACKEND_A`)。
+- **严格校验**:目标包声明了 `[features]` 表时,请求一个未声明的 feature 会产生
+ warning,在 `--strict` 下是错误。不声明 `[features]` 的包接受任意请求(纯宏用法)。
### 依赖的 feature
-`<依赖>/` 形式的记号打开某个依赖的 feature。依赖以消费方清单所写的键命名
-(`spike.fw`、`compat.opencv`),记号是可加的:它只打开依赖更多的部分,从不把依赖拉进来。
+`<依赖>/` 形式的记号,打开某个依赖的一个 feature。依赖以消费方 manifest
+所写的键命名(`spike.fw`、`compat.opencv`),记号是可加的:它只打开依赖更多的
+部分,从不把依赖本身拉进来。
```toml
[dependencies]
spike.fw = { path = "../fw" }
[features]
-windows-installer = ["spike.fw/installer"] # 与表形式中的 `forward = [...]` 相同
+windows-installer = ["spike.fw/installer"] # the same as `forward = [...]` in the table form
```
-- **命令行上**(mcpp 2026.9.16.1+),`mcpp build --features spike.fw/installer` 为一条命令
- 打开同一个 feature,与根的转发相同;`run`、`test`、`pack`、`emit build-database` 与
- `why deps` 同样接受该记号。它从不是根的 feature,也从不变成宏。
-- **校验**读取写下该转发的清单的每一张依赖表:`[dependencies]`、`[build-dependencies]`、
- `[dev-dependencies]` 与 `[feature-deps.]`,覆盖所有行。只在别的行或未激活的 feature
- 下声明的键同样算已声明;在当前行上该转发不到达任何边,不产生效果。没有任何表声明的键会被
- 报告,在 `--strict` 下报告为错误。命令行上无论根是否声明 `[features]`,都做同样的检查。
+- **命令行上**(mcpp 2026.9.16.1+),`mcpp build --features spike.fw/installer` 为
+ 一条命令打开同一个 feature,效果与根的转发相同;`run`、`test`、`pack`、
+ `emit build-database` 与 `why deps` 同样接受这个记号。它从不是根自己的 feature,
+ 也从不变成宏。
+- **校验**会读遍写下该转发的那份 manifest 里的每一张依赖表:`[dependencies]`、
+ `[build-dependencies]`、`[dev-dependencies]` 与 `[feature-deps.]`,覆盖
+ 所有行。一个键只在别的行、或未激活的 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`*):
+`[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。
+# Array shorthand: just implied features.
docking = ["extra"]
extra = []
-# 表形式:激活时贡献一个包自有的宏。
+# Table form: contribute a package-owned define when active.
mpl2only = { defines = ["EIGEN_MPL2_ONLY"] }
-# 表形式:宏 + 一个隐含 feature。
+# Table form: a define + an implied feature.
fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] }
-# 表形式:feature 门控源 + 与其同居的 per-glob 旗标。
+# 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"] } ] }
```
- **表形式恰好接受** `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 限定作用面、顺序确定、无跨包效应。
+ `requires`、`provides`。其余键会被报为一条 schema warning 并忽略
+ (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]]` 这种 array-of-tables 拼法)。feature 激活时,
+ 条目追加在 base `[build].flags` **之后**(各 feature 按名字排序),"最后一条
+ 旗标胜出"使 feature 规则能覆盖更宽的 base 规则;未激活时,这些条目根本不存在
+ (不会产生死 glob 告警)。这让 feature 的组内专属旗标与它的 `sources` 同居,
+ 而不必写成 base 规则、在 feature 关闭的构建里留下一条注定命中不到的 glob。
+ 与 `defines` 不同,feature 的 `flags` 是**私有的、per-TU 的构建旗标**——它们
+ 从不传播给消费方(与 `[build].flags` 同一契约),因此不破坏可加模型:由 glob
+ 限定作用面,顺序确定,没有跨包效应。
### 作为构建规则的 feature(mcpp 2026.9.7.1+)
-两个键把一个 feature 变成其它包可以使用的构建规则。它们是消费者只写一条依赖边、
-不写构建程序的原因。
+两个键能把一个 feature 变成其他包可以使用的构建规则。它们是消费方只写一条
+依赖边、不必写构建程序的原因。
```toml
[features.rules-spirv]
@@ -113,49 +122,52 @@ 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 的内置表中移除,现在正是经由这条路到达的。
+`device_extensions` 陈述这条规则编译哪些**设备源**扩展名。激活了该 feature 的
+消费方会把它们分类为设备源 —— 不做 import 扫描、不产出 BMI、由 mcpp 不驱动的
+编译器编译。这与 `[build] module_extensions` 是同一个形状:mcpp 知道设备源
+*是什么*,不知道 `.cu` 是 CUDA,所以**一门新的设备语言不需要引擎改动**。
+[42 —— 异构硬件构建](42-heterogeneous-builds.md) 里"第六个后端是一个包而不是
+一次引擎改动"这句话由此才成立,而不只是愿望;`.slang` 已经从 mcpp 的内置表中
+移除,现在正是经由这条路到达的。
-`rule_module` 给出消费者的构建程序为够到这条规则而 import 的模块,以及它调用的
-`compile()` 所在。它是**声明**的而不是从源码扫描出来的,因为那个程序必须在任何东西
-被编译**之前**写出来,而一次为了决定写什么而去扫描依赖源码的构建会把两者的顺序颠倒。
+`rule_module` 给出消费方的构建程序为够到这条规则而 import 的模块,以及它要调用
+的 `compile()` 所在。它是**声明**出来的,而不是从源码扫描出来的,因为那个程序
+必须在任何东西被编译**之前**写出来,而一次为了决定写什么、去扫描依赖源码的构建,
+会把两者的先后顺序颠倒。
-由此得出两件事,而且都不把任何包名放进 mcpp:
+由此得出两件事,而且都没有把任何包名放进 mcpp:
-- **`host-module = true` 被推出来。** 一个点名了规则模块的 feature 已经说过那是使用
- 它的唯一方式,所以依赖边不必再说一遍。
-- **没有 `build.mcpp` 的包会得到一个。** mcpp 把这些规则描述的程序写进构建目录并编译
- 它。自带程序的包保留自己的:合成只填补缺席、绝不覆盖;而生成出来的那份就是这个工程
- 本来要手写的那份,所以接管它是一次复制加一次编辑。
+- **`host-module = true` 被隐含。** 一个点名了规则模块的 feature,已经说明那是
+ 使用它的唯一方式,所以依赖边不必再说一遍。
+- **没有 `build.mcpp` 的包会得到一个。** mcpp 把这些规则描述的程序写进构建目录
+ 并编译它。自带程序的包保留自己的那份:合成只填补缺席,绝不覆盖已有的;而
+ 生成出来的那份,正是这个工程本来要手写的那份,所以接管它是一次复制加一次
+ 编辑。
-feature 仍然**按名字**请求:
+feature 仍然**按名字**请求:
```toml
[build-dependencies.mcpp]
plugins = { version = "0.3.0", features = ["rules-spirv"] }
```
-早先的一版设计从工程源码里出现的扩展名推导这个集合。它被撤销了,因为两个包可能认领
-同一个扩展名 —— 第三方写一条处理 `.cu` 的规则是会发生的事 —— 也因为 manifest 的职责
-是描述这次构建,而派生出来的 feature 集合让文件不再陈述它。
+早先的一版设计,从工程源码里出现的扩展名推导这个集合。它被撤销了,原因有二:
+两个包可能认领同一个扩展名 —— 第三方写一条处理 CUDA 的规则是会发生的事 ——
+而且 manifest 的职责是描述这次构建,派生出来的 feature 集合不再做到这一点。
-两个键必须成对出现。只写其一是一条没有任何东西能据以行动的声明,会在解析期被拒绝,
-而不是留到消费者的构建里。
+两个键必须成对出现。只写其一是一条没有任何东西能据以行动的声明,会在解析期
+被拒绝,而不是留到消费方的构建里才发现。
-## `provides` / `requires` —— 能力(后端选择)
+## `provides` / `requires` —— 能力(后端选择)
-**capability(能力)** 是一个共享的抽象名字(如 `blas`)。包可以 *provide*(提供)
-一种能力;feature 可以 *require*(需要)一种能力而非点名某个具体包,解析器会从依赖
-图中绑定**恰好一个** provider。这样就能在多个可互换后端(OpenBLAS / MKL / …)中选其
-一,而不必把选择写死进库里。
+**能力**(capability)是一个共享的抽象名字(例如 `blas`)。包可以 *provide*
+(提供)一种能力;feature 可以 *require*(需要)一种能力而不点名某个具体包,
+解析器会从依赖图中绑定**恰好一个** provider。这样就能在多个可互换的后端
+(OpenBLAS / MKL / …)之间选出一个,而不必把选择写死进库里。
```toml
-# provider 包为任何 require 它的依赖方满足某能力。
+# A provider package satisfies a capability for any dependent that requires it.
[package]
name = "compat.openblas"
version = "0.3.0"
@@ -163,21 +175,20 @@ provides = ["blas", "lapack"]
```
```toml
-# 消费方经由自己的某个 feature 来 require 这个抽象能力。
+# A consumer requires the abstract capability via one of its features.
[features]
use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] }
-# 图中有 >1 个 provider 时,选其一(否则构建报错并列出候选)。
+# When >1 provider is in the graph, pick one (else the build errors and lists them).
[capabilities]
-blas = "compat.openblas" # 等价于:mcpp build --cap blas=compat.openblas
+blas = "compat.openblas" # equivalently: mcpp build --cap blas=compat.openblas
[dependencies]
-compat.openblas = "0.3.0" # provider 必须是图中真实存在的依赖
+compat.openblas = "0.3.0" # the provider must be a real dependency in the graph
```
-保留前缀 `mcpp:` 命名本引擎解析的目标侧层,这些名字对照一个闭集校验。
-包级 `requires` 数组承载对称的陈述 —— 某个目标侧层必须解析为什么,
-本包才可用。
+保留前缀 `mcpp:` 命名本引擎解析的目标侧层,这些名字对照一个闭集做校验。包级的
+`requires` 数组承载对称的陈述 —— 某个目标侧层必须解析出什么,本包才可用。
```toml
[package]
@@ -187,7 +198,8 @@ provides = ["mcpp:compiler-runtime=compiler-rt", "mcpp:c++-abi=libc++"]
requires = ["mcpp:compiler=llvm"]
```
-对产物 ABI 开关的需求用 `requires_abi` 陈述,写在包上或某个 feature 上,而不是写成层:
+对产物 ABI 开关的需求,用 `requires_abi` 陈述,写在包上或某个 feature 上,而不是
+写成一层:
```toml
[package]
@@ -197,12 +209,13 @@ requires_abi = { threads = true }
mt = { requires_abi = { threads = true } }
```
-只有根 manifest 设置该开关(`[target..abi]`,见 [22 —— 目标侧](22-target-side.md));
-根包未满足的需求在编译之前被拒绝,拒绝信息指出包与 feature。安装钩子针对某一个 C++ 标准库编译静态库的
-包,则把该实现陈述为层需求,即 `requires = ["mcpp:c++-abi=libstdc++"]`,原因见同一章。
+只有根 manifest 能设置这个开关(`[target..abi]`,见
+[22 —— 目标侧](22-target-side.md));根未满足的需求会在编译之前被拒绝,拒绝信息
+点名包与 feature。安装钩子针对某一个 C++ 标准库编译静态库的包,把该实现陈述为
+层需求,即 `requires = ["mcpp:c++-abi=libstdc++"]`,原因见同一章。
-作为标准库的包在 `[build]` 下陈述它的 `std` 模块源,
-其所需的 flag 在那里与任何其它构建输入一样可条件化。
+作为标准库的包,在 `[build]` 下陈述它的 `std` 模块源,它所需的 flag 在那里与
+任何其他构建输入一样,可以条件化。
```toml
[build]
@@ -214,41 +227,43 @@ std-module-flags = ["--no-default-config", "-nostdinc++"]
std-module-flags = ["-D_GNU_SOURCE"]
```
-五个层、约束它们的规则与相应诊断,见 [22 - 目标侧](22-target-side.md)。
+五个层、约束它们的规则,以及相应的诊断,见 [22 —— 目标侧](22-target-side.md)。
-绑定是**确定性**的:
+绑定是**确定性**的:
| 图中某被需要能力的 provider 数量 | 结果 |
|---|---|
-| 恰好一个 | 自动绑定(无需配置) |
+| 恰好一个 | 自动绑定(无需配置) |
| `[capabilities]` pin / `--cap` 指定了一个 | 以 pin 为准 |
-| 零个 | **报错**:没有包提供 `` |
-| 两个及以上且未 pin | **报错**并列出候选——绝不静默猜测 |
+| 零个 | **报错**:没有包提供 `` |
+| 两个及以上,且未 pin | **报错**并列出候选 —— 绝不静默猜测 |
-被绑定 provider 的链接/头文件旗标经由常规依赖机制流到消费方;capability 层是那道
-*选择与校验* 步骤,把"静默选错后端 / 缺后端"变成构建期的显式报错。
+被绑定 provider 的链接/头文件旗标,经由普通的依赖机制流向消费方;capability
+层是那道*选择与校验*步骤,把"静默选错后端"或"缺后端"变成构建期的显式报错。
-**绑定选中的是 provider,它不裁剪链接行。** 依赖包的目标文件一律进入消费方的链接,
-与它的能力是否被绑定无关。实测:两个包都提供同一能力且都定义 `cap_probe`,未 pin 时
-解析按上表报错;按提示用 `[capabilities]` pin 其中一个之后,构建走到链接器才失败——
+**绑定选中的是 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 实现导出不同的符号集合,按链接选其一)不受此影响。
+这一点对**多个 provider 定义同一批符号**的能力有影响 —— 一个全程序单例(例如
+`operator new`),或一个名字集合固定的 C 接口。对这类能力而言,图中出现两个
+provider 是一个**待修的缺陷**,而不是一个可以 pin 的歧义:pin 会把一个点名两个
+候选的报错,换成一个点名 mangled 符号的报错。可互换的**库**(各 BLAS 实现导出
+不同的符号集合,按链接各自选一个)不受此影响。
### `exclusive` —— 包声明自己是唯一提供者
-上一段描述的是一个引擎**看不见**的缺陷:要看出两个 provider 定义了同一批符号,
-需要它们的目标文件,而绑定 capability 时那些还不存在;而「一律拒绝重复 provider」
-又会打断同一段里那个合法的 BLAS 用例。
+上一段描述的是一个引擎**看不见**的缺陷:要看出两个 provider 定义了同一批符号,
+需要它们的目标文件,而绑定 capability 时那些文件还不存在;而"一律拒绝重复
+provider"这条规则,又会打断同一段里那个合法的 BLAS 用例。
-所以由包自己声明:
+所以由包自己声明:
```toml
[package]
@@ -257,25 +272,29 @@ provides = ["gpu-blas"]
exclusive = ["gpu-blas"]
```
-两个都提供 `gpu-blas` 的包,只要其中至少一个声明了独占,就在绑定 capability 时
-被拒绝 —— 在任何东西被编译之前,并点名该能力与双方:
+两个都提供 `gpu-blas` 的包,只要其中至少一个声明了独占,就在绑定 capability
+时被拒绝 —— 在任何东西被编译之前,并点名该能力与双方 provider:
```
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.
```
-该拒绝在 `--format json` 里报 `exclusive-capability`(见第 11 章)。
+这个拒绝在 `--format json` 里报 `exclusive-capability`(见第 11 章)。
-### `version-floor` —— 对机器的要求高于它所有
+### `version-floor` —— 对机器的要求超出它所有
-有些关于机器的事实约束着能为它构建什么,而忽略它们时的失败来得很晚:
-一个针对比它将遇到的驱动更新的运行时构建出来的程序,**干净地链接**,
-在第一次使用时失败,而消息里两侧都没有。
+关于一台机器的某些事实约束着能为它构建什么,而忽略它们时的失败来得很晚:一个
+针对比它将遇到的驱动更新的运行时构建出来的程序,链接得干干净净,却在第一次
+使用时失败,而且错误消息不点名任何一侧。
-包声明它需要什么:
+包声明它需要什么:
```toml
[[runtime.requirements]]
@@ -283,36 +302,38 @@ kind = "version-floor"
value = "cuda.driver >= 12.0"
```
-而某个在**安装期**(探测该发生的地方)确立了机器某项事实的包,声明它:
+而某个在**安装期**(探测本该发生的地方)确立了机器上某项事实的包,声明它:
```toml
[runtime]
provides = ["cuda.driver=12.4"]
```
-mcpp 在绑定 capability 时比较二者,并在任何东西被编译之前拒绝,
-报 `version-floor-unmet`:
+mcpp 在绑定 capability 时比较二者,并在任何东西被编译之前拒绝,报
+`version-floor-unmet`:
```
error: `toolkitnew` requires cuda.driver >= 13.0, and cuda.driver is stated as 12.4.
stated by: driverfact
```
-**没有任何厂商词汇抵达引擎。** 它读到的是一个名字、一个关系和一个版本;
-`cuda.driver` 是流过的数据,一个 mcpp 从未听说过的后端比较方式完全相同。
+**没有任何厂商词汇抵达引擎。** 它读到的是一个名字、一个关系和一个版本;
+`cuda.driver` 只是流过它的数据,一个 mcpp 从未听说过的后端,比较方式完全相同。
-**没人回答的下界是沉默的。** 一台从未声明自己有什么的机器,不是「未满足下界」的机器,
-而是「没人问过」的机器。把「我们不知道」变成「不行」正是这个机制要避免的失败,
-并且有直接判据:`tests/e2e/603_version_floor.sh` 会构建一个下界指向无人提供之物的工程。
+**没人回答过的下界是沉默的。** 一台从未声明过自己有什么的机器,不是"未满足
+下界"的机器,而是"没人问过"的机器。把"我们不知道"变成"不行",正是这个机制
+要避免的那种失败,而且有一条直接判据:`tests/e2e/603_version_floor.sh` 构建了
+一个下界指向无人提供之物的工程。
-**引擎陈述目标的平台下限**(mcpp 2026.9.14.2+)。在编译器接受最低平台版本的行上,
-引擎以平台自己的术语把该版本陈述为一项事实,包像对待其他事实一样对它写下界:
+**引擎陈述目标的平台下限**(mcpp 2026.9.14.2+)。在编译器接受一个最低平台版本
+的那一行上,引擎以该平台自己的说法,把这个版本陈述为一项事实,包像对待其他
+事实一样,对它写下界:
-| 事实 | 行 | 设定来源 |
+| 事实 | 适用行 | 设定来源 |
|---|---|---|
-| `android.api-level` | `*-linux-android` | `[target.] min_api_level`,否则为工具链支持的最低级别 |
-| `ios.deployment-target` | iOS 真机与模拟器各行 | `[build] ios_deployment_target`,否则为定位到的 SDK 版本 |
-| `macos.deployment-target` | macOS 各行 | `[build] macos_deployment_target`,否则为 mcpp 的 macOS 默认值 |
+| `android.api-level` | `*-linux-android` | `[target.] min_api_level`,否则取工具链支持的最低级别 |
+| `ios.deployment-target` | iOS 真机与模拟器各行 | `[build] ios_deployment_target`,否则取定位到的 SDK 版本 |
+| `macos.deployment-target` | macOS 各行 | `[build] macos_deployment_target`,否则取 mcpp 在 macOS 上的默认值 |
```toml
[[runtime.requirements]]
@@ -325,55 +346,58 @@ error: `fw` requires android.api-level >= 23, and this build targets 21.
set by: [target.x86_64-linux-android] min_api_level
```
-不陈述这类事实的行上,该要求保持沉默,因此要求本身不需要选择器。下限不会替依赖
-抬高:键设定的值就是编译器面向的值,应用安装到哪些设备上由应用决定。包陈述同名
-事实不会替换引擎陈述的那一项。
+不陈述这类事实的行,让这条要求保持沉默,因此要求本身不需要选择器。下限不会替
+依赖抬高:这个键设定的值,就是编译器实际面向的值,而应用要安装到哪些设备上,
+是应用自己的决定。一个包陈述同名事实,不会替换引擎陈述的那一项。
-**它是关于这个包自己的符号的声明**,所以一条指向本包并不提供的能力的条目会被报为
-schema 警告:那里没有可独占的东西。而无人声明独占的能力行为完全不变 —— 两个 BLAS
-实现照常共存,既有的「两个或更多、未 pin」报错也仍然只在**有人 require** 该能力时出现。
+**这是一条关于本包自己的符号的声明**,所以一条指向本包并不提供的能力的条目,
+会被报为一条 schema warning:那里没有可独占的东西。而无人声明独占的能力,
+行为完全不变 —— 两个 BLAS 实现照常共存,既有的"两个或更多、未 pin"报错也仍然
+只在**有人 require** 该能力时才出现。
-## `[feature-deps.]` —— 由 feature 拉取的依赖
+## `[feature-deps.]` —— feature 拉取的依赖
-在 `[feature-deps.]` 下声明的依赖是**可选的**:仅当该 feature 激活时(根 `--features`,
-或某依赖 spec 的 `features = [...]`)才会被解析。`[dependencies]` 中的依赖始终被解析;
-可选性由声明的*位置*表达,而非某个标志位。
+在 `[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)。
+# 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"`,而不是 `"0.3.x"` 或 `"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`,点名的是包 —— 而该包存在 |
+| `cmdline = "0.0"` | 解析通过,随后 `install path missing after fetch` |
+| `cmdline = "0.0.x"` | `E_NOT_FOUND`,点名的是那个存在的包 |
-这三种结果值得分开,因为两个更弱的判据各自会放行一种不可用的写法:
-「没有 `E_NOT_FOUND`」放行两段前缀,「解析通过」同样放行它。**只有对着真实索引构建
-一次**才能定论。
+这三种结果值得分开看,因为有两个更弱的判据,各自会放行一种不可用的写法:
+"没有 `E_NOT_FOUND`"放行两段式前缀,"解析通过"同样放行它。**只有对着真实索引
+构建一次**才能定论。
-这一点在此处比在 `[dependencies]` 中更要紧:**实现取不回来的 feature 等于不存在的
-feature**,而开发期使用 **path** 依赖的工程根本不查索引 —— 该失败只在发布之后才出现,
-而且是出现在别人身上。
+这一点在此处比在 `[dependencies]` 里更要紧。一个取不回实现的 feature,等于
+一个不存在的 feature;而开发期使用**path** 依赖的工程根本不查索引 —— 这个
+失败只在发布之后才出现,而且出现在别人身上。
-该机制与能力(上文*`provides` / `requires`*)组合:单个 `backend-openblas` feature 既**拉取** provider
-(`compat.openblas`,其 `provides = ["blas"]`),又**开启**消费方开关
-(`implies = ["use_blas"]`,其 `requires = ["blas"]`)。当图中只有一个 provider 时,
-能力自动绑定——消费方只需写 `features = ["backend-openblas"]`。
+这一机制与能力(见上文 *`provides` / `requires`*)组合使用:单个
+`backend-openblas` feature 既**拉取** provider(`compat.openblas`,其
+`provides = ["blas"]`),又**开启**消费方的开关(`implies = ["use_blas"]`,
+其 `requires = ["blas"]`)。图中只有一个 provider 时,能力自动绑定 ——
+消费方只需写 `features = ["backend-openblas"]`。
-在索引包的 Lua 描述符中,等价写法为内联形式:
+在索引包的 Lua 描述符中,同样的内容写成内联形式:
```lua
features = {
@@ -387,71 +411,76 @@ features = {
### 保持可替换的默认实现
-同样这三件东西,也覆盖"库希望**提供**一份实现但不**强加**一份"的情形 —— 全程序单例,
-例如 `operator new`、日志 sink、panic handler:
+同样这三件东西,也覆盖了"库希望**提供**一份实现,但不**强加**一份"的情形 ——
+一个全程序单例,例如 `operator new`、一个日志 sink、一个 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"] }
-# 仅在 `alloc-kal` 激活时解析,因此库本体不携带对该实现的依赖。
+# 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"
```
-三种用法各一行:
+三种用法各一行:
-| 消费方需要 | 清单中的写法 |
+| 消费方需要 | manifest 中的写法 |
|---|---|
| 不用会分配的那部分 | `std-freestanding = "0.2.0"` —— 分配器不进图 |
-| 默认实现 | `features = ["alloc-kal"]` —— 实现随之进图,**无需知道其包名** |
-| 自己的或第三方的 | `features = ["alloc"]` 加一个 `provides = ["freestanding-allocator"]` 的包 |
+| 内置的默认实现 | `features = ["alloc-kal"]` —— 实现随之进图,不必知道它的包名 |
+| 自己的或第三方的实现 | `features = ["alloc"]` 加一个 `provides = ["freestanding-allocator"]` 的包 |
-有两条性质使该形状优于无条件随包提供实现。随包提供实现的库替程序做了本属程序的决定,
-而且**撤销不掉**:feature 是**加性**的,消费方没有把某个默认**关掉**的手段。以及,由于
-依赖包的目标文件无条件参与链接(上文*`provides` / `requires`*),随包的默认加上程序自备的那份是**重复定义**
-而非替换 —— 让 C++ 标准库能提供可替换 `operator new` 的那套归档语义,对包依赖并不适用。
-把实现放在开关之后,意味着两者**从不共存**。
+有两条性质,使这个形状优于随包无条件提供实现。随包提供实现的库,替程序做了
+一个本该属于程序的决定,而且**撤销不掉**:feature 是**可加的**,消费方没有
+把某个默认实现**关掉**的手段。此外,由于依赖包的目标文件无条件参与链接
+(见上文 *`provides` / `requires`*),随包的默认实现加上程序自备的一份,
+是**重复定义**而不是替换 —— 让 C++ 标准库能提供一个可替换的 `operator new`
+的那套归档语义,并不适用于普通的包依赖。把实现放在开关之后,意味着两者
+**从不共存**。
### 平台 SDK 依赖保持私有
-一个绑定到某个平台的包 —— 它需要那个平台的头文件才能实现某个功能,而不是
-为了陈述自己的接口 —— 在 `[feature-deps.]` 下用 `visibility = "private"`
-依赖该 SDK:
+一个绑定到某个平台的包 —— 它需要那个平台的头文件才能实现某项功能,而不是为了
+陈述自己的接口 —— 在 `[feature-deps.]` 下用 `visibility = "private"`
+来依赖那个 SDK:
```toml
[features]
windows-crt = {}
-# 仅在激活该行时解析,其头文件只到达本包自己的翻译单元。
+# Resolved only on the row that activates it, and its headers reach ONLY
+# this package's own translation units.
[feature-deps.windows-crt]
some.windows-headers = { version = "1.0", visibility = "private" }
```
-`visibility` 是任意依赖项 spec 的一个字段(默认 `public`,还可以是 `private`
-或 `interface` —— 见[05 — 依赖](05-dependencies.md))。让 SDK 不跨越包边界的正是
-`private`:该依赖的头文件目录、宏定义与 flag 只并入本包自己的构建,到此为止,
-正如 `privateIncludeDirs` 让一个包**自己的**内部头文件不到达它的消费方。
-一个依赖该平台绑定包并激活 `windows-crt` 的消费方,会得到这个功能;它不会在
-自己的 `-I` 列表里得到 `some.windows-headers` 的目录,甚至无法按名字
+`visibility` 是任意依赖 spec 的一个字段(默认 `public`,还可以是 `private`
+或 `interface` —— 见 [05 —— 依赖](05-dependencies.md))。让 SDK 不跨越包边界的
+正是 `private`:该依赖的头文件目录、宏定义与 flag 只并入本包自己的构建,到此
+为止,正如 `privateIncludeDirs` 让一个包**自己的**内部头文件不到达它的消费方。
+一个依赖该平台绑定包、并激活 `windows-crt` 的消费方,会得到这项功能;它不会在
+自己的 `-I` 列表里得到 `some.windows-headers` 的目录,甚至无法按名字
`#include` 它的头文件。
-这正是[24 —— openkal 与由依赖图供给的目标](24-openkal-cross.md)所指向的模式:
-一个包若需要它声明的层(`kernel-abi`、`c-abi`、`c++-abi`)之外的平台头文件,
-这个依赖是合法的,但不能变成每一个消费方的问题。把它写成 `public`
-(或者不写 `visibility`,二者等价)正是本节要指出的错误 —— 对声明它的包而言这样
-可以工作,却会把 SDK 的头文件不由分说地交给消费方,而消费方构建的目标上很可能
-根本不该出现这个 SDK。
+这正是 [24 —— openkal 与由依赖图供给的目标](24-openkal-cross.md) 为一个需要
+超出其已声明层(`kernel-abi`、`c-abi`、`c++-abi`)之外平台头文件的包所指向
+的模式:这条依赖是合法的,但它不能变成每一个消费方的问题。把它写成 `public`
+(或者不写 `visibility`,二者等价)正是本节要指出的错误 —— 对声明它的包而言,
+这样能工作;但这会把 SDK 的头文件不由分说地交给消费方,而消费方构建的目标上
+很可能根本不该出现这个 SDK。
-#### 让闭包也能看见它,而不只是私有(mcpp 2026.9.18+)
+#### 让闭包也能看见它,而不只是私有(mcpp 2026.9.18+)
-`visibility = "private"` 回答的是「这个依赖会不会泄漏到消费方的 `-I` 列表」,不回答
-「这个依赖到底在不在图里」——后者是[22 —— 目标侧](22-target-side.md#闭包可见性)
-要为**整个构建**回答的问题。由 SDK 包自己陈述报告或拒绝开关需要的事实:
+`visibility = "private"` 回答的是"这个依赖会不会泄漏到消费方的 `-I` 列表",
+不回答"这个依赖到底在不在图里" —— 后者是
+[22 —— 目标侧](22-target-side.md#闭包可见性) 要为**整个构建**回答的问题。
+SDK 包自己陈述报告或拒绝所需要的那项事实:
```toml
[package]
@@ -460,22 +489,22 @@ version = "1.0.0"
provides = ["platform-sdk"]
```
-`platform-sdk` 是一个普通的、不带命名空间前缀的能力——像上面的 `blas`,不像
-`mcpp:c-abi=`——因为它不指代引擎解析的任何一层,只是包对自己陈述的一个事实。
-构建的 `Target` 报告会列出图中每一个声明了它的包(没有则显示为空);
-`[build] platform-dependencies = "refuse"` 则在它出现时直接让构建失败——这是
-「本次构建完全是基于其 kernel-abi 实现的闭包,不多不少」这句话的机器可核验形式。
-同时声明 `provides = ["platform-sdk"]` 与 `visibility = "private"` 才是完整的陈述:
-private 让头文件不出现在消费方的搜索路径上,`platform-sdk` 让这个事实不从任何人的
-报告里消失。
+`platform-sdk` 是一个普通的、不带命名空间前缀的能力 —— 像上文的 `blas`,不像
+`mcpp:c-abi=` —— 因为它不指代引擎解析的任何一层,只是包对自己陈述的一项
+事实。一次构建的 `Target` 报告会列出图中每一个声明了它的包(没有则为空);
+`[build] platform-dependencies = "refuse"` 会在它出现时直接让构建失败 —— 这是
+"本次构建完全是基于其 kernel-abi 实现的闭包,不多不少"这句话的机器可核验形式。
+同时声明 `provides = ["platform-sdk"]` 与 `visibility = "private"`,才是完整
+的陈述:private 让头文件不出现在消费方的搜索路径上,`platform-sdk` 让这项事实
+不从任何人的报告里消失。
## 当前边界
-**默认 feature 在 manifest 里关掉,不在命令行上关掉。** 没有 `--no-default-features`。
-`mcpp build --features metrics` 激活的是 `default ∪ {metrics}`;要在不带 `default`
-某个成员的情况下构建,得改 `[features] default`。实测于 2026.9.8.1。
-
-**依赖不能以加速器为条件。** `accelerator` 是从依赖图解析出来的,因此由它选择的
-依赖会决定它自己在问的那个答案。mcpp 会报告该谓词并忽略它。包要么无条件,要么以
-平台为条件;由加速器选择的是 `[build] sources`。
+**默认 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/07-workspace.md b/docs/zh/07-workspace.md
index a19ec8d15..128c4c436 100644
--- a/docs/zh/07-workspace.md
+++ b/docs/zh/07-workspace.md
@@ -1,24 +1,26 @@
# 07 —— 工作空间
-**读者:**仓库里不止一个包的作者。
+**读者:** 仓库里不止一个包的作者。
-**本章回答的那一个问题:**多个包怎样成为一次构建,以及一个成员与其余成员共享什么。
+**本章回答的那一个问题:** 多个包怎样成为一次构建,以及一个成员与其余成员共享什么。
-**不在这里:**把这些包发布出去,那是 [11 —— 发布一个库](11-publishing-a-library.md)。
-在此之前:[06 —— Feature 与能力](06-features-and-capabilities.md)。在此之后:
+**不在这里:** 把这些包发布出去,那是 [11 —— 发布一个库](11-publishing-a-library.md)。
+在此之前:[06 —— Feature 与能力](06-features-and-capabilities.md)。在此之后:
[08 —— 测试](08-testing.md)。
-工作空间允许在同一个仓库中组织和管理多个相关的 mcpp 包(库或应用程序)。各成员包共享统一的依赖版本配置和工具链设置,同时保持独立的 `mcpp.toml` 工程文件。
+工作空间在同一个仓库中组织多个相关的 mcpp 包(库或应用程序)。各成员包共享统一
+的依赖版本与工具链配置,同时各自保留独立的 `mcpp.toml` 工程文件。
## 1. 概述
工作空间解决以下问题:
-- **依赖版本统一管理** — 多个子包使用相同版本的第三方依赖,避免重复声明和版本不一致
-- **工具链配置共享** — 在工作空间根目录统一声明工具链,各成员继承或覆盖
-- **多包协同开发** — 库与应用在同一仓库中开发,通过 `path` 依赖相互引用
+- **依赖版本统一管理**——多个子包使用相同版本的第三方依赖,避免重复声明与版本漂移。
+- **工具链配置共享**——在工作空间根声明一次工具链,成员继承或按需覆盖。
+- **多包协同开发**——库与应用在同一仓库中开发,通过 `path` 依赖相互引用。
-工作空间不改变依赖声明方式。成员之间通过已有的 `path = "..."` 机制声明依赖关系,与非工作空间项目的用法完全一致。
+工作空间不改变依赖的声明方式。成员之间通过既有的 `path = "..."` 机制相互引用,
+与非工作空间工程的用法完全一致。
## 2. 工程文件结构
@@ -35,9 +37,9 @@ members = [
]
```
-`members` 列出各成员包的相对路径,每个路径下须包含独立的 `mcpp.toml`。
+`members` 列出各成员包的相对路径,每个路径下必须包含各自的 `mcpp.toml`。
-可选 `exclude` 字段排除特定路径:
+可选字段 `exclude` 排除特定路径:
```toml
[workspace]
@@ -45,17 +47,19 @@ members = ["libs/*"]
exclude = ["libs/experimental"]
```
-### 2.2 虚拟工作空间与根包工作空间
+### 2.2 虚拟工作空间与带根包的工作空间
-**虚拟工作空间**:根 `mcpp.toml` 仅包含 `[workspace]`,不包含 `[package]`。根目录不产出构建产物,仅作为管理节点。
+**虚拟工作空间**:根 `mcpp.toml` 只含 `[workspace]`,不含 `[package]`。根目录不
+产出构建产物,只作为管理节点。
```toml
-# 虚拟工作空间 — 只有 [workspace]
+# 虚拟工作空间 —— 只有 [workspace]
[workspace]
members = ["libs/core", "apps/server"]
```
-**根包工作空间**:根 `mcpp.toml` 同时包含 `[package]` 和 `[workspace]`。根目录本身也是一个可构建的包。
+**带根包的工作空间**:根 `mcpp.toml` 同时含 `[package]` 与 `[workspace]`。根目录
+本身也是一个可构建的包。
```toml
[workspace]
@@ -71,7 +75,7 @@ myproject.core = { path = "libs/core" }
### 2.3 成员工程文件
-各成员维护独立的 `mcpp.toml`,结构与普通项目一致:
+各成员维护自己的 `mcpp.toml`,结构与普通工程相同:
```toml
# libs/core/mcpp.toml
@@ -84,7 +88,7 @@ version = "0.1.0"
kind = "lib"
```
-成员之间通过 `path` 依赖引用:
+成员之间通过 `path` 依赖相互引用:
```toml
# libs/http/mcpp.toml
@@ -100,9 +104,10 @@ myproject.core = { path = "../core" }
mbedtls.workspace = true
```
-## 3. 依赖版本继承
+## 3. 依赖版本的继承
-在 `[workspace.dependencies]` 中集中声明依赖版本,成员通过 `.workspace = true` 继承:
+在 `[workspace.dependencies]` 中集中声明依赖版本,成员通过 `.workspace = true`
+继承:
```toml
# 根 mcpp.toml
@@ -124,16 +129,17 @@ mbedtls.workspace = true # 继承版本 → "3.6.1"
gtest.workspace = true # 继承版本 → "1.15.2"
```
-成员可以覆盖继承的版本:
+成员可以覆盖继承来的版本:
```toml
[dependencies.compat]
-mbedtls = "4.0.0" # 覆盖,不使用 workspace 版本
+mbedtls = "4.0.0" # override; does not use the workspace version
```
-## 4. 工具链与构建配置继承
+## 4. 工具链与构建配置的继承
-工作空间根的 `[toolchain]` 和 `[target.]` 配置自动继承到所有成员。成员可在自身的工程文件中覆盖。
+工作空间根的 `[toolchain]` 与 `[target.]` 配置由全体成员自动继承。成员
+可以在自己的工程文件中覆盖。
配置优先级(从高到低):
@@ -144,7 +150,7 @@ mbedtls = "4.0.0" # 覆盖,不使用 workspace 版本
5. 内置默认值
```toml
-# 工作空间根
+# workspace root
[toolchain]
default = "gcc@16.1.0"
@@ -154,21 +160,21 @@ linkage = "static"
```
```toml
-# 某成员覆盖工具链
+# a member overrides the toolchain
[toolchain]
default = "llvm@20.1.7"
```
### 4.1 `[workspace.package]` 与 `[workspace.build]`
-所有成员共享的包元信息与构建标志,在 workspace 根声明一次:
+全体成员共享的包元信息与构建标志,在工作空间根声明一次:
```toml
[workspace]
members = ["libs/core", "libs/http", "apps/server"]
[workspace.package]
-standard = 26 # 也可写 "c++26",两种拼法都接受
+standard = 26 # or "c++26"; both spellings are accepted
version = "0.4.2"
license = "Apache-2.0"
authors = ["example"]
@@ -178,58 +184,61 @@ cxxflags = ["-Wall", "-Wextra"]
dialect_cxxflags = ["-fno-exceptions"]
```
-成员只声明属于它自己的部分:
+成员只声明属于自己的部分:
```toml
[package]
name = "core"
-# standard / version / license / authors 继承自 workspace
-# [workspace.build] 的 cxxflags 也继承
+# standard, version, license and authors are inherited;
+# [workspace.build] cxxflags are inherited
```
**合并规则。**
-| 类别 | 规则 |
+| 种类 | 规则 |
|---|---|
-| 标量(`standard`、`version`、`license`、`c_standard`、`linkage` 等) | 成员**声明了该键**时成员优先;否则取 workspace 的值 |
-| 向量(`cxxflags`、`ldflags`、`defines`、`dialect_cxxflags`、`include_dirs` 等) | 追加,**workspace 在前** —— 成员自己的标志排在命令行后面,后者生效 |
-| `[workspace.dependencies]` | 逐依赖显式选择加入,`x.workspace = true`(§3) |
-
-"声明了"指的是**这个键被写过**,而不是它的值与默认值不同。成员在
-`[workspace.package] standard = 26` 之下刻意写 `standard = "c++23"`,得到的就是
-c++23;什么都不写的成员得到 c++26。这两种情况的值相同而意图相反,所以这个事实是被
-**记录**下来的,而不是推断出来的。
-
-标量与向量是**隐式继承**,不需要逐键选择加入。workspace 要消除的漂移正是"某个成员忘了
-选择加入",所以继承是默认行为,覆盖才是需要主动表达的动作。依赖保留显式选择加入,因为
-依赖是解析图上的一条**边**:隐式继承一条边,会在成员自己的 manifest 只字未提的情况下改变
-它解析到什么。
-
-**成员可以省略 `version`**,只要 `[workspace.package]` 提供了它。这个字段整体上仍是必需的
-—— 两边都没有时会被拒绝,并同时指出成员文件和本该提供它的 workspace 键。
-
-**并非所有键都可继承。** `[workspace.build] allow_host_libs` 会被拒绝:它关掉的是某个具体
-产物的 hermetic 链接检查,而 workspace 根若能设置一次,就等于替所有后来加入的成员也关掉了
-这项检查 —— 而那些成员的作者可能从没读过根 manifest。**描述"如何构建"的键可继承;描述
-"不要跑哪项安全检查"的键留在产物所属的那个包里。** `[workspace.package]` /
-`[workspace.build]` 中其他不认识的键同样会被拒绝而不是忽略:一个以"传播"为唯一目的的表,
-若能静默丢弃某个键,产出的就是"看起来配置好了、实际没有"的 workspace。
-
-**没有 `[workspace.target.]`。** workspace 根里一个普通的 `[target.]` 块
-本来就会按 triple 逐项被所有成员继承(成员优先)。为同一能力再加一种拼法,只会增加接口面
-而不增加功能。
+| 标量(`standard`、`version`、`license`、`c_standard`、`linkage` 等) | 成员**声明了该键**时成员胜出;否则取工作空间的值 |
+| 向量(`cxxflags`、`ldflags`、`defines`、`dialect_cxxflags`、`include_dirs` 等) | 追加,**工作空间在前**——因而成员自己的标志排在命令行更后面,后者胜出 |
+| `[workspace.dependencies]` | 逐依赖显式选择加入,`x.workspace = true`(§3) |
+
+"声明了"指的是这个键被写过,而不是它的值与默认值不同。成员在
+`[workspace.package] standard = 26` 之下刻意写 `standard = "c++23"`,得到的就是
+c++23;什么都不写的成员得到 c++26。这两种情况值相同而意图相反,所以这一点被
+记录下来,而不是靠推断。
+
+标量与向量都是**隐式继承**,不需要逐键选择加入。工作空间要消除的正是"某个成员
+忘了选择加入"这种漂移,所以继承是默认行为,覆盖才是需要主动写出的动作。依赖保留
+显式选择加入,因为依赖是解析图上的一条边:隐式继承一条边,会在成员自己的
+manifest 只字未提的情况下改变它解析到什么。
+
+**成员可以省略 `version`**,只要 `[workspace.package]` 提供了它。这个字段整体上
+仍是必需的——两边都没有时会被拒绝,同时指出成员文件和本该提供它的那个
+workspace 键。
+
+**并非所有键都可继承。** `[workspace.build] allow_host_libs` 会被拒绝:它关闭的
+是某个具体产物的 hermetic 链接检查,而工作空间根若能设置一次,就等于替所有后来
+加入、可能从未读过根 manifest 的成员一并关闭了这项检查。**描述"如何构建"的键可
+继承;描述"不跑哪项安全检查"的键留在产物所属的那个包里。** `[workspace.package]`
+与 `[workspace.build]` 里其他不认识的键同样会被拒绝而不是被忽略:一张以"传播"
+为唯一目的的表,如果能静默丢弃某个键,产出的就是一个看起来配置好了、实际上没有
+的工作空间。
+
+**没有 `[workspace.target.]`。** 工作空间根里一个普通的 `[target.]`
+块本来就按 triple 逐项被全体成员继承(成员优先)。为同一能力再造一种拼法,只会
+增加接口面而不增加功能。
### 4.2 整个模块图只有一个标准
-C++ 模块图有且只有一个标准:BMI 跨档位不兼容,因此根包的 `standard` 会施加到图中每一个包,
-依赖也不例外。依赖自己的 `standard` 不会被应用。
+C++ 模块图有且只有一个标准:BMI 跨档位不兼容,因此根包的 `standard` 施加于图中
+每一个包,依赖也不例外。依赖自己的 `standard` 不会被应用。
-有一类包是例外。供给 C++ 层的包(即标准库本身)陈述了 `standard` 时,它的每个既不提供也不导入
-模块的翻译单元恰好以该档位编译;它的模块单元仍按图的档位编译
-([22 —— 目标侧](22-target-side.md)「标准库自身的语言级别」)。没有 BMI 穿过这些单元,因此上面的
-规则没有被打破;正是这一点让 c++20 工程可以使用源码按 C++23 编写的标准库。
+有一类包是例外。供给 C++ 层(即标准库本身)的包陈述了 `standard` 时,它那些既
+不提供也不导入模块的翻译单元恰好按该档位编译;它的模块单元仍按图的档位编译
+([22 —— 目标侧](22-target-side.md)「标准库自身的语言级别」)。没有 BMI 穿过这些
+单元,因此上面的规则并未被打破;正是这一点让一个 c++20 工程可以使用源码按 C++23
+编写的标准库。
-当依赖**声明**了高于当前图的档位时,mcpp 在编译前就报出来:
+当依赖**声明**了高于当前图的档位时,mcpp 在编译前就报出来:
```
warning: dependency `render` declares standard = "c++26", and this graph is
@@ -243,52 +252,56 @@ warning: dependency `render` declares standard = "c++26", and this graph is
standard = "c++26"
```
-这是 warning 而不是 error —— 这类构建通常仍然成功;`--strict` 会把它提升为错误。按上文被应用了声明的 C++ 层供给者不会被报出。它只对
-**工程作者自己拥有的 manifest** 生效(根包、workspace 成员、`path` 依赖):从索引解析来的
-包,其 `standard` 是描述符生成器写的,不是读到这条消息的人写的。
+这是 warning 而不是 error——这类构建通常仍会成功,`--strict` 会把它提升为
+错误。按上文被应用了声明的 C++ 层供给者不会被这样报出。它只对**工程作者自己拥有
+的 manifest** 生效(根包、workspace 成员、`path` 依赖):从索引解析来的包,其
+`standard` 是由描述符生成器写的,而不是由读到这条消息的人写的。
## 5. 构建命令
-### 5.1 从工作空间根目录构建与测试
+### 5.1 从工作空间根构建与测试
```bash
-mcpp build # 虚拟工作空间 → 构建所有成员;带根包 → 构建根包
-mcpp build -p server # 构建指定成员及其依赖
-mcpp build --workspace # 显式构建每个成员
-mcpp test # 虚拟工作空间 → 测试所有成员;带根包 → 测试根包
-mcpp test -p core # 测试单个成员
-mcpp test --workspace # 测试每个成员(逐成员汇报;遇失败继续)
+mcpp build # virtual workspace → builds ALL members; rooted → the root package
+mcpp build -p server # build a specific member and its dependencies
+mcpp build --workspace # build every member explicitly
+mcpp test # virtual workspace → tests ALL members; rooted → the root package
+mcpp test -p core # test a single member
+mcpp test --workspace # test every member (one report per member; continues past failures)
```
-在**虚拟工作空间**根(只有 `[workspace]`、无 `[package]`)下,裸 `mcpp build` /
-`mcpp test` 作用于**所有**成员;在**带根包工作空间**(`[package]` + `[workspace]`)下作用于
-根包,用 `--workspace` 纳入全部成员。`mcpp test --workspace` 独立构建+运行每个成员的
-`tests/**/*.cpp`——测试发现按成员隔离,因此两个成员各有一个 `tests/main.cpp` 也不会冲突。
+在**虚拟**工作空间根(只有 `[workspace]`、没有 `[package]`)下,裸 `mcpp build` /
+`mcpp test` 作用于**全体**成员;在**带根包**的工作空间(`[package]` +
+`[workspace]`)下,两者作用于根包,用 `--workspace` 才纳入全体成员。
+`mcpp test --workspace` 独立构建并运行每个成员的 `tests/**/*.cpp`——发现按成员
+隔离,因此两个成员各有一个 `tests/main.cpp` 也不冲突。
### 5.2 从成员子目录构建
```bash
cd libs/http
-mcpp build # 自动检测工作空间,构建当前成员
+mcpp build # auto-detects the workspace and builds the current member
```
-mcpp 从当前目录向上搜索,若发现包含 `[workspace]` 的 `mcpp.toml` 且当前目录在 `members` 列表中,则自动进入工作空间模式,继承工作空间配置。
+mcpp 从当前目录向上搜索;若发现某个 `mcpp.toml` 含 `[workspace]` 且当前目录在其
+`members` 列表中,则自动进入工作空间模式并继承工作空间配置。
### 5.3 `-p, --package` 选项
-`-p` 可用于 `build`、`test`、`run` 等命令,指定构建的目标成员。参数值为成员路径的最后一段目录名或完整相对路径:
+`-p` 可用于 `build`、`test`、`run` 等命令,指定目标成员。参数值可以是成员目录名
+的最后一段,也可以是完整相对路径:
```bash
-mcpp build -p server # 匹配 apps/server
-mcpp test -p core # 匹配 libs/core
+mcpp build -p server # matches apps/server
+mcpp test -p core # matches libs/core
mcpp run -p server -- --port 8080
```
-`--workspace`(用于 `build` 和 `test`)是扇出形式:作用于**每个**成员。
-`mcpp test --workspace` 逐成员独立汇报、遇失败继续,只要有任一成员失败即非零退出——
-非常适合作为「一个测试众多库的工作空间」的单条、无 shell 的 CI 步骤。
+`--workspace`(用于 `build` 与 `test`)是扇出形式:作用于**每个**成员。
+`mcpp test --workspace` 逐成员分别汇报,遇失败继续,只要有任一成员失败就非零
+退出——很适合作为"一个测试众多库的工作空间"单条、无需 shell 的 CI 步骤。
-#### 扇出的汇报内容
+#### 扇出的汇报
```
Workspace testing member 'libs/core' (3/97)
@@ -300,25 +313,26 @@ test_paths ... ok (0.31s)
slowest: libs/jsc 93.5s, libs/install 32.2s, libs/http 24.1s
```
-`M/N` 进度、逐测试耗时,以及**按 build / run 拆开**的成员耗时。拆开才是有用的那部分:
-一个测试只要几毫秒、但链接要 90 秒的成员,在单个合并数字里和「测试套件很慢」长得一模一样,
-而两者里只有一个值得去查。
+`M/N` 进度、逐测试耗时,以及按 **build** 与 **run** 拆开的成员耗时。拆开才是有用
+的部分:一个测试只要几毫秒、但链接要 90 秒的成员,在单个合并数字里与"测试套件本身
+很慢"长得一模一样,而这两种情形只有一种值得去查。
-`--message-format json` 承载同样的数据。每条 test 记录都带 `"member"` 限定,流末尾是一条
-`workspace_summary`,列出失败成员与未运行成员 —— 一旦两个成员都有名为 `smoke` 的测试,
-裸测试名就不再可归因。
+`--message-format json` 以 NDJSON 承载同样的数据。每条 test 记录都带成员限定
+字段(`"member"`),流的末尾是一条 `workspace_summary` 记录,列出失败成员与未
+运行成员——一旦两个成员都有一个叫 `smoke` 的测试,裸测试名就不再能归因。
#### 给扇出设期限
```bash
-mcpp test --workspace --timeout 60 # 单测试**运行**期限(默认 300)
-mcpp test --workspace --build-timeout 300 # 单次 ninja 驱动期限(默认 0 = 不限)
-mcpp test --workspace --workspace-timeout 1800 # 整条扇出(默认 0 = 不限)
+mcpp test --workspace --timeout 60 # per-test RUN deadline (default 300)
+mcpp test --workspace --build-timeout 300 # per-ninja-drive deadline (default 0 = no limit)
+mcpp test --workspace --workspace-timeout 1800 # whole fan-out (default 0 = no limit)
```
-扇出是串行的,所以一个没有上界的成员会拖住它后面的所有成员。三个期限都是**汇报而非中止**:
-测试超时只判该测试失败、扇出继续;构建超时只判该成员失败;`--workspace-timeout` 停止扇出并
-列出未运行的成员 —— 而不是把进程留给 CI 去 kill(那会把它想说的话一并丢掉)。
+扇出是串行的,所以一个没有上界的成员会拖住排在它后面的每一个成员。三个期限都是
+**汇报而非中止**:测试超时只判该测试失败,扇出继续;构建超时只判该成员失败;
+`--workspace-timeout` 停止扇出并列出未运行的成员,而不是把进程留给 CI 去 kill——
+那样会把进程本该说出的话一并丢掉。
## 6. 目录布局
@@ -326,7 +340,7 @@ mcpp test --workspace --workspace-timeout 1800 # 整条扇出(默认 0 = 不
```
myproject/
-├── mcpp.toml # [workspace] 声明
+├── mcpp.toml # [workspace] declaration
├── libs/
│ ├── core/
│ │ ├── mcpp.toml # [package] namespace="myproject" name="core"
@@ -343,22 +357,24 @@ myproject/
└── main.cpp # import myproject.http;
```
-各成员的构建产物位于各自的 `target/` 子目录下。
+各成员的构建产物存在各自的 `target/` 子目录下。
-工作空间之外的项目以 member 的身份引用托管在 git 上的工作空间中的 member:
-`myproject.http = { git = "...", rev = "..." }` 在根清单的 `members` 中选中 `libs/http`,
-提交相同,且该 member 与在此处一样继承 `[workspace.package]`(mcpp 2026.9.16.1+;见
-[05 —— 依赖](05-dependencies.md))。
+工作空间之外的工程,以成员身份引用托管在 git 上的工作空间中的一个成员:
+`myproject.http = { git = "...", rev = "..." }` 会在根 manifest 的 `members`
+中选中 `libs/http`,取同一个提交,而该成员会像在工作空间内部一样继承
+`[workspace.package]`(mcpp 2026.9.16.1+;见 [05 —— 依赖](05-dependencies.md))。
## 7. 与 C++ 模块的关系
工作空间与 C++23 模块机制协同工作:
-- **接口可见性由语言控制** — `export module` 和 `import` 语句决定模块的公开接口,工作空间不做额外的可见性限制
-- **模块名由库作者决定** — 工作空间不强制模块名与包名或命名空间一致
-- **partition 用于内部组织** — `import :internal;`(不带 `export`)的 partition 对消费者不可见,无需构建工具介入
+- **接口可见性由语言控制**——`export module` 与 `import` 语句决定一个模块的
+ 公开接口,工作空间不施加额外的可见性限制。
+- **模块名由库作者决定**——工作空间不要求模块名与包名或命名空间一致。
+- **partition 用于内部组织**——通过 `import :internal;`(不带 `export`)导入
+ 的 partition 对消费者不可见,不需要构建工具介入。
## 8. 完整示例
-参见 [`examples/04-workspace/`](../../examples/04-workspace/),包含一个三成员工作空间的完整可运行示例。
-
+见 [`examples/04-workspace/`](../../examples/04-workspace/),一个三成员工作空间
+的完整可运行示例。
diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md
index 9070d0277..9ce4b0c1a 100644
--- a/docs/zh/08-testing.md
+++ b/docs/zh/08-testing.md
@@ -1,79 +1,80 @@
# 08 —— 测试
-**读者:**任何有代码需要持续可用的人。
+**读者:** 任何拥有「必须持续可用」的代码的人。
-**本章回答的那一个问题:**测试怎么写、怎么跑,mcpp 认为什么是一个测试,以及
-在本机跑不了的东西怎么测。
+**本章回答的那一个问题:** 测试怎么写、怎么跑,mcpp 把什么算作一个测试,以及
+在本机跑不了的东西如何测试。
-**不在这里:**runner 怎么抵达一台设备 —— 那是
-[41 —— 抵达一台设备](41-devices.md);以及机器可读流的 schema,那是
-[50 —— 机器可读输出](50-machine-output.md)。本章只说明哪个旗标产生它,到此为止。
+**不在这里:** runner 如何抵达一台设备 —— 那是
+[41 —— 抵达一台设备](41-devices.md);机器可读流的 schema 是
+[50 —— 机器可读输出](50-machine-output.md)。本章只说明哪个旗标产生它,到此为止。
-在此之前:[05 —— 依赖与解析](05-dependencies.md) 覆盖 `[dev-dependencies]`,
-那是测试如何取到产物取不到的包。在此之后:
+在此之前:[05 —— 依赖与解析](05-dependencies.md) 覆盖 `[dev-dependencies]`,
+那是测试用来取到产物取不到的包的手段。在此之后:
[09 —— 按场景选命令](09-commands-by-scenario.md) 是其余一切的查阅入口。
## 测试的定义
-每一个 `tests/**/*.cpp` 都是一个测试:mcpp 把每个文件编译成它自己的程序并运行它。
-测试通过的判据是它的程序以 0 退出。
+每一个 `tests/**/*.cpp` 都是一个测试:mcpp 把每个文件编译成它自己的程序并运行。
+测试通过的判据是该程序以 0 退出。
```
myproject/
mcpp.toml
src/…
tests/
- test_parse.cpp 一个程序
- unit/test_span.cpp 另一个
+ test_parse.cpp one program
+ unit/test_span.cpp another
```
-没有框架,也不需要注册。测试**可以**用一个框架 —— `[dev-dependencies]` 是它取到
-框架的方式 —— 但 mcpp 持有的契约是退出码,这也是为什么为别的框架写的测试不需要
-适配层。
+没有框架,也不需要注册。测试可以使用一个框架 —— `[dev-dependencies]` 是取到框架的
+途径 —— 但 mcpp 持有的契约只是退出码,这也是为什么为其他框架写的测试不需要适配层。
-`mcpp new` 会生成 `tests/test_smoke.cpp`,让工程一开始就有这个目录。
+`mcpp new` 生成 `tests/test_smoke.cpp`,使工程从一开始就带有这个目录。
### 测试的位置
-`tests/**/*.cpp` 是一个键的默认值:
+`tests/**/*.cpp` 是一个键的默认值:
```toml
[test]
discover = ["checks/**/*.cpp", "!checks/fixtures/**"]
```
-`discover` 接受与 `[build] sources` 同一套词汇的 glob:glob 匹配到的每个文件都是一个
-测试程序,以 `!` 开头的 glob 把它匹配到的文件从集合中去掉,无论是哪个 glob 找到的。
-测试的名字是它相对于第一个匹配它的 glob 的固定目录的路径,去掉扩展名,因此默认值
-把 `tests/unit/test_span.cpp` 命名为 `unit/test_span`。`discover = []` 不发现任何测试。
-名字相同的两个文件会被拒绝,并点名两者。
+`discover` 接受与 `[build] sources` 同一套词汇的 glob:某个 glob 匹配到的每个文件
+都是一个测试程序,以 `!` 开头的 glob 把它匹配到的文件从集合中剔除,无论这些文件是
+被哪个 glob 找到的。一个测试的名字,是它相对于第一个匹配它的 glob 所在固定目录的
+路径,去掉扩展名 —— 因此默认配置下 `tests/unit/test_span.cpp` 的名字是
+`unit/test_span`。`discover = []` 不发现任何测试。两个名字相同的文件会被拒绝,并
+点出这两者。
-由多个源文件编译成的测试套件是一个独立的包:一个工作区成员,它的 `[build] sources`
-承载套件,它唯一的测试程序驱动套件,用 `mcpp test -p ` 选中。
+由多个源文件编译而成的测试套件本身是一个独立的包:一个工作区成员,它的
+`[build] sources` 承载整套套件,其唯一的测试程序驱动套件,用
+`mcpp test -p ` 选中。
-## 运行它们
+## 运行测试
```bash
-mcpp test # 构建并运行每个测试
-mcpp test parse # 只运行名字匹配的那些
-mcpp test --list # 列出会跑哪些,不构建也不运行
-mcpp test -- --verbose # `--` 之后的一切传给每个测试程序
+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
```
-测试的构建轴与 `mcpp build` 相同,因此测试跑在它要检查的那个配置上,而不是默认
-配置上:
+测试的构建轴与 `mcpp build` 相同,因此测试跑在它要检查的那个配置上,而不是默认
+配置上:
| 旗标 | 选中的集合 |
|---|---|
-| `--profile ` | `dev`(默认)、`release`、`dist`,或 manifest 声明的某个 `[profile.*]` |
-| `--features ` | 这次测试构建的 feature 集合 |
+| `--profile ` | `dev`(默认)、`release`、`dist`,或 manifest 声明的某个 `[profile.*]` |
+| `--features ` | 本次测试构建的 feature 集合 |
| `--target ` | 宿主以外的目标 |
| `--accel ` / `--no-accel` | 本次构建面向的设备后端 |
| `--cap ` | 钉住某个能力的 provider |
-| `--toolchain ` | 本次调用使用的工具链,例如 `llvm@22.1.8` |
+| `--toolchain ` | 本次调用使用的工具链,例如 `llvm@22.1.8` |
-`--timeout ` 杀掉仍在运行的测试(默认 300;`0` 关闭),`--build-timeout `
-限制编译。一个挂住的测试被报为**以它自己的名字失败**,而不是一个停下来的任务。
+`--timeout ` 杀掉仍在运行的测试(默认 300;`0` 关闭),`--build-timeout `
+限制编译耗时。挂起的测试以它自己的名字被报为失败,而不是报成一个停止的任务。
## 取到产物取不到的包的测试
@@ -82,57 +83,58 @@ mcpp test -- --verbose # `--` 之后的一切传给每个测试程序
counters = { path = "../counters" }
```
-`[dev-dependencies]` 的条目只为测试构建解析,别处一概不用:它不在产物里,包的
-消费者也永远看不见它。这就是「测试的依赖」与「包自己的依赖」之间的区别,也是
-测试框架不会变成一个库所发布内容的一部分的原因。
+`[dev-dependencies]` 的条目只为测试构建解析,别处一律不用:它不在产物里,包的消费者
+也永远看不到它。这就是「测试的依赖」与「包自己的依赖」之间的区别,也是测试框架不会
+成为一个库所发布内容之一部分的原因。
-[`examples/11-features`](../../examples/11-features/) 声明了一个并使用它。
+[`examples/11-features`](../../examples/11-features/) 声明了一个这样的依赖并使用它。
## 在本机跑不了的目标上测试
-面向交叉目标或裸机板子的测试,会被编译到那个目标,并经由一个 **runner** 执行 ——
-runner 是板级支持包提供的一串 argv,mcpp 把测试二进制附加在其后执行它。
+面向交叉目标或裸机板子的测试被编译到那个目标,并经由一个 **runner** 执行 ——
+runner 是板级支持包提供的一串 argv,mcpp 把测试二进制附加在其后交给它执行。
```bash
-mcpp test --target thumbv7em-none-eabihf # 为板子构建,经它的 runner 运行
-mcpp test --no-runner # 忽略 runner,直接执行
-mcpp test --target aarch64-macos --no-run # 只为目标构建测试,不执行
+mcpp test --target thumbv7em-none-eabihf # built for the board, run through its runner
+mcpp test --no-runner # ignore the runner and execute directly
+mcpp test --target aarch64-macos --no-run # build the tests for the target and stop
```
-测试本身一个字都不用改。同样的 `tests/**/*.cpp` 为设备编译,判据仍然是退出码 ——
-这正是裸机 runner 被选成「能产生 semihosting 退出码或 QEMU 退出码」的原因。
+测试本身没有任何改变。同样的 `tests/**/*.cpp` 为设备编译,判据仍然是退出码 ——
+这正是裸机 runner 被选来产生 semihosting 退出码或 QEMU 退出码的原因。
-`--no-runner` 是给「本机就能原生执行这些二进制、不该为模拟器付代价」的宿主准备的。
+`--no-runner` 是为「本机能原生执行这些二进制、不该为模拟器付代价」的宿主准备的。
-`--no-run` 做的是更窄的那个断言,而它必须被显式要求。没有它时,一个本机既不能执行、
-也没有 runner 可达的目标,会让每个测试都停在 not run,命令退出 2:mcpp 没有查明这些
-测试是否通过,而把它报成成功是本仓记录得最多的一种假读数。但 2 同样是 runner 坏掉时
-的退出码,于是一个只想要「构建」的调用方无法区分这两者。在 `--no-run` 下,每个被选中
-的测试都为该目标编译并链接,没有任何一个被执行,结果也这么写:
+`--no-run` 做出的是更窄的断言,且必须显式要求。不给它时,一个本机既无法执行、也没有
+runner 可达的目标,会让每个测试都停在「未运行」,命令退出 2:mcpp 没有查明这些测试
+是否通过,把这种情况报成成功,是本仓记录得最多的一种假读数。但 2 同样是 runner
+坏掉时的退出码,于是一个只想要「构建」结果的调用方无法区分这两种情况。在 `--no-run`
+下,每个被选中的测试都为目标编译并链接,没有一个被执行,结果也如实写出:
```
test result ok. 0 passed; 0 failed; 2 built, not run
```
-编译不过的测试仍然是失败;`--no-run` 与 `--no-runner` 同时给出会被拒绝,而不是在两者
-之间挑一个:一个说的是「不经声明的 runner 直接执行」,另一个说的是「不要执行」。
+编译不过的测试仍然是失败。`--no-run` 与 `--no-runner` 同时给出会被拒绝,而不是在
+两者间择一执行:一个说的是「不经声明的 runner 直接执行二进制」,另一个说的是
+「不要执行」。
-测试程序把它读取的文件带在身边:runner 收到 `MCPP_RUNTIME_FILES`,即它部署的文件与
-它加载的共享库的清单,把程序移到设备上的 runner 连同这些文件一起复制。测试按相对于
-自身所在目录的路径定位这类文件。在 Android 行上,除非 `cxx_runtime` 另有声明,测试
-程序静态链接 C++ 运行时,因此设备上不需要 `libc++_shared.so`。
+测试程序把它读取的文件带在身边:runner 收到 `MCPP_RUNTIME_FILES`,即它已部署的文件
+与它加载的共享库的清单,把程序移动到设备上的 runner 会把这些文件一并复制过去。测试
+按相对于自身所在目录的路径定位这类文件。在 Android 的各行上,除非 `cxx_runtime`
+另有声明,测试程序静态链接 C++ 运行时,因此设备上不需要 `libc++_shared.so`。
-runner 本身、具名 runner,以及一块板子声明什么,见
+runner 本身、具名 runner,以及一块板子声明了什么,见
[41 —— 抵达一台设备](41-devices.md)。
## 不能彼此并排运行的测试
-`mcpp test` 在一个工作池上运行测试程序。一块板子接一个探针、一块 GPU、一个串口,
-或者一份单座许可证,同一时刻只容一个使用者;两个 worker 同时去拿的结果是交错,
+`mcpp test` 在一个工作池上运行测试程序。一块接了一个探针的板子、一块 GPU、一个串口,
+或者一份单座许可证,同一时刻只容一个使用者;两个 worker 同时去争抢它的结果是交错,
而不是干净地失败。
-拥有该资源的那个包**自己声明**这一点,`mcpp test` 随后把这些测试串行化。工程方
-永远不需要记得加 `-j1`。
+拥有该资源的包对此做出声明,`mcpp test` 随后把这些测试串行化。工程方永远不需要
+记得加上 `-j1`。
## 报告给程序
@@ -140,16 +142,17 @@ runner 本身、具名 runner,以及一块板子声明什么,见
mcpp test --message-format json
```
-每个测试一条 NDJSON 记录,供 CI 任务或编辑器消费。schema 及其版本见
-[50 —— 机器可读输出](50-machine-output.md);属于本章的只有「这个旗标存在」以及
-「人类可读格式是默认」。
+每个测试一条 NDJSON 记录,供 CI 任务或编辑器使用。schema 及其版本见
+[50 —— 机器可读输出](50-machine-output.md);本章只交代这个旗标存在,以及人类可读
+格式是默认值。
## 当前边界
-- 一个测试是一个 `.cpp` 产出一个程序。mcpp 不发现文件内部的用例,因此框架的
- 逐用例选择发生在程序内部,经由 `--` 之后的参数。
-- 清单无法加载时,`mcpp test --list` 列出的是 `tests/**/*.cpp`,而不是它读不到的
+- 一个测试是一个 `.cpp` 产出一个程序。mcpp 不发现文件内部的用例,因此框架的逐用例
+ 选择发生在程序内部,经由 `--` 之后的参数传递。
+- manifest 加载不了时,`mcpp test --list` 列出的是 `tests/**/*.cpp`,而不是它读不到的
`[test] discover` 集合。
- `--build-timeout` 只在 POSIX 上有效。
-- `--workspace-timeout` 限制 `--workspace` 的扇出并报告跑到了哪些;它不把超时
- 归因到某个成员。
+- `--workspace-timeout` 限制 `--workspace` 扇出的耗时,并报告哪些成员跑完了;它不把
+ 超时归因到某一个成员。
+
diff --git a/docs/zh/09-commands-by-scenario.md b/docs/zh/09-commands-by-scenario.md
index 8a7cc9cb7..d94d1d83c 100644
--- a/docs/zh/09-commands-by-scenario.md
+++ b/docs/zh/09-commands-by-scenario.md
@@ -1,35 +1,37 @@
# 09 —— 按场景选命令
-**读者:**已经认识那些名词、现在想找动词的人。
+**读者:** 已经认识那些名词、现在想找动词的人。
-**本章回答的那一个问题:**手上这件事该用哪个命令 —— 回收磁盘、解释一次解析、
-校验一个描述符、诊断环境。
+**本章回答的那一个问题:** 手上这件事该用哪条命令 —— 回收磁盘、解释一次
+解析、校验一个描述符、诊断环境。
-**不在这里:**每个命令的含义细节。一个场景点名命令,并链接到拥有它的那一章。
-在此之前:[08 —— 测试](08-testing.md)。
+**不在这里:** 每条命令含义上的细节。一个场景点名命令,并链接到拥有它的
+那一章。在此之前:[08 —— 测试](08-testing.md)。
-命令清单是 `mcpp --help`,每个子命令还有自己的 `--help`。本章回答的是另一个问题:
-某个情形已经发生时该用哪条命令 —— 构建目录一直变大、解析结果出乎意料、描述符即将
-发布、索引可能陈旧。这里收的都是名字本身没有说出它所属场景的命令。
+命令清单是 `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)。
+相关文档:[01 —— 快速开始](01-getting-started.md)(日常构建与测试循环)、
+[20 —— 工具链管理](20-toolchains.md)、
+[11 —— 发布一个库](11-publishing-a-library.md)、
+[50 —— 机器可读输出](50-machine-output.md)。
-下面每段输出都由本章所在版本的 mcpp 实际产生。
+下面每一段输出,都由本章所对应版本的 mcpp 实际产生。
## 回收磁盘而不触发重编
-有两个存储会增长,增长的原因不同,各由一条命令清空。把两者弄混的代价是一次全量重编。
+有两个存储会增长,增长的原因不同,各由一条命令清空。把两者弄混,代价是一次
+全量重编。
| 存储 | 作用域 | 增长时机 | 清空方式 |
|---|---|---|---|
-| `target/<三元组>/<指纹>/` | 单个工程 | 配置指纹变化,开出新目录 | `mcpp clean`、`mcpp clean --stale` |
-| 构建缓存(`mcpp cache dir`) | 整台机器 | 任何工程编译依赖、`std` 模块,或构建一个 host 工具 | `mcpp cache gc`、`mcpp cache prune`、`mcpp cache clean` |
+| `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` 整个删掉 `target/`,下次构建重编一切。`mcpp clean --stale`
+只删已无构建记录使用的指纹目录,在用的配置保留:
```
$ mcpp clean --stale --dry-run
@@ -37,22 +39,25 @@ would remove target/x86_64-linux-gnu/0123456789abcdef (0.0 B)
Would remove 1 directory (0.0 B)
```
-「在用」指被 `target/.build_cache` 记录 —— 它由 `mcpp build` 写入,由快路径读取。
-这个定义带来三个后果:
+"在用"指被 `target/.build_cache` 记录 —— 它由 `mcpp build` 写入,由快
+路径读取。这个定义带来三个推论:
-- 没有记录并不足以让一个目录被删。`mcpp test` 走的构建路径不写记录,`--no-cache`
- 构建同样不写。未被记录但在 `--older-than`(默认一天)之内写过的目录保留;更旧的
- 会被删,而判断错误的代价是重编一个此后无人碰过的配置。
-- 完全没有记录时,命令拒绝执行而不是猜测。跑一次 `mcpp build` 即可确立什么是当前的。
-- `target/` 下不是指纹目录的东西 —— 例如 `mcpp pack` 的 `dist/` —— 从不被访问。
+- 一个没有记录的目录,不会仅因此就被删除。`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`
-不保留任何未记录目录;负的时长被拒绝。
+`--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)` 那样的行,是被
-中断的构建留下的条目:
+构建缓存是全机共享的,因此工程级命令不得清空它 —— `--stale` 与
+`--bmi-cache` 不能同时给出。`mcpp cache list` 列出占用。行没有排序,而像
+`0.0 B (incomplete)` 那样的行,是被中断的构建留下的条目:
```
$ mcpp cache list
@@ -61,14 +66,15 @@ key kind size last used package
9234eed9ef786c13 std 0.0 B 2d ago std (incomplete)
```
-`mcpp cache gc` 要求给出 `--max-size`、`--older-than` 或两者,并且只驱逐包条目。一份
-`std` BMI 被机器上每个工程共享,实现以「重建它是用大量时间换少量磁盘」为由把它排除在
-按体积驱逐之外。`mcpp cache clean --std` 仍是显式移除它的做法。
+`mcpp cache gc` 要求给出 `--max-size`、`--older-than` 或两者,并且只驱逐
+包条目。一份 `std` BMI 被机器上每个工程共享,实现以"重建它是用大量时间
+换少量磁盘"为由,把它排除在按体积驱逐之外。`mcpp cache clean --std` 仍是
+显式移除它的做法。
## 一个包已发布的版本
-`mcpp search` 按子串匹配,并在每个命中行后附上该包发布的版本 —— 跨描述符的 per-OS 表
-合并,按 semver 降序:
+`mcpp search` 按子串匹配,并在每个命中行后附上该包发布的版本 —— 跨描述符
+的 per-OS 表合并,按 semver 降序:
```
$ mcpp search imgui
@@ -76,24 +82,25 @@ $ mcpp search imgui
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 失败。
+末尾的 `, ...` 标记截断:默认显示三个版本,没有这个标记就说明列表是完整的。
+`--all-versions` 打印全部。一个描述符读不到的包,按两列输出 —— 版本列表是
+尽力而为的展示,从不会让 search 失败。
-`mcpp add` 在名字解析不到时携带同样的信息。建议里给出该写的命名空间,以及它背后的版本:
+`mcpp add` 在一个名字解析不到时,携带同样的信息。建议里给出该写的命名
+空间,以及它背后的版本:
```
a package with this name exists under another namespace:
compat.eui-neo (0.5.6, 0.5.5, 0.5.3)
```
-这次扫描只在查找已经失败之后进行,结果只进入错误文本与 search 输出。裸名不会因此跨
-命名空间解析。
+这次扫描只在查找已经失败之后进行,结果只进入错误文本与 search 输出。裸名
+不会因此跨命名空间解析。
## 为一次调用换一个工具链
-`mcpp build`、`mcpp run`、`mcpp test` 与 `mcpp pack` 接受 `--toolchain `,
-它为这一次调用选择编译器,不写入任何东西:
+`mcpp build`、`mcpp run`、`mcpp test` 与 `mcpp pack` 接受
+`--toolchain `,它为这一次调用选择编译器,不写入任何东西:
```bash
mcpp test --toolchain llvm@22.1.8
@@ -101,13 +108,14 @@ mcpp run --toolchain gcc@16.1.0
mcpp pack --toolchain llvm@22.1.8 --format dir
```
-对这一次调用,这个选项取代 `mcpp.toml` 中的 `[toolchain] default`,其优先级即
-[20 —— 工具链管理](20-toolchains.md) 给 `MCPP_TOOLCHAIN` 的那一级。每个工具链构建到
-它自己的输出目录,一次记录下来的构建只为记录它的那个工具链请求重放。
+对这一次调用,这个选项取代 `mcpp.toml` 中的 `[toolchain] default`,优先级
+与 [20 —— 工具链管理](20-toolchains.md) 给 `MCPP_TOOLCHAIN` 的那一级相同。
+每个工具链构建到它自己的输出目录,一次记录下来的构建只为记录它的那个工具链
+请求重放。
## 解释一次解析
-`mcpp why` 报告一次构建会解析出什么,并且不构建任何东西:
+`mcpp why` 报告一次构建会解析出什么,并且不构建任何东西:
```
$ mcpp why toolchain
@@ -116,9 +124,9 @@ toolchain: gcc 16.1.0 (x86_64-linux-gnu)
reason: [toolchain] in mcpp.toml if set, else platform-native default
```
-`mcpp why deps` 在 `mcpp.lock` 的各行之前列出解析出的依赖图(2026.9.14.2+):每个包、
-每条请求书写时用的键与所在的表,以及库的链接形态与其原因。锁文件不记录的 `path`
-依赖同样列出:
+`mcpp why deps` 在 `mcpp.lock` 各行之前列出解析出的依赖图(2026.9.14.2+):
+每个包、每条请求书写时用的键与所在的表,以及库的链接形态与其原因。锁文件
+不记录的 `path` 依赖同样列出:
```
$ mcpp why deps
@@ -130,14 +138,16 @@ dependency graph:
linked static (default)
```
-同一张图记录在 `target///resolution.json` 的 `graph` 下,每个包一条,
-根在最前:`package`(规范身份、命名空间、名字、版本、来源)、`root`、
-`requested_by`(`requester`、`key`、`table`),库还有 `link`(`form`、`reason`)。
+同一张图记录在 `target///resolution.json` 的 `graph` 下,
+每个包一条,根在最前:`package`(规范身份、命名空间、名字、版本、来源)、
+`root`、`requested_by`(`requester`、`key`、`table`),对库还有 `link`
+(`form`、`reason`)。
-话题是 `toolchain`、`runtime`、`deps` 或 `runners`,不给话题时四者全报。`--target` 与
-`--toolchain` 把报告变成对当前目录并不使用的那一对的查询,目标矩阵正是这样逐格提问的。
+话题是 `toolchain`、`runtime`、`deps` 或 `runners`,不给话题时四者全报。
+`--target` 与 `--toolchain` 把报告变成对当前目录并不使用的那一对的查询,
+一个目标矩阵正是这样逐格提问的。
-诊断里的错误码可以用 `mcpp self explain` 展开:
+诊断里的一个错误码,可以用 `mcpp self explain` 展开:
```
$ mcpp self explain E0006
@@ -150,7 +160,7 @@ silently misbehave, so resolution stops instead. Upgrade mcpp:
## 索引新鲜度与离线构建
-`mcpp index status` 在不碰网络的前提下回答本地索引副本是否当前:
+`mcpp index status` 在不碰网络的前提下,回答本地索引副本是否当前:
```
$ mcpp index status
@@ -159,17 +169,19 @@ $ mcpp index status
mcpplibs fresh 28s ago d4b36d7 /home/speak/.mcpp/registry/data/mcpplibs
```
-`mcpp index update` 刷新它们。一个刚发布几分钟、刷新后仍然找不到的包,是传播问题而不是
-名字问题 —— 索引以 artifact 而非 git clone 的形式到达客户端。
+`mcpp index update` 刷新它们。一个刚发布几分钟、刷新后仍然找不到的包,
+是传播问题而不是命名问题 —— 索引以 artifact 而非 git clone 的形式到达
+客户端。
-`--offline`(或 `MCPP_OFFLINE=1`)在单次调用中禁止网络,宁可失败也不拉取。`--locked` 在
-解析结果与 `mcpp.lock` 不一致时失败而不是改写它,这正是 CI 作业需要的形状。
-`mcpp index pin ` 把自定义索引的某个 commit 记进 `mcpp.toml`;
-`mcpp index unpin` 移除它。
+`--offline`(或 `MCPP_OFFLINE=1`)在单次调用中禁止网络,宁可失败也不拉取。
+`--locked` 在解析结果与 `mcpp.lock` 不一致时失败,而不是改写它,这正是 CI
+作业需要的形状。`mcpp index pin ` 把一个自定义索引的某个
+commit 记进 `mcpp.toml`;`mcpp index unpin` 移除它。
## 发布前校验描述符
-`mcpp xpkg parse` 用解析器自己的文法读描述符,所以它报告的就是解析时会看到的:
+`mcpp xpkg parse` 用解析器自己的文法读一个描述符,所以它报告的就是解析时
+会看到的:
```
$ mcpp xpkg parse mcpp.plugins.lua
@@ -181,9 +193,9 @@ form A — no mcpp segment (build info from the source's mcpp.toml)
parse OK
```
-per-OS 列表分开打印是有意的:一个版本只加进了某一个平台表而在其余表里被遗漏,在缺它的
-平台上读起来就是「找不到」,而文件里明明含有这个版本字符串。`--json` 以同样的事实供脚本
-使用:
+per-OS 列表分开打印是有意的:一个版本只加进了某一个平台表、在其余表里被
+遗漏,在缺它的平台上读起来就是"找不到",而文件里明明含有这个版本字符串。
+`--json` 以同样的事实供脚本使用:
```
$ mcpp xpkg parse mcpp.plugins.lua --json
@@ -191,12 +203,12 @@ $ mcpp xpkg parse mcpp.plugins.lua --json
```
`mcpp emit xpkg` 生成要提交的条目。完整路径见
-[11 — 发布库到 mcpp-index](11-publishing-a-library.md)。
+[11 —— 发布一个库](11-publishing-a-library.md)。
## 环境诊断
-`mcpp self doctor` 检查工具链、`std` 模块、registry、缓存健康与最近一次运行期闭包判定,
-并报告它查到了什么,而不只报告失败的部分:
+`mcpp self doctor` 检查工具链、`std` 模块、registry、缓存健康与最近一次
+运行期闭包判定,并报告它查到了什么,而不只报告失败的部分:
```
$ mcpp self doctor
@@ -207,19 +219,20 @@ $ mcpp self doctor
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 self env` 打印路径与已解析的工具链,包括 `--format json`。
+`mcpp self config --mirror CN|GLOBAL` 选择下载镜像;mcpp 与 xlings 各自
+持有这个设置,为其中一个选定,不会为另一个选定。
-## `[hooks]` —— 项目构建生命周期命令(实验性)
+## `[hooks]` —— 项目构建生命周期命令(实验性)
-> **实验性。** Hook 目前**不能**决定一次构建成功与否。所有 Hook 失败都以
-> **warning** 报出,`mcpp build` 保留它自己挣来的结果;`side_effect = true`
-> 会被报错拒绝,而不是被采纳。这个键保留在 schema 里,这样今天写下的 manifest
-> 在该功能转正时无需改动。另有两条限制是永久的、不是临时的:**只有根项目的 Hook
-> 会执行**,而且**只有 `mcpp build` 会执行它们**。
+> **实验性。** Hook 目前**不能**决定一次构建是否成功。每一次 Hook 失败
+> 都以 **warning** 报出,`mcpp build` 保留它自己挣来的结果;
+> `side_effect = true` 会被报错拒绝,而不是被采纳。这个键留在 schema 里,
+> 这样今天写下的 manifest,在该功能转正时不必改动。另有两条限制是永久的,
+> 不是临时的:**只有根项目的 Hook 会执行**,而且**只有 `mcpp build`
+> 会执行它们**。
-Hook 是 `mcpp build` **在一段区间内持有**的命令,事件名就是那段区间:
+Hook 是 `mcpp build` **在一段区间内持有**的命令,事件名就是那段区间:
```toml
[hooks]
@@ -227,107 +240,119 @@ 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` 会被拒绝
+side_effect = false # `true` is refused while this is experimental
```
| 键 | 类型 | 默认值 | 它命名的区间 |
|---|---|---:|---|
-| `build_start` | 命令 | — | 项目准备完成后开启,命令退出时闭合 |
-| `build_finished` | 命令 | — | 构建成功后开启,命令退出时闭合 |
-| `build_failed` | 命令 | — | 构建失败后开启,命令退出时闭合 |
-| `during_build` | 命令 | — | 构建开始前开启,构建结束后闭合 |
-| `timeout_seconds` | 整数,1–86400 | `10` | 单次运行的时限 |
-| `enabled` | 布尔 | `true` | 是否启用本表中的全部命令 |
-| `side_effect` | 布尔 | `false` | Hook 失败是否让本次构建失败。**保留键**——实验期内只接受 `false` |
+| `build_start` | 命令 | — | 项目准备完成后开启,命令退出时闭合 |
+| `build_finished` | 命令 | — | 构建成功后开启,命令退出时闭合 |
+| `build_failed` | 命令 | — | 构建失败后开启,命令退出时闭合 |
+| `during_build` | 命令 | — | 构建开始前开启,构建结束后闭合 |
+| `timeout_seconds` | 整数,1–86400 | `10` | 单次运行的时限 |
+| `enabled` | 布尔 | `true` | 是否启用这张表里的全部命令 |
+| `side_effect` | 布尔 | `false` | Hook 失败是否让本次构建失败。**保留键** —— 实验期内只接受 `false` |
-前三个区间是**自闭合**的——命令退出,区间就结束。"同步"在这里不是一种单独的模式,
-它就是自闭合区间的样子。`during_build` 是唯一由别的东西闭合的区间,而那两个只对其中
-一种形状有意义的键,是从这一点推出来的,不是额外规定的例外。
+前三个区间是**自闭合**的 —— 命令一退出,区间就结束。"同步"在这里不是一种
+单独的模式,它就是自闭合区间的样子。`during_build` 是唯一由别的东西闭合的
+区间,而那两个只对其中一种形状有意义的键,是从这一点推出来的,不是额外
+规定的例外。
-命令写成字符串;需要选项时写成表:
+一条命令写成字符串,需要选项时写成一张表:
| 表内键 | 适用于 | 含义 |
|---|---|---|
-| `cmd` | 所有事件 | 命令本身,必填 |
-| `timeout_seconds` | 自闭合事件 | 覆盖本表默认值 |
-| `loop` | `during_build` | 命令在区间闭合前退出时重新启动 |
+| `cmd` | 所有事件 | 命令本身,必填 |
+| `timeout_seconds` | 自闭合事件 | 覆盖这张表的默认值 |
+| `loop` | `during_build` | 命令在区间闭合之前退出时重新启动 |
-`loop` 写在自闭合事件上、`timeout_seconds` 写在 `during_build` 上,都是**错误**而不是
-被忽略的键:自闭合区间随命令退出而结束,没有东西可重启;而 `during_build` 已经由构建
-定界。一个被接受却什么都不做的键,读起来就是"这功能坏了"。
+`loop` 写在一个自闭合事件上、`timeout_seconds` 写在 `during_build` 上,
+都是**错误**,而不是被忽略的键:自闭合区间随它的命令退出而结束,没有东西
+可重启;而 `during_build` 已经由构建本身定界。一个被接受、却什么都不做的
+键,读起来就是"这功能坏了"。
-命令通过宿主 Shell(`/bin/sh` 或 `cmd.exe`)执行,工作目录是**项目根目录**——不是敲
-`mcpp build` 的那个目录,所以 Hook 里的相对路径在哪儿发起构建都指同一处。自闭合命令
-的标准输入、输出和错误沿用普通终端行为。没有配置的事件直接跳过。
+命令通过宿主 Shell(`/bin/sh` 或 `cmd.exe`)执行,工作目录是**项目根
+目录** —— 不是敲下 `mcpp build` 的那个目录,所以 Hook 里的相对路径,不论
+在哪里发起构建都指同一处。一个自闭合命令沿用普通终端的输入、输出。没有
+配置的事件直接跳过。
-生命周期为:
+生命周期为:
```text
-during_build 开启
+during_build opens
build_start
- ├─ 构建成功 → during_build 闭合 → build_finished
- └─ 构建失败 → during_build 闭合 → build_failed
+ ├─ build succeeds → during_build closes → build_finished
+ └─ build fails → during_build closes → build_failed
```
-`during_build` 在终止 Hook **之前**闭合,因此两条命令不会重叠执行。
+`during_build` 在终止 Hook **之前**闭合,因此这两条命令从不重叠执行。
-`build_failed` 与 `build_finished` 互斥,而且两者都只在 `build_start` 已经执行之后
-才可达。项目**准备**阶段就失败的情况——manifest 非法、依赖无法解析、没有可用工具链
-——一个 Hook 都不触发:此时构建尚未开始,而 Hook 程序本身可能正是准备阶段要装的东西。
+`build_failed` 与 `build_finished` 互斥,而且两者都只在 `build_start` 已经
+执行之后才可达。项目**准备**阶段就失败的情形 —— manifest 非法、依赖无法
+解析、没有可用工具链 —— 一个 Hook 都不触发:构建此时尚未开始,而 Hook
+程序本身可能正是准备阶段要装的那个东西。
-命令无法启动、返回非零或超过时限均视为 Hook 失败。`during_build` 还多一种:开了 `loop`
-的命令**起不来**——连续五次在一秒内以非零状态结束——就不再重启,并被报出来。(很快就
-成功结束的命令,正是 `loop` 被要求重复的那件事,不算失败。)以上每一种都以 **warning**
-报出,构建保留它自己挣来的结果——`[hooks]` 还在实验期,它没有投票权。Hook 自身失败不会
-再触发另一个 Hook。
+一个 Hook 命令无法启动、返回非零,或超过时限,都算作一次 Hook 失败。
+`during_build` 还多一种:一个开了 `loop` 的命令**起不来** —— 连续五次
+在一秒之内以非零状态结束 —— 就不再被重启,并被报出来。(一个很快就成功
+结束的命令,正是 `loop` 被要求重复的那件事,不算失败。)以上每一种都以
+**warning** 报出,构建保留它自己挣来的结果 ——`[hooks]` 还在实验期,它
+没有投票权。一个 Hook 自身的失败不会触发另一个 Hook。
-改变这一点的正是 `side_effect = true`,而今天写它是一个错误:
+改变这一点的正是 `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 处理媒体资源:
+是拒绝而不是悄悄降级,因为两种沉默的做法都更糟:采纳它,等于让一个实验性
+功能对每一次构建都有否决权;忽略它,则让项目误以为自己的构建被一个通知
+程序把着关,而实际上没有。功能转正之后,`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。
+- 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]
@@ -335,45 +360,47 @@ 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"]
+[xlings.workspace]
+"xim:mcpp-hooks-audioplayer" = "0.0.1"
```
-根据构建成功或失败播放不同提示音。`side_effect = false` 写出来而不是靠默认值:它是
-这份 manifest 自己就想要的值——缺个音频设备不该让构建失败——所以等这个键有了不止
-一个可接受的值之后,它仍然会这么写。
+构建成功或失败播放不同的提示音。`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) |
+| 一个包名、一个版本,或"没有候选" | 解析 | [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 的任何一段都没有
+参与,它的文档不会有帮助。
## 当前边界
- `mcpp why --format json` 只对 `toolchain` 话题有定义。其余话题报
`'' has no machine-readable shape yet` 并以非零退出。
-- `mcpp search` 按子串匹配;没有字段选择器,也没有把搜索限定到单个命名空间的方式。
-- `mcpp clean --stale` 读 `target/.build_cache`,它保存的近期条目数量有上限。一个工程
- 如果构建过的 (目标, profile) 组合多于这个上限,最旧的条目会被挤掉;条目被挤掉的目录
- 随后按未记录处理 —— 在 `--older-than` 之内保留,超出则删除。
+- `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,但这一种取值上不一致。
+ (expected {s,m,h,d})` 被拒绝,而 `mcpp clean --stale --older-than 0`
+ 接受同样的值。两个选项共用一个 parser,但这一种取值上不一致。
-`mcpp emit xpkg` 写出一个 `mcpp xpkg parse` 不认识的键。对一个自带 `mcpp.toml`
-的包,产出的 `mcpp` 段以 `manifest = "mcpp.toml"` 结尾,而描述符解析器把它报为
-未知键:
+`mcpp emit xpkg` 写出一个 `mcpp xpkg parse` 不认识的键。对一个自带
+`mcpp.toml` 的包,产出的 `mcpp` 段以 `manifest = "mcpp.toml"` 结尾,而
+描述符解析器把它报为未知键:
```
error: unknown mcpp-segment key 'manifest' — silently ignored at build time
@@ -382,11 +409,10 @@ 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。
+第二个错误由第一个导出:键被忽略,于是没有从它点名的那份 manifest 推导出
+任何源。手工补上 `sources = { … }` 只能消掉第二个,消不掉第一个,
+`mcpp xpkg parse` 仍然以 1 退出。
+`mcpp-index` 里没有任何描述符使用那个键 —— 218 个里 0 个。自带
+`mcpp.toml` 的包**整个省略 `mcpp` 字段**,由 mcpp 在版本目录下查找那份
+manifest。实测于 2026.9.8.1。
diff --git a/docs/zh/10-pack-and-release.md b/docs/zh/10-pack-and-release.md
index b7e22d8d6..f02a56cbe 100644
--- a/docs/zh/10-pack-and-release.md
+++ b/docs/zh/10-pack-and-release.md
@@ -1,156 +1,181 @@
# 10 —— 发布打包
-**读者:**要把一个程序交付到没有 mcpp 的机器上的人。
+**读者:** 要把一个程序交付到没有 mcpp 的机器上的人。
-**本章回答的那一个问题:**怎样把一次构建变成另一台机器能运行的东西,以及每种打包
-模式各自携带什么。
+**本章回答的那一个问题:** 一次构建怎样变成另一台机器能运行的东西,以及每一种
+打包模式各自携带什么。
-**不在这里:**交付一个供其它包构建时使用的**库**,那是
-[12 —— 分发预编译库](12-binary-distribution.md)。在此之后:
+**不在这里:** 交付一个供其它包在构建时使用的**库**,那是
+[12 —— 分发预构建库](12-binary-distribution.md)。在此之后:
[11 —— 发布一个库到 mcpp-index](11-publishing-a-library.md)。
-> 默认的动态链接 `mcpp build` 产物会把 loader 与 RUNPATH 指向构建沙盒。它是
-> 开发产物,不是交付物。有三条路把它变成交付物 —— **没有一条使用宿主的 C 库**。
+> 本页讲的是打包一个**程序**。若要以「接口 + 预构建二进制」的形式发布一个*库*,
+> 见 [12 —— 分发预构建库](12-binary-distribution.md)。
+>
+> `mcpp pack` 具体做哪一种,由 target 的 `kind` 决定,不由某个 flag 决定:
+> `mcpp pack ` 打包 `[targets.]`,一个 `bin` 会变成一个 bundle,
+> 而一个 `lib`/`shared` 会变成一个库包。不给名字时,mcpp 选择唯一可打包的
+> target。
+
+> `mcpp build` 默认产出的动态链接二进制,其 loader 与 RUNPATH 都指向构建
+> 沙盒。它是开发产物,不是交付物。有三条路径能把它变成交付物 —— 没有一条会
+> 用到宿主的 C 库。
## 三种分发路径
-下面每一条产出的产物,其 C 运行时都来自生态,而不是 `/lib64`。这是有意的:
-mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决于底下是哪个发行版;
-如果最后一步又伸手去拿宿主的 libc,前面这件事就白做了。
+下面每一种路径产出的产物,其 C 运行时都来自生态,从不来自 `/lib64`。这是有意
+安排的:mcpp 之所以针对私有 glibc 构建,正是为了让产物的行为不取决于底层是
+哪一个发行版;如果分发的最后一步又伸手去用宿主的 libc,前面这一步做的事就白
+费了。
-| | 方式 | 命令 | C 运行时的来源 | 适用条件 |
+| | 路径 | 命令 | C 运行时的来源 | 适用条件 |
|---|---|---|---|---|
-| **A** | 走生态 | `mcpp emit xpkg` → `xlings install ` | 目标机自己的 xlings 载荷 | 目标机装了 xlings |
-| **B** | 静态单文件 | `mcpp build --target x86_64-linux-musl` | 不来自任何地方 —— 已链进去 | 想要一个无任何运行时依赖的单文件 |
-| **C** | 自带运行时 | `mcpp pack --mode self-contained` | 随 bundle 一起分发 | 任何 Linux,含比构建机更老的 |
+| **A** | 走生态 | `mcpp emit xpkg` → `xlings install ` | 目标机自己的 xlings 载荷 | 目标机已装 xlings |
+| **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 专有行为
-(NSS 查询、`dlopen` 宿主插件)时应当首选。
+**关于路径 B。** `--target …-musl` 隐含静态链接,因此没有 loader、没有
+RUNPATH,运行期不需要查找任何东西。它的结果最小也最可移植,是程序不需要
+glibc 专有行为(NSS 查询、`dlopen` 宿主插件)时的首选。
-**关于 C。** bundle 里带着这套工具链的 glibc 与 loader,因此能在比构建机更老的
-发行版上跑 —— 这是 B 覆盖不了、而又确实需要 glibc 时的那一格。选它之前先读下面
-的 `/proc/self/exe` 一节:经 bundled loader 启动会改变程序对「自己在哪」的认知。
+**关于路径 C。** bundle 内携带这套工具链的 glibc 及其 loader,因此能在比构建机
+更旧的发行版上运行 —— 这是路径 B 覆盖不到、而又确实需要 glibc 时的那一格。选
+它之前先读下面 `/proc/self/exe` 一节:经由内置 loader 启动会改变程序对「自己
+在哪」的认知。
-## 两条轴:target(libc) × mode(打包深度)
+## 两条轴:target(libc)× mode(打包深度)
-发布有两项正交选择:
+分发有两项正交的选择:
-- **libc / static** 是*构建 target* 属性:`--target …-linux-gnu`(glibc) 与
- `--target …-linux-musl`(musl,static)。`--target …-musl` 隐含 `static`。
-- **打包深度** 是*pack* 属性:产物携带多少共享库闭包,由 `--mode` 选择。
+- **libc / static** 是一项*构建 target* 属性:`--target …-linux-gnu`(glibc)
+ 与 `--target …-linux-musl`(musl,静态)。`--target …-musl` 隐含 `static`。
+- **打包深度** 是一项*pack* 属性:产物随身携带多少共享库闭包,由 `--mode`
+ 选择。
-| 模式 | 宿主必须提供 | 体积 | 使用场景 |
+| Mode | 宿主必须提供 | 体积 | 使用场景 |
|---|---|---|---|
-| `system` | 所有 `.so`(含第三方) | 最小 | `.deb`/`.rpm`,同发行版集群(包管理器声明依赖) |
-| `vendored`(默认) | libc / libstdc++ / loader | +几 MB | 主流发行版(Ubuntu 22+,Debian 12+,RHEL 9+) |
-| `self-contained` | 无 | +30–50 MB | 任意 Linux(含旧 glibc);携带闭包与 `run.sh` wrapper |
-| `static` | 无(单文件) | +5–10 MB | musl;匹配架构的 Linux x86_64 或 aarch64,Docker scratch,Alpine |
+| `system` | 每一个 `.so`(含第三方) | 最小 | `.deb`/`.rpm`,同发行版机群(由包管理器声明依赖) |
+| `vendored`(默认) | libc / libstdc++ / loader | +几 MB | 主流发行版(Ubuntu 22+、Debian 12+、RHEL 9+) |
+| `self-contained` | 无 | +30–50 MB | 任意 Linux,含较旧的 glibc;携带闭包与 `run.sh` wrapper |
+| `static` | 无(单文件) | +5–10 MB | musl;架构匹配的 Linux x86_64 或 aarch64,Docker scratch,Alpine |
+
+如何选择:
-选择建议:
+- 分发格式打包(`.deb`/`.rpm`)或同发行版内部部署 → `system`
+- 面向主流发行版的桌面 / 服务端发布 → `vendored`(默认)
+- 跨发行版,或目标是较旧的 glibc(旧版 CentOS、麒麟等) → `self-contained`
+- 单个便携文件、不依赖宿主 → `static`
-- `.deb`/`.rpm` 或同发行版内部部署 → `system`
-- 桌面或服务端发布,目标为主流 Linux 发行版 → `vendored`(默认)
-- 需兼容老旧 CentOS、麒麟等 glibc 版本较低的环境 → `self-contained`
-- 单个便携文件、无宿主依赖 → `static`
+**没有任何 mode 会携带构建机的路径。** 开发构建有意寻址的是本机:它的
+`DT_RPATH` 命名工具链的载荷目录与 SubOS 的库视图(`/lib`),它的
+`PT_INTERP` 命名一个私有 loader。每一种 mode 都改写这两者 —— `vendored` 与
+`self-contained` 改写为相对 `$ORIGIN` 的路径,`system` 则整个清空搜索路径并
+恢复平台标准的 interpreter。`system` 不是「原样保留构建时的样子」,而是「目标
+机会提供一切」这一断言 —— 这是关于目标机的陈述,不可能用本机的绝对路径拼出
+来。e2e 215 会扫描 bundle 里每一个 ELF,查找任何落在 `$MCPP_HOME` 之下的路径,
+命中即失败。
-### 需要宿主提供能力的程序
+### 需要宿主提供某种能力的程序
-「自包含」有一个下限。有些库只能来自目标机器:图形驱动的用户态部分与正在运行的
-内核模块版本绑定,而对专有栈而言,再分发是不被允许的。这类依赖应声明为运行期能力
-需求(`docs/zh/04-mcpp-toml.md` §2.11),模式表随之多出一列:
+「自包含」有一个下限。有些库只能来自目标机器:图形驱动的用户态部分与正在
+运行的内核模块版本绑定,而对专有栈而言,再分发是不被允许的。这类依赖应声明为
+运行期能力需求(`docs/04-mcpp-toml.md` §2.11),模式表因此多出一列:
| Mode | 需要宿主提供能力的程序 |
|---|---|
| `system` | yes |
-| `vendored`(默认) | **这类程序的正确默认值** |
+| `vendored`(默认) | **对这类程序而言正确的默认值** |
| `self-contained` | **打包期拒绝** |
| `static` | **打包期拒绝** |
-两处拒绝出自同一个事实:**自带 libc 的 bundle 无法消费宿主提供的库。** 那个 `.so`
-带着它对**目标机 libc** 的要求到达,而该进程没有那份 libc —— 双向实测记录于
-mcpp#392 / mcpp#401:私有 glibc 遇上宿主加载的对象,会在重定位阶段、`main` 之前
-崩溃。此前这两种模式都是链接通过、启动时失败,或静默降级(图形栈的表现是软件
-渲染,而没有任何提示)。
+两处拒绝出自同一个事实:**自带 libc 的 bundle 无法消费宿主提供的库。** 那个
+`.so` 带着它对*目标机* libc 的要求到达,而该进程没有那份 libc —— 双向实测记录
+于 mcpp#392 / mcpp#401:私有 glibc 遇上宿主加载的对象,会在重定位阶段、
+`main` 之前崩溃。此前这两种 mode 的行为要么是链接通过、启动时失败,要么是
+静默降级(就图形栈而言表现为软件渲染,且没有任何提示)。
-`vendored` 会打包这类程序,并在 bundle 根目录写出一个 **`HOST-REQUIREMENTS`**
-文件,说明目标机必须提供什么:
+`vendored` 会打包这类程序,并在 bundle 根目录写出一个 **`HOST-REQUIREMENTS`**
+文件,说明目标机必须提供什么:
```
capability=opengl.glx.driver discovery=rpath-of-dispatch
```
-`discovery` 是可行动的那一半 —— 各机制彼此独立,满足其一并不等于满足另一个。
-该文件仅在确有内容时写出:一个空文件等于**声称**什么都不需要。
+`discovery` 是可操作的那一半 —— 各机制彼此独立,满足其一不代表满足另一个。该
+文件只在确有内容需要说明时才写出:一个空文件等于**声称**什么都不需要。
-### 模式名兼容性
+### 模式名的兼容性
-上表是规范名称。旧名称仍是**永久兼容别名**:`bundle-project` = `vendored`,
-`bundle-all` = `self-contained`。tarball 后缀是被冻结的 wire 格式(由
-`install.sh` 消费),不会跟随名称改变:`vendored` 无后缀,`self-contained` 是
-`-bundle-all`,`static` 是 `-static`,`system` 是 `-system`。
+上表给出规范名称。旧名称仍是**永久性的兼容别名**:`bundle-project` =
+`vendored`,`bundle-all` = `self-contained`。tarball 的文件名后缀是被冻结的
+wire 格式(由 `install.sh` 消费),**不**跟随改名:`vendored` 无后缀,
+`self-contained` 是 `-bundle-all`,`static` 是 `-static`,`system` 是
+`-system`。
## 命令
```bash
-mcpp pack # 默认 vendored
+mcpp pack # vendored by default
mcpp pack --mode system
mcpp pack --mode static
-mcpp pack --mode self-contained # 别名:--mode bundle-all
-mcpp pack --target x86_64-linux-musl # 等价 --mode static
-mcpp pack --target aarch64-linux-musl # ARM64 等价写法
-mcpp pack --format dir # 输出为目录,不打包 tarball
-mcpp pack --format appimage # 由图里某个包提供的格式
-mcpp pack -o myapp.tar.gz # 仅文件名:落到 target/dist/myapp.tar.gz
-mcpp pack -o /abs/path/myapp.tar.gz # 含目录:按字面路径输出
-mcpp pack --profile dev # 换一个 profile 构建(默认 release)
-mcpp pack --dev # 同上,拼法与 build、run 一致;--profile 优先于它
-mcpp pack --message-format json # 在 stdout 上输出一个 mcpp.pack 信封(mcpp 2026.9.16.1+)
-mcpp pack --no-strip # 按构建原样发货,不剥符号
-mcpp pack --debug-symbols dbg/ # 把分离出的 *.debug 写到 dbg/
-mcpp pack --format msi --features installer # 为这次打包启用根包 feature
+mcpp pack --mode self-contained # alias: --mode bundle-all
+mcpp pack --target x86_64-linux-musl # equivalent to --mode static
+mcpp pack --target aarch64-linux-musl # ARM64 equivalent
+mcpp pack --format dir # output as a directory, no tarball
+mcpp pack --format appimage # a format a package in the graph provides
+mcpp pack -o myapp.tar.gz # filename only: lands at target/dist/myapp.tar.gz
+mcpp pack -o /abs/path/myapp.tar.gz # includes a directory: output to the literal path
+mcpp pack --profile dev # build with a different profile (default: release)
+mcpp pack --dev # the same, as `build` and `run` spell it; --profile wins over it
+mcpp pack --message-format json # one mcpp.pack envelope on stdout (mcpp 2026.9.16.1+)
+mcpp pack --no-strip # ship the artifacts as built
+mcpp pack --debug-symbols dbg/ # write the separated *.debug files under dbg/
+mcpp pack --format msi --features installer # activate root-package features for the pack
```
-`--features `(mcpp 2026.9.15.2+)为打包执行的每一次构建启用根包 feature:每条
-`--target` 腿,以及分派格式的两次构建。它与 `mcpp build --features` 接受同样的取值,
-因此只为某一种发布才需要的主机工具,可以声明在带 `tools = [...]` 的
-`[feature-deps.]` 下,只由点名 `` 的那次打包构建。
-`mcpp run --format --features ` 把同样的 feature 交给它执行的那次打包。
+`--features `(mcpp 2026.9.15.2+)为打包所执行的每一次构建启用根包
+feature:每一条 `--target` 腿,以及被分派格式的两遍构建。它接受的取值与
+`mcpp build --features` 相同,因此只为某一种发布才需要的宿主工具,可以声明在
+带 `tools = [...]` 的 `[feature-deps.]` 下,只由点名 `` 的那次打包构建
+出来。`mcpp run --format --features ` 把同样的 feature 交给它
+执行的那次打包。
-`--release` 与 `--dev`(mcpp 2026.9.16.1+)是 `build`、`run` 所接受的简写,优先级相同:
-三条命令上都是 `--profile` 优先于它们。
+`--release` 与 `--dev`(mcpp 2026.9.16.1+)是 `build`、`run` 已接受的简写,
+优先级相同:三条命令上都是 `--profile` 优先于它们。
-`--message-format json`(mcpp 2026.9.16.1+)在命令结束后于 stdout 上输出一个
-`mcpp.pack` 信封,所有给人读的行都走 stderr。其 `data.artifacts` 列出产出的每个文件或
-目录:绝对路径、`type`(`file` 或 `directory`)、`--format` 取值以及各条腿的三元组;
-`data.stage` 给出暂存树、它的 manifest 以及闭包是否走通
-([50 —— 机器输出](50-machine-output.md))。这条命令上的 `--format` 表示包格式,因此
-机器输出按 `mcpp test` 的方式请求。
+`--message-format json`(mcpp 2026.9.16.1+)在命令结束后于 stdout 上输出一个
+`mcpp.pack` 信封,所有给人看的行都改走 stderr。它的 `data.artifacts` 列出
+产出的每一个文件或目录,带绝对路径、`type`(`file` 或 `directory`)、
+`--format` 取值以及各条腿的三元组;`data.stage` 给出暂存树、它的 manifest,
+以及闭包是否已经走通(见 [50 —— 机器输出](50-machine-output.md))。这条命令
+上的 `--format` 表示的是包格式,因此机器输出改用 `mcpp test` 请求它的那种
+方式。
-### `--format` 是一个轴,引擎只拥有其中两个取值
+### `--format` 只是一根轴,引擎只拥有它的两个取值
-`tar` 与 `dir` 回答的问题,和 `msi` 与 `appimage` 回答的问题是同一个 —— 输出取什么
-形状 —— 所以它们是一个 flag 的取值,而不是第二个 flag 的开端。引擎持有什么、包持有
-什么,分界是:
+`tar` 与 `dir` 回答的问题,和 `msi` 与 `appimage` 回答的问题是同一个 ——
+输出取什么形状 —— 所以它们是同一个 flag 的不同取值,而不是第二个 flag 的开端。
+引擎持有什么、包持有什么,分界如下:
-> **`mcpp pack` 拥有机制,以及那一种通用格式。其余每一种格式都住在包里,由
-> `mcpp pack` 分派过去。**
+> **`mcpp pack` 拥有机制,以及那一种通用格式。其余每一种格式都住在某个包里,
+> 由 `mcpp pack` 分派过去。**
-那种通用格式就是它已经在产出的东西:一个解开就能跑的归档。它「通用」只在这里唯一
-要紧的那个意义上 —— 它不需要知道任何别人的发布。此外的一切都需要。dpkg 的 control
-字段、AppImage 的 runtime、WiX 的 schema、Apple 的公证、Android 的签名方案:其中任何
-一个被绑进引擎,都会把一次 mcpp 的发布耦合到一次 mcpp 并不控制的发布上。这与本项目
-早已为语言做过的论证是同一个 —— Slang 被支持,而引擎里没有它的名字。
+那种通用格式就是它已经在产出的东西:一个解开即可运行的归档。它「通用」只在这
+里唯一要紧的那个意义上成立 —— 它不需要了解任何别人的发布方式。此外的一切都
+需要。dpkg 的 control 字段、AppImage 的 runtime、WiX 的 schema、Apple 的
+公证、Android 的签名方案:其中任何一个被绑进引擎,都会把一次 mcpp 的发布耦合
+到一次 mcpp 并不控制的发布上。这与本项目已经为语言做过的论证是同一个 ——
+Slang 被支持,而引擎里没有它的名字。
-所以取值集合是开放的(mcpp 2026.9.11.1+)。`--format ` 在解析后的图里找到声明
-了 `` 的那个包,并把暂存树交给它;一个未知的取值会点名**当下确实可用**的那些,
-而不是一份固定清单:
+所以取值集合是开放的(mcpp 2026.9.11.1+)。`--format ` 在解析后的图里
+找到声明了 `` 的那个包,并把暂存树交给它;一个未知的取值点名的是
+**当下确实可用**的那些,而不是一份固定清单:
```
error: unknown --format 'bogus'.
@@ -160,377 +185,391 @@ error: unknown --format 'bogus'.
that provides 'bogus' to [build-dependencies] and activate its feature.
```
-这次拒绝发生在任何东西被编译之前。怎么写这样一个包,见
-[产出可分发物](30-build-mcpp.md#产出可分发物pack_format-与-stage_dir20269111);
-引擎加的三样东西是:一棵 `artifact` action 可以消费的暂存树、`[package]` 的其余字段
-进入构建程序、以及这次分派本身。每一样都与格式无关 —— 而「与格式无关」正是判断某样
-东西该不该进引擎的判据。
+这次拒绝发生在任何东西被编译之前。怎样写这样一个包,见
+[产出可分发物](30-build-mcpp.md#产出可分发物pack_format-与-stage_dir20269111);
+引擎为此新增的三样东西是:一棵 `artifact` action 可以消费的暂存树、
+`[package]` 里其余字段进入构建程序、以及这次分派本身。每一样都与具体格式无关
+—— 「与格式无关」正是判断某样东西该不该进引擎的判据。
-被分派的格式作用于一个**程序** target——`kind = "app"` 也不例外,不论这一行把它
-链接成什么文件(见[04 §2.2](04-mcpp-toml.md))。库包发的是一份接口加上每个三元组
-的预构建产物,没有单独一棵暂存树,所以 `mcpp pack <库> --format ` 会被拒绝,
-而不是被忽略。
+被分派的格式作用于一个**程序** target —— `kind = "app"` 也不例外,无论这一行
+把它链接成什么文件(见 [04 §2.2](04-mcpp-toml.md))。库包发布的是一份接口
+加上按三元组给出的预构建产物,没有单独一棵暂存树,所以
+`mcpp pack <库> --format ` 会被拒绝,而不是被忽略。
**产物是共享目标文件的 `kind = "app"` target 可以接受一个以上的 `--target`**
-(mcpp 2026.9.13.2+):在每一行 Android 上,一个应用*就是*平台加载的那个共享库,
-所以 `mcpp pack myapp --target aarch64-linux-android --target x86_64-linux-android`
-会构建并把两条腿暂存进同一棵树里,与库包的多三元组做法完全一致。每条腿落在
-`lib//lib.so`(`aarch64` → `arm64-v8a`,`x86_64` → `x86_64`),各自的
-闭包暂存在它旁边(见 [Android](#android应用目标文件与它的闭包在-lib-下)),声明的
-部署文件只暂存一次,随后对这棵合并后的树只跑一次分派——这正是 `dist-apk` 这样的
-成员能构建出一个通用 APK 的原因。只给一个 `--target` 时,今天这种扁平的
-`lib/lib.so` 布局保持不变。产物在任何被请求的一行上是可执行文件的 target,
-第二个 `--target` 依旧被拒绝:为多个三元组打包一个可执行文件需要多个可执行文件,
-那是另一种机制(`lipo` 的通用二进制),不在此列。
-
-`-o` 接受裸文件名时自动归到 `target/dist/`;含目录(相对或绝对)
-时按字面路径输出。
+(mcpp 2026.9.13.2+):在每一行 Android 上,应用*就是*平台加载的那个共享库,
+因此 `mcpp pack myapp --target aarch64-linux-android --target x86_64-linux-android`
+会构建两条腿并把它们暂存进同一棵树,与库包本已具备的多三元组做法完全一致。
+每条腿落在 `lib//lib.so`(`aarch64` → `arm64-v8a`,`x86_64` →
+`x86_64`),各自的闭包暂存在它旁边(见
+[Android](#android应用目标文件与它的闭包在-lib-下)),声明的部署文件只暂存
+一次,随后只对这棵合并后的树跑一次分派 —— 这正是 `dist-apk` 这样的成员能构建
+出一个通用 APK 的原因。只给一个 `--target` 时,今天这种扁平的
+`lib/lib.so` 布局保持不变。产物在任何被请求的一行上是可执行文件的
+target,第二个 `--target` 依旧被拒绝:为多个三元组打包同一个可执行文件需要
+多个可执行文件,那是另一种机制(`lipo` 的通用二进制),不在这份能力之内。
+
+`-o` 接受裸文件名时,输出落在 `target/dist/` 下;含目录(相对或绝对)时,按
+字面路径输出。
完整选项参见 `mcpp pack --help`。
-### `mcpp run --format `(mcpp 2026.9.12.3+)
+### `mcpp run --format `(mcpp 2026.9.12.3+)
```bash
mcpp run --target x86_64-linux-android --format apk
mcpp run --target aarch64-ios-sim --format app
```
-`--format` 与 `mcpp pack` 用的是同一个 flag,这里复用它来覆盖一种普通 `mcpp run`
-够不到的情形:一个 Android 应用程序是一个 `.apk`,一个已安装的 iOS 应用程序是一个
-`.app`,两者都不是 `mcpp run` 默认执行的链接产物。`mcpp run --format ` 先为
-`` 打包——与 `mcpp pack --format ` 相同的两遍与暂存树——再运行打包
-报出的那个产物,经由为一个程序解析出的 runner:项目的 `[target.] runner`,
-其次依赖的 `mcpp::runner(...)`,再次载荷描述文件的。
-
-打包报出的产物是这次请求的**终端**产物:在请求引入的 `artifact` 动作中,没有被其他
-引入动作当作输入的那个输出。提供者常常是一条链(`dist-apk`:链接、加库、对齐、签名),
-链上每个输出都会被核验存在,但只有最后一个是发布物,也只有它以 `Packed` 报出。链的
-末端有两个文件的格式会被 `mcpp run --format` 拒绝并点名两者,因为 runner 只接受一个
-操作数。
-
-未知的 `