Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions .agents/skills/mcpp-docs-style/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
name: mcpp-docs-style
description: Use when writing or editing anything under docs/ (English or 简体中文), README files, or long-form design records — states the register mcpp documentation is written in (declarative, precise, professional), the constructions that are not admitted (question headings, conversational asides, internet slang, figurative jargon), and the bilingual parity rules.
---

# mcpp 文档风格规范

## 适用范围

`docs/**`(含 `docs/zh/**`)、`README.md`、`.agents/docs/**` 的对外部分。

代码注释与 commit message **不受本规范约束** —— 它们的读者、篇幅与目的都不同,
那里允许并鼓励叙述「为什么」以及实测过程。本规范约束的是**面向用户的文档**。

## 一、总原则

文档是**参考资料**,不是博客,也不是聊天记录。判据只有一条:

> 一位不认识作者、只想解决自己问题的工程师,能不能在最短时间内
> 拿到准确的事实,并且不会误以为某个说法比实际更随意或更绝对。

由此得到三条可执行的规则:陈述、精确、克制。

## 二、标题

**标题一律是名词短语或陈述句,不使用疑问句、不使用口语片段。**

疑问句标题把「读者已经知道自己在找什么」这个前提丢掉了 —— 目录里一列问句,
读者要先把每个问句翻译成主题才能定位。

| 不采用 | 采用 |
|---|---|
| 一段话讲完 | 概述 |
| 打什么由谁决定 | 打包内容的决定依据 |
| 哪些 `.cppm` 会被发布 | 发布的接口单元 |
| 消费者的构建会检查什么 | 消费端的构建检查 |
| 怎么消费 | 消费方式 |
| 老版本 mcpp 拿到这种包会怎样 | 旧版本 mcpp 的行为 |
| 为什么两者都不许裁剪 | 两个集合不可裁剪的原因 |
| 这些说法验证到哪一步、在哪台机器上 | 验证范围 |
| The whole idea in one paragraph | Overview |
| What decides what gets packed | What determines the package contents |
| Consuming one | Consuming a package |
| What you may rely on, and what changes | Stability guarantees |

「…的原因」「…的依据」「…的范围」是把 why 型标题转成名词短语的常用形。
**保留 why 本身,去掉疑问语气。**

## 三、词汇

### 不采用的类别

1. **网络用语与口语**:搞定、干活、坑、真香、翻车、打脸、一把梭、白给、
凉了、炸了、神器、黑科技、敲黑板、划重点。
2. **拟人与比喻性行话**:姊妹篇、腿(fat package 的一份产物)、travel(源码
「旅行」)、picky、happy path 的中文直译。技术术语本身可以是比喻
(rpath、sysroot),但**不要新造比喻**。
3. **填充语**:其实、说白了、简单来说、众所周知、显然、当然、值得一提的是。
如果一件事显然,就不必说;如果不显然,「显然」会让读者怀疑自己。
4. **含糊的程度词**:很快、非常、极其、基本上、差不多。用数字或范围替代 ——
「2.42×」「64.77s」「四个平台中的三个」。

### 人称

默认**不使用第二人称**。写动作的对象,不写「你」。

- 不采用:你可以在 `mcpp.toml` 里写 …
- 采用:在 `mcpp.toml` 中声明 …

例外:**教程体**文档可以使用第二人称,因为那里读者正在跟着做。教程体是
**列出来的,不是推断的**:`00-getting-started.md`、`01-examples.md`、
`04-build-from-source.md`。其余全部按参考文档处理。

引用 mcpp 自身输出的部分不受此限:`did you mean 'x86_64-linux-musl'?` 与
`your toolchain : …` 是程序打印的原文,**逐字复现是要求,不是文风问题**。
检查脚本因此会先剔除行内代码段再判定。

## 四、句式

- **陈述句优先。** 命令式仅用于操作步骤(「运行 `mcpp build`」)。
- **一句话一个事实。** 从句套从句的长句拆开。
- **不使用反问。**「难道不应该……吗?」没有信息量。
- **不使用感叹号。**
- **破折号克制使用**:插入语用逗号或括号;破折号留给「随后是对前半句的
重述或收束」这一种用法。

## 五、断言的强度必须与证据相符

这是本规范里最实质的一条,也是最容易违反的一条。

| 证据 | 允许的表述 |
|---|---|
| 跑过、有输出 | 「实测」「测量得到」,并给出数字或报错原文 |
| 读代码推断 | 「按 X 的实现」「由 Y 决定」 |
| 未验证 | 「未验证」「尚无测试覆盖」—— **必须写出来** |

**不要把推断写成实测。** 反例(本仓库真实发生过):把「守卫在原生构建上失效」
写成实测结论,而它是从「`targetTriple` 结构上可能为空」推断的;实际运行时
它非空,结论不成立。判据:**「结构上可能」不等于「运行时确实」——
要么读运行时产物,要么不要写成实测。**

同理,不要用「完全」「永远」「所有平台」这类全称词,除非确实逐个验证过;
写「已在 Linux / macOS / Windows 验证」比写「全平台可用」更有价值,
因为前者可被检验。

## 六、双语对照

`docs/X.md` 与 `docs/zh/X.md` 是**同一份文档的两个版本**,不是两篇文章。

- 章节结构、标题层级、表格行数必须一一对应;
- 代码块、命令、报错原文**逐字相同**,不翻译;
- 术语表统一:module interface unit / 模块接口单元、implementation partition /
实现分区、import library / 导入库、install name / install name(不译)。
- 改动一侧时**同时改另一侧**。只改一侧会让两份文档随时间分叉,
而读者无从知道哪一份是新的。

## 七、结构

- 顶部一段引言说明**这份文档回答什么问题**,以及相关文档的链接
(用「相关文档:」,不用「姊妹篇」)。
- 表格用于枚举与对照,散文用于因果。**不要用散文列举**。
- 「当前边界 / Current limitations」一节是必要的,不是可选的:
没有写出边界的文档,读者只能靠踩到才知道。

## 八、机器检查

规则里可判定的那一半由 `.github/tools/check_docs_style.sh` 执行:

```
bash .github/tools/check_docs_style.sh
```

它检查三条:标题不是疑问句/口语片段;参考文档不使用第二人称;
`docs/X.md` 与 `docs/zh/X.md` 的标题结构一致(按层级序列比对,
并剔除代码块内的 `#` 注释 —— 第一版脚本把 ```sh 块里的 `# GET, never HEAD`
数成了标题,报出一个并不存在的结构分歧)。

**它不检查第五节** —— 断言强度与证据是否相符需要读者判断,而那是本规范里
最重要的一条。脚本能做的事不等于规范的全部。

## 九、自检清单

提交文档改动前:

```
[ ] 标题没有疑问句、没有口语片段
[ ] 没有网络用语、没有新造比喻
[ ] 没有第二人称(教程体除外)
[ ] 每条「实测」都有数字、路径或报错原文
[ ] 没有未经验证的全称断言
[ ] 中英两版结构对应,代码块逐字一致
[ ] 有「当前边界」一节
[ ] `bash .github/tools/check_docs_style.sh` 通过
```
100 changes: 100 additions & 0 deletions .github/tools/check_docs_style.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# check_docs_style.sh — the mechanically checkable half of .agents/skills/mcpp-docs-style.
#
# A style rule nobody can check is a suggestion. This covers the three rules
# that are decidable from the text alone:
#
# 1. headings are not questions and not conversational fragments
# 2. reference docs do not address the reader in the second person
# (tutorials do — they are listed below, not inferred)
# 3. docs/X.md and docs/zh/X.md have the same heading structure
#
# What it deliberately does NOT check: whether a claim's strength matches its
# evidence. That is the most important rule in the skill and it needs a reader.
#
# Usage: bash .github/tools/check_docs_style.sh
set -uo pipefail
cd "$(dirname "$0")/../.." || exit 1

fail=0
bad() { echo "FAIL: $*"; fail=1; }

# Tutorials address the reader on purpose: the reader is following along.
TUTORIALS="00-getting-started.md 01-examples.md 04-build-from-source.md"

# Headings outside fenced code blocks. `# …` inside a ```sh block is a shell
# comment, and counting it made the first version of this script report a
# parity gap in docs/10 that did not exist.
headings() {
awk '
/^```/ { infence = !infence; next }
!infence && /^#{1,6} / { print }
' "$1"
}

for f in docs/*.md docs/zh/*.md; do
base="$(basename "$f")"

# ── 1. heading register ───────────────────────────────────────────────
while IFS= read -r h; do
case "$h" in
*"?"*|*"吗"*|*"呢"*)
bad "$f: question heading — use a noun phrase: $h" ;;
esac
case "$h" in
*"一段话"*|*"讲完"*|*"姊妹篇"*|*"干活"*|*"怎么"*|*"会怎样"*|*"不许"*)
bad "$f: conversational heading: $h" ;;
*"The whole idea"*|*"in one paragraph"*|*"Consuming one"*|*"the thing that"*)
bad "$f: conversational heading: $h" ;;
esac
done < <(headings "$f")

# ── 2. second person in reference docs ────────────────────────────────
case " $TUTORIALS " in
*" $base "*) ;;
*)
# Prose only: quoted program output keeps its own wording ("your
# toolchain : …" comes out of mcpp and must be reproduced verbatim), so
# fenced blocks and lines that are clearly transcript are skipped.
hits=$(awk '
/^```/ { infence = !infence; next }
infence { next }
{
# Inline code spans are quoted material — mcpp prints
# `did you mean ...?` and `your toolchain : ...`, and reproducing
# those verbatim is required, not a style lapse. Blank them before
# matching rather than exempting whole lines, so prose on the same
# line is still checked.
line = $0
gsub(/`[^`]*`/, "", line)
if (line ~ /\<you\>|\<your\>|\<yours\>/ || line ~ /你/)
print FILENAME ":" FNR ": " $0
}
' "$f")
if [[ -n "$hits" ]]; then
while IFS= read -r line; do
bad "$f: second person in a reference doc: ${line#*: }"
done <<< "$hits"
fi ;;
esac
done

# ── 3. bilingual heading parity ───────────────────────────────────────────
for f in docs/*.md; do
z="docs/zh/$(basename "$f")"
[[ -f "$z" ]] || continue
# The LEVEL SEQUENCE, not just the count: two documents can have the same
# number of headings and still disagree about which are sections and which
# are subsections. Comparing counts would call that identical.
levels() { headings "$1" | sed -E 's/^(#+).*/\1/' | awk '{print length($0)}'; }
if ! diff -q <(levels "$f") <(levels "$z") >/dev/null; then
ne=$(headings "$f" | wc -l); nz=$(headings "$z" | wc -l)
bad "$(basename "$f"): heading structure differs (en=$ne zh=$nz headings); first divergence:"
diff <(levels "$f") <(levels "$z") | head -4 | sed 's/^/ /'
fi
done

if [[ "$fail" -eq 0 ]]; then
echo "OK: docs style checks pass"
fi
exit "$fail"
6 changes: 6 additions & 0 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ jobs:
- name: Check version / xlings pin consistency
run: bash .github/tools/check_version_pins.sh

# Same placement and the same reason: pure text, no toolchain, and the
# drift it catches — a 简体中文 page that has silently fallen behind its
# English original — is invisible to every other job.
- name: Check documentation style and bilingual parity
run: bash .github/tools/check_docs_style.sh

- uses: ./.github/actions/bootstrap-mcpp

- name: Configure mirror + Build mcpp from source (self-host)
Expand Down
24 changes: 12 additions & 12 deletions docs/02-pack-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
> deliverable. Three routes turn it into one — and none of them uses the host's
> C library.

## Three ways to ship
## Three distribution routes

Every route below produces an artifact whose C runtime comes from the
ecosystem, never from `/lib64`. That is deliberate: mcpp builds against a
Expand All @@ -24,16 +24,16 @@ libc to distribute would give that away at the last step.
| | Route | Command | Where its C runtime comes from | Choose it when |
|---|---|---|---|---|
| **A** | Through the ecosystem | `mcpp emit xpkg` → `xlings install <pkg>` | the target machine's own xlings payloads | the target has xlings |
| **B** | One static file | `mcpp build --target x86_64-linux-musl` | nowhere — it is linked in | you want a single file with no runtime at all |
| **B** | One static file | `mcpp build --target x86_64-linux-musl` | nowhere — it is linked in | a single file with no runtime dependency |
| **C** | Carry the runtime | `mcpp pack --mode self-contained` | shipped inside the bundle | any Linux, including older than the build machine |

**On route A, and the thing that surprises people.** The `PT_INTERP` baked into
a freshly built binary points at *your* machine's payload, so copying that file
to another machine by hand does not work — the path is not there. That is not a
property of the artifact so much as of the copy: installed through `xlings`, the
package's ELF files are repointed at the target machine's own payloads at
install time. The baked path is a build-machine detail, not a distribution
format. If you are hand-copying binaries between machines, you want B or C.
**On route A.** The `PT_INTERP` recorded in a freshly built binary points at the
build machine's payload, so copying that file to another machine by hand does not
work: the path does not exist there. This is a property of the copy rather than of
the artifact installed through `xlings`, the package's ELF files are repointed at
the target machine's own payloads at install time. The recorded path is a
build-machine detail, not a distribution format. Routes B and C are the ones that
survive hand-copying.

**On route B.** `--target …-musl` implies a static link, so there is no loader,
no RUNPATH and nothing to find at run time. It is the smallest and most
Expand Down Expand Up @@ -213,10 +213,10 @@ exec "$here/lib/ld-linux-x86-64.so.2" --library-path "$here/lib" "$here/bin/myap
The layout and wrapper above use an x86_64 example. The packer derives the
loader name from the target; for aarch64 it is `ld-linux-aarch64.so.1`.

#### Trap: `/proc/self/exe` under the bundled loader
#### `/proc/self/exe` under the bundled loader

Being started *by* the loader has a consequence the layout above does not
show: the kernel sets `/proc/self/exe` to the **loader**, not to your program,
show: the kernel sets `/proc/self/exe` to the **loader**, not to the program,
and `/proc/self/cmdline` carries the `--library-path` argument. Every "find my
resources next to the executable" path therefore resolves against `lib/`
instead of the bundle root — and it does so silently. In practice that means
Expand Down Expand Up @@ -244,7 +244,7 @@ own resolution, say — use `--mode vendored` instead. It repoints `PT_INTERP`
at the host loader, at the cost of requiring the host's glibc to be at least
as new as the one the artifact was built against.

### Windows (PE)a `.zip`, and the DLLs sit beside the `.exe`
### Windows (PE): a `.zip`, with the DLLs beside the `.exe`

A Windows target produces a **`.zip`**, not a `.tar.gz`, and the layout is
flat:
Expand Down
14 changes: 7 additions & 7 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ C++23 modules are fairly sensitive to compiler versions, and different releases

## Automatic Installation

The first time you run `mcpp build`, if no toolchain is configured yet, mcpp
On the first `mcpp build` with no toolchain configured, mcpp
installs and persists a default pair for the current host. The choice is
host-aware:

Expand Down Expand Up @@ -171,7 +171,7 @@ splits on the *env* segment, not on the OS:
| `x86_64-windows-msvc` | `foo.lib` (MSVC convention) |

Before 2026.8.3.3 a mingw build on a Windows host emitted `foo.lib` — a GNU
archive wearing an MSVC name, which MSVC cannot consume. If you have a script
archive wearing an MSVC name, which MSVC cannot consume. A script
that globs `*.lib` out of a `windows-gnu` build, it needs to glob `*.a` now.

## Linux ELF from Windows (`x86_64-linux-musl`, no WSL required)
Expand Down Expand Up @@ -218,10 +218,10 @@ Targets block, that host genuinely cannot serve it (implemented by
An MSVC toolset reaches a build one of two ways, and the **version axis of the
spec** says which:

| Spec | Origin | Which compiler you get |
| Spec | Origin | Compiler resolved |
|---|---|---|
| `msvc@system` (or bare `msvc`) | the machine's own Visual Studio | whatever is installed here |
| `msvc@<toolset>` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the one you named, on every machine |
| `msvc@<toolset>` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the named toolset, identically on every machine |

They are not alternatives to pick between once — they answer different
questions. `msvc@system` asks *"use what this developer already has"*;
Expand All @@ -236,7 +236,7 @@ Pinned toolsets coexist with each other and with a system Visual Studio.
> refusing to use what is already installed would cost more than it buys:
> Visual Studio is very often present and cannot always be redistributed.
> `<family>@system` for any other family is an error that names both things
> you might have meant. (The family-less `[toolchain] … = "system"` — the PATH
> that may have been intended. (The family-less `[toolchain] … = "system"` — the PATH
> compiler — is a separate and deliberate escape hatch, and is unaffected.)

### `msvc@system` — the machine's own Visual Studio
Expand Down Expand Up @@ -267,7 +267,7 @@ Default set to msvc@system (was: llvm@20.1.7)
```

If no Visual Studio is installed, mcpp says so and offers both routes — a
pinned toolset it can install for you, or the Visual Studio Installer /
pinned toolset it can install, or the Visual Studio Installer /
`winget install Microsoft.VisualStudio.2022.BuildTools`.

`mcpp toolchain list` shows the detected MSVC in a separate `System:` section,
Expand Down Expand Up @@ -459,7 +459,7 @@ unaffected, so this is a regression somewhere between Clang 18 and 20.
This bites the module-package pattern directly. Wrapping an upstream header
whose operators are `static inline` templates, and mirroring their signatures
with a trivially-true constraint (the standard mixed-TU subsumption recipe),
is exactly how you hit it.
is exactly how it is reached.

**The rule of thumb:** every template parameter should be pinned by the
**first** function argument. Shapes that break this are the poisonous ones:
Expand Down
Loading
Loading