mcpp.toml 是 mcpp 构建工具的项目配置文件,类似 Cargo 的 Cargo.toml 或 Node 的 package.json。放在项目根目录下,mcpp build 会自动发现并读取它。
mcpp 的设计原则是 约定优于配置 —— 大多数字段都有合理默认值,最简单的 mcpp.toml 只需几行:
[package]
name = "hello"
version = "0.1.0"mcpp 自动推断:
- 源文件:
src/**/*.{cppm,cpp,cc,c,S,s,asm} - 入口:
src/main.cpp→ 生成hello二进制 - 标准: C++23
- 模块: 扫描
export module ...声明自动建立依赖图
[package]
name = "mylib"
version = "0.1.0"
[targets.mylib]
kind = "lib"lib-root 约定:主模块接口默认在 src/mylib.cppm(包名的最后一段)。
[package]
name = "myapp" # 包名(必填)
version = "0.1.0" # 语义化版本(必填)
standard = "c++23" # C++ 标准(默认 c++23; 可设 c++20 / c++26)
description = "My awesome app" # 简介(可选)
license = "MIT" # 许可证(可选)
authors = ["Alice", "Bob"] # 作者列表(可选)
repo = "https://github.com/user/myapp" # 仓库地址(可选)standard 是 C++ 语言标准的一等配置。推荐值:
c++23:默认值,适合当前模块化默认模板。c++20:mcpp 接受的最低档位——命名模块本身是 C++20 特性,再往下这套构建模型就不存在了。当外部约束(公司内规、只到 C++20 的第三方 API)必须压低档位时使用。import std;在这一档依然可用:它虽然是 C++23 的库特性,但 GCC(≥ 15)、Clang + libc++(≥ 17)与 MSVC STL(VS 2022 17.8 起)都在 C++20 模式下提供std模块。代价是 C++23 库设施(std::print、std::expected等)不可用——包括mcpp new生成的模板代码。c++26:需要 C++26 语言特性时使用。c++2a/c++2c:兼容别名,解析后分别归一为c++20/c++26。gnu++20/gnu++23/gnu++26:需要 GNU dialect 时使用,会进入 fingerprint 和 std BMI cache key。c++latest:跟随当前 mcpp 支持的最新标准,适合本地试验,不推荐要求可复现的发布包使用。c++fly:c++latest再加上该工具链能开启的全部实验性标准特性(语言 + 标准库)。GCC ≥ 16 上会打开 C++26 反射(-freflection)与契约;Clang/libc++ 上追加-fexperimental-library;不支持的门会跳过并打印 summary。刻意是工具链相关的——最前沿的试验场模式,永远不要用于发布包。
两条需要知道的性质:
- 标准是模块图全局的。 根包的
standard作用于本次构建的每一个 TU,依赖也不例外—— 依赖自己 manifest 里的standard在它作为依赖被构建时不生效。这不是简化:BMI 跨档位 不兼容(GCC 直接报language dialect differs),同一张图物理上不可能存在两个档位。 - 档位之间从不共用缓存。 标准同时进入 fingerprint、
import std的 BMI 身份和依赖构建 缓存键,所以在c++20与c++23之间切换只会各自拿到独立的产物目录和独立的 std BMI, 不会出现错误命中。
如果源码在某个档位上 import std; 而解析出的工具链在该档位不提供 std 模块,
mcpp 会在编译前失败,并同时报出工具链与工程档位。
# 可执行程序(默认,有 src/main.cpp 时自动推断)
[targets.myapp]
kind = "bin"
main = "src/main.cpp" # 可选,默认 src/main.cpp
# 静态库
[targets.mylib]
kind = "lib"
# 共享库
[targets.mylib]
kind = "shared"
soname = "libmylib.so.1" # 可选: Linux/ELF ABI 名称,运行时会生成同名 aliassoname 用于共享库的 ABI 名称,类似 Autotools/CMake 中的
SOVERSION/SONAME。在 Linux 上,mcpp 会向链接器传递
-Wl,-soname,<name>,并在输出目录生成 <name> -> lib<target>.so alias,
让下游程序可通过标准 ABI 名称 DT_NEEDED 或 dlopen() 加载该库。
该字段只对 kind = "shared" 有效,值必须是文件名 basename。
当前共享库目标只支持 Linux/ELF。面向 macOS 或 Windows 的
kind = "shared" 目标(包括交叉构建)会在规划阶段直接拒绝,因为 mcpp
尚未建模 Mach-O install name 或 PE import library。若目标是这些平台,请使用
kind = "lib" 构建静态库,或将共享库目标设为 Linux。
[targets.server]
kind = "bin"
main = "src/server.cpp"
defines = ["BUILD_SERVER=1", "PORT=8080"] # -D 宏,只作用于该目标的入口
cxxflags = ["-Wno-deprecated-declarations"] # 该目标入口的额外 C++ 标志(不要放 -std=...)
cflags = ["-DPURE_C"] # 该目标入口的额外 C 标志
[targets.gui]
kind = "bin"
main = "src/gui.cpp"
required_features = ["gui"] # 仅当 feature `gui` 激活时才构建| 键 | 含义 |
|---|---|
defines |
预处理宏(name 或 name=value),脱糖为 -D<x>,作用于该目标入口的 C 与 C++ 编译。 |
cxxflags / cflags |
该目标的额外编译标志。不要放 -std=...——用 [package].standard。 |
required_features |
仅当列出的 feature 全部激活时才生成该目标,否则静默跳过。只是门禁——不激活 feature(用 --features / [features].default)。 |
作用域(重要): 目标上的
defines/cxxflags/cflags只作用于该目标独占的入口源 (它的main)——绝不作用于共享的模块/实现对象(那些只编译一次、被每个目标链接,即 mcpp 的 compile-once 模型)。当标志只需影响某个二进制(或测试)自己的入口时,这正是合适的工具 —— 例如某个测试的main里触发契约违规、需要按测试设置契约求值语义 (-fcontract-evaluation-semantic=observe),或入口独享的 feature 宏、局部告警抑制。 若标志必须穿透共享代码,就不该放在这里 —— 改用 workspace member 或[features];若是整次构建的模式,用[profile.*](mcpp test --profile <name>会让包括被测 代码在内的整个测试镜像都在该 profile 下编译)。
[targets.<name>]下的不支持键会产生 warning(--strict下为 error)。
构建配置该放哪 —— 当多个二进制需要不同配置时:
| 你想要 | 用 |
|---|---|
| 某二进制自己入口上的不同宏/标志 | per-target defines / cxxflags(见上) |
| 两个产品差异在它们共享的代码里 | 拆成 workspace member,各自 [build] 标志,共享一个 lib |
| 选择某共享库的变体(如某后端) | 在该库上用 [features](§2.8)——additive,作用到库自己的编译 |
| 整次构建的模式(sanitizer、契约语义、优化档) | [profile.<name>](§2.9)+ --profile;mcpp test --profile <name> 同样支持 |
mcpp 刻意不在一次构建里把同一个共享源编译成两份:一个源对应一个对象(模块还对应一个 BMI), 所以"必须穿透共享代码"的差异应放在包/feature 边界,而非单个目标上。
[build]
sources = ["src/**/*.cppm", "src/**/*.cpp"] # 源文件 glob(默认: src/**/*.{cppm,cpp,cc,c,S,s,asm})
include_dirs = ["include", "third_party/include"] # 头文件搜索路径
include_dirs_after = ["*"] # 排在系统目录之后搜索的头文件目录(-idirafter)
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 生效)
cache = "global" # 依赖的全局构建缓存:global(默认)| local | off(见 §2.10)include_dirs_after(#249)列出排在工具链系统目录之后搜索的头文件目录
(GCC/Clang 发射为 -idirafter;MSVC 方言退化为排在末尾的 /I,NASM 汇编
单元退化为普通 -I——两者都没有对应 flag,也都没有需要保护的系统头搜索链)。当目录是解压后的源码 tarball 根目录、且其中的文件名会与标准头冲突时,
用它代替 include_dirs —— 例如 ffmpeg 根目录的 VERSION 文件在大小写不敏感
的 macOS 文件系统上会把 libc++ 的 <version> 遮蔽(若该根目录挂在 -I 上)。
使用 include_dirs_after 时系统头永远优先,而包自己的真实头文件
(<libavutil/frame.h>)仍能找到。条目支持与 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
会自动重建模块缓存。
cxx_runtime 声明的是产物对运行它的机器做出的承诺。它是分发属性而非
构建属性 —— 它描述的是运行期依赖集,而兑现它的 flag 逐平台不同。
[build]
cxx_runtime = "self-contained" # 作用于所有目标(默认值)
# 或者按角色分别指定:
[build.cxx_runtime]
default = "self-contained" # 可执行文件与共享库
tests = "host-coupled" # 测试二进制从不离开本机
# 或者按目标三元组 —— 与 `linkage` 并列,因为它们是同一根轴:
[target.x86_64-linux-gnu]
cxx_runtime = "host-coupled" # 例如这次构建是为发行版打包| 取值 | 产物运行时需要 | 典型场景 |
|---|---|---|
self-contained(默认) |
自身之外不需要任何 C++ 运行时 | 分发二进制 |
toolchain-coupled |
mcpp 装的那份工具链的 C++ 运行时 | 本地迭代 |
host-coupled |
驱动默认解析到的那份(通常是系统运行时) | 发行版打包;必须与宿主共用同一份运行时的 dlopen 插件 |
默认即自包含(portable by default):macOS 上这会静态链入 LLVM 自带的
libc++/libc++abi —— 系统 libc++ 会把实际可运行版本钉死在构建机的 OS(老系统
缺新符号,如 std::print 的支撑符号),只有静态化才能真正兑现
macos_deployment_target 的 floor。Linux/MinGW 上它是 -static-libstdc++
(GCC)或整条链的 -static(MinGW);Linux 上的 clang/libc++ 工具链则显式链入
libc++.a/libc++abi.a/libunwind.a。更低的 macOS floor(11–13)需自建 libc++
归档(已验证可行,数据级切换,按需提供)。
static_stdlib 是旧拼写,仍然有效:true 等价于 self-contained,false
等价于 host-coupled。显式写了 cxx_runtime 时以后者为准。
当前实现限制。 解析器能识别
cxx_runtime,但当前[build]未知键白名单漏了 它。因此普通构建可能输出 unsupported-key warning,--strict会拒绝该 manifest。 这是实现缺陷,不是另一种拼写或不同的运行时契约。
兑现不了的契约会被报出来,绝不静默降级。 若工具链不带 libc++.a,或某个
契约在该平台上没有对应机制(MSVC 运行时的 self-contained 需要 /MT,mcpp
目前不发射),构建会打印实际退到了哪一档,而不是悄悄交付一个与 manifest 所述
不同的产物。
边界。 该契约只管 C++ 运行时。静态 libc 是另一根轴(linkage = "static"
/ --static,如 musl 目标),部署下限是第三根轴(macos_deployment_target)。
另外,host-coupled 只承诺 mcpp 不做任何"把 C++ 运行时打进产物"的动作,它不会
去掉链接因其它原因已经携带的工具链 rpath —— 所以在 ELF 上这类产物仍可能优先
找到工具链的库。
macOS +
self-contained与静态初始化次序。 Mach-O 没有按优先级排序的 初始化段,而 libc++ 的<iostream>也不像 libstdc++ / MSVC STL 那样自带ios_base::Init守卫 —— 于是从libc++.a里拉出来的流初始化器本来会排在 程序自己的全局构造函数之后:一个在构造函数里碰std::cout的全局对象会 读到尚未构造的流,进程启动即崩。mcpp 会把一个极小的生成对象排在链接最前面 把流顶上去,你的代码不需要做任何事。详见 #336。
defines 接受裸宏名(不带 -D),把每个条目脱糖为 -D<x>,同时作用于 C 和
C++ 编译通道。它覆盖包内每个 TU(含模块接口单元),因此也会进入 P1689 模块扫描
—— 这正是被宏保护的 import 能被解析的前提。汇编单元同样能拿到。它是普通的构建
输入,所以 [target.'cfg(...)'.build] 也能承载它:
[build]
defines = ["APP_NAME=\"demo\""]
[target.'cfg(windows)'.build]
defines = ["USE_WIN32", "WINVER=0x0A00"]选择合适的轴:
| 想让宏作用于… | 用 |
|---|---|
| 本包的每个 TU | [build].defines(本节) |
| 仅某个二进制自己的入口源 | [targets.<name>].defines |
| 指定的一批文件 | [build].flags 配 glob + defines |
| 本包每个 TU 以及消费者的 TU | [features.<name>].defines(接口贡献) |
[build].defines 是包私有的:不会传播给消费者。
[build] 下不支持的键会作为警告报出(--strict 下为错误),而不是被静默忽略。
C++ 标准不要通过 build.cxxflags = ["-std=..."] 配置。请使用:
[package]
standard = "c++26"mcpp 会把同一个标准用于普通 C++ 编译、模块扫描、compile_commands.json 和 import std 的标准库 BMI 构建。
glob 排除(! 前缀,mcpp 0.0.4+):
[build]
sources = [
"src/**/*.cpp",
"!src/**/*_test.cpp", # 排除测试文件
"!src/**/*_fuzzer.cpp", # 排除 fuzzer
]per-glob 旗标(mcpp 0.0.95+):[build] flags 是有序的内联表数组,把额外
编译旗标只附加到 glob 命中的源文件——SIMD 多档 dispatch TU 与三方代码告警隔离的
正解:
[build]
flags = [
{ glob = "third_party/**", cflags = ["-w"], cxxflags = ["-w"] },
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"], defines = ["HAVE_AVX2"] },
{ glob = "src/x86/**/*.asm", asmflags = ["-DPREFIX"] },
]每条目键:glob(相对包根,必填)+ cflags / cxxflags / asmflags /
defines(没有 ldflags——链接没有 per-TU 作用域)。声明顺序即应用顺序:靠后
条目的旗标排在命令行更后,配合 GNU "后旗标胜",窄 glob 放在宽 glob 之后即可覆盖。
所有命中条目都生效;这些是私有构建旗标,不会传播给消费者。glob 零命中会打印
warning(打错的 glob 不允许静默无效)。
生成文件(mcpp 0.0.95+):[generated_files] 把相对路径映射到文件内容(支持
TOML 多行字符串)。条目在源 glob 展开之前写入工程树——与 index 描述符合成模块
包装文件是同一机制——内容进指纹,改内容即重建:
[generated_files]
"src/gen/wrap.cppm" = """
module;
#include <vendored.h>
export module wrap;
"""路径必须留在工程根之内(.. / 绝对路径是解析错误)。
汇编源(mcpp 0.0.95+):.S/.s(GAS——由 C 驱动器预处理,覆盖 ARM 与
AT&T 语法 x86)和 .asm(NASM——Intel 语法 x86)是一等源文件:默认 glob 收录、
进指纹、增量并行构建、像任何对象一样链接。NASM 的输出格式由目标三元组推导
(elf64/win64/macho64/...——交叉构建零特判);nasm 仅在存在 .asm 单元时
惰性解析:先 PATH,再 mcpp 沙箱,再 xlings install nasm;找不到 ≥2.16 的
nasm 则硬失败(汇编绝不静默跳过)。限制:.asm 仅限 x86 目标(其他目标硬
报错——用条件 sources 门控)、MSVC 工具链不支持 .S、.asm 即 NASM 语法
(MASM 源请用 ! 排除)。
[lib]
path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置默认约定:src/<包名最后一段>.cppm(如包名 mcpplibs.cmdline → src/cmdline.cppm)。
# 默认包空间(mcpplibs)下的包
[dependencies]
gtest = "1.15.2" # 精确版本
mbedtls = "3.6.1"
ftxui = "6.1.9"
# dotted selector: 先匹配 mcpplibs.<path>, 找不到再匹配同级 peer root。
# 例如 imgui.core 会按顺序尝试 mcpplibs.imgui/core, imgui/core。
[dependencies]
capi.lua = "0.0.3"
compat.gtest = "1.15.2"
imgui.core = "0.0.1"
imgui.backend.glfw_opengl3 = "0.0.1"
# 命名空间子表写法
[dependencies.mcpplibs]
cmdline = "0.0.2"
tinyhttps = "0.2.2"
llmapi = "0.2.5"
[dependencies.compat]
glfw = "3.4" # 显式 namespace, 不走 mcpplibs 优先候选
# 路径依赖(本地开发)
[dependencies]
mylib = { path = "../mylib" }
# Git 依赖 —— tag / branch / rev 三选一
[dependencies]
mylib = { git = "https://github.com/user/mylib.git", tag = "v1.0.0" }
applib = { git = "https://github.com/user/applib.git", branch = "develop" }
# 长式 dep spec:features 与 backend 旋钮
[dependencies]
imgui = { version = "0.0.3", features = ["docking"] } # 请求该依赖的 feature
widget = { version = "1.0", backend = "glfw_opengl3" } # 糖:= features=["backend-glfw_opengl3"]backend = "<impl>" 是通用约定糖:1:1 脱糖为请求该依赖的 backend-<impl>
feature(库若支持该旋钮,应在自己的 [features] 中声明 backend-* 系列)。
若目标包声明了 [features] 但不含所请求的 feature(含 backend 脱糖结果),
默认给出 warning,mcpp build --strict 下报错。
Git 依赖与 mcpp.lock:tag 和 rev 本身就指向历史中的固定点,而 branch
是会动的。首次构建把分支解析成一个 commit 并写进 mcpp.lock,此后每次构建都重建
那个 commit —— lock 是权威而不是缓存提示,所以删掉 ~/.mcpp/git 或换一台机器
都不会悄悄把你挪到更新的分支头上。要新的分支头,得显式要:
mcpp update mylib # 丢掉记录的 commit,下次构建重新解析
mcpp update # 同上,对所有依赖既然记录的 commit 已经足以决定构建什么,那么在 ~/.mcpp/git 里已有克隆的情况下,
重新构建完全不发网络请求,--offline 下照常工作。只有两件事需要网络:解析一个在
lock 里没有 commit 的分支,以及克隆一个尚未缓存的 commit。git = 若指向本地目录
(或 file:// URL),这两件事都不需要网络,因此离线下也绝不会被拒绝。
SemVer 约束:
[dependencies]
foo = "^1.2.3" # >= 1.2.3, < 2.0.0 (caret,默认)
bar = "~1.2.3" # >= 1.2.3, < 1.3.0 (tilde)
baz = "=1.2.3" # 精确匹配
qux = ">=1.0, <2.0" # 范围组合每个包的身份是命名空间 + 名字二元组。依赖 key 的写法决定 mcpp 到哪些命名空间里找。
裸名只在三个地方解析,按序:
| # | 命名空间 | 示例 |
|---|---|---|
| 1 | mcpplibs — 默认命名空间 |
cmdline = "0.0.2" |
| 2 | compat — 第三方 C/C++ 库的包装命名空间 |
gtest = "1.15.2" → compat.gtest |
| 3 | 完全没有声明命名空间的上游包 | opencv = "4.10.0" |
其他命名空间一律必须写全。 不存在按短名的全索引模糊搜索:
# ✅ 正确 —— 点式选择器
[dependencies]
"chriskohlhoff.asio" = "1.38.1"
# ✅ 正确 —— 命名空间子表(同一组织有多个包时更推荐)
[dependencies.chriskohlhoff]
asio = "1.38.1"
# ❌ 错误 —— 裸名永远到不了 chriskohlhoff 命名空间
[dependencies]
asio = "1.38.1"第三种写法会明确报错,并列出搜索过的命名空间;若该短名的包存在于别处,错误信息会直接给出应当改写成的那一行。
为什么不让裸名跨所有命名空间去找? 因为依赖解析必须可复现。全域短名搜索意味着:(a) 两个命名空间拥有同名包时,胜负由索引顺序决定;(b) 新增一个索引可能悄悄改变某个既有依赖解析到的包。要求写出命名空间,才能让同一份 mcpp.toml 在每台机器上解析到相同的包。
给 xpkg 作者: 索引描述符里,身份是 (package.namespace, package.name) 二元组。命名空间是点分路径,name 是单一原子段:
package = {
namespace = "chriskohlhoff",
name = "asio", -- 单一段;不是 "chriskohlhoff.asio"
}
package = {
namespace = "mcpplibs.capi", -- 层级放这里
name = "lua",
}文件名只是提示 —— 描述符按声明的身份被发现,所以 pkgs/c/chriskohlhoff.asio.lua 与 pkgs/z/anything.lua 解析结果完全相同。推荐 <name>.lua 或 <namespace>.<name>.lua(命中 mcpp 的快路径),但不强制。
旧的完全限定拼写(name = "chriskohlhoff.asio")仍被接受,已发布的描述符无需改动。mcpp xpkg parse 会校验该规则,请在索引 CI 里跑它。需要 mcpp >= 0.0.106 与 xlings >= 0.4.69;规范全文见 docs/spec/package-identity.md。
[features] 的条目除了写成数组,还可写成表,从而让该 feature 在隐含 feature
之外,携带包自有的预处理 defines、feature 门控的源 glob(sources,mcpp
0.0.95+——列出的 glob 离开默认构建,仅当 feature 激活时才编译,与 index 描述符的
features.<f>.sources 完全对等;这正是 vendored 大库最高频的形态:feature =
一组源文件 + 一个 define)、feature 门控的 per-glob 编译旗标(flags,mcpp
0.0.101+),以及 capability 的 requires / provides(见 §2.8.1):
[features]
default = []
# 数组简写:仅隐含 feature。
docking = ["extra"]
extra = []
# 表形式:激活时贡献一个包自有的宏。
mpl2only = { defines = ["EIGEN_MPL2_ONLY"] }
# 表形式:宏 + 一个隐含 feature。
fast_math = { defines = ["APP_FAST=1"], implies = ["extra"] }
# 表形式:feature 门控源 + 与其同居的 per-glob 旗标。
simd = { sources = ["src/simd/**"], flags = [
{ glob = "src/simd/**/*.avx2.cpp", cxxflags = ["-mavx2"] } ] }defines为裸宏名(不带-D);feature 激活时每个脱糖为-D<x>,加到该包 自己的编译上——与[targets.*] defines完全一致。按约定仅限包自有的带命名 空间宏:feature 不注入自由的包级cflags/ldflags,否则会破坏加性的 feature 并集模型。链接旗标来自 provider 依赖(§2.8.1),而非 feature。- 每个激活的 feature 仍会得到自动的
-DMCPP_FEATURE_<NAME>,defines与之叠加。 flags(mcpp 0.0.101+)与[build].flags(§2.3)共用同一有序 inline-table 数组 文法(glob必填,加cflags/cxxflags/asmflags/defines;与[[build.flags]]一样也接受[[features.<name>.flags]]拼写)。feature 激活时 条目追加在 base[build].flags之后(feature 按名 序),"last flag wins" 使 feature 规则可覆盖更宽的 base 规则;未激活时条目根本 不存在(不会有死 glob 告警)。这让 feature 的组内专属旗标与其sources同居, 而不必写成 base 规则、在 feature-off 构建里留下必死的 glob。与defines不同, featureflags是私有 per-TU 构建旗标——永不传播给消费者(与[build].flags同契约),因此不破坏加性模型:glob 限定作用面、顺序确定、无跨包效应。
mcpp build / run / test 只在依赖无法用本地索引解析时刷新包索引,绝不会
因为"时间到了"就刷。具体地说,只有三种情况会触发:本地根本没有索引、依赖的描述符
不在其中、或 SemVer 约束在本地已知版本里无解。只要所有依赖都能在本地解析出来,
无论本地索引多旧,构建都不会发起任何网络请求。
由此带来的一个需要知道的语义:^1.2 这类约束是对本地索引已知的版本求解的。
如果上游在你上次刷新之后发布了 1.3.0,你需要主动去取:
mcpp index update # 同步索引
mcpp update # 同步索引,并重新解析依赖
mcpp index status # 看本地现状:状态、年龄、修订号三个开关,优先级从高到低:
| 开关 | 作用 |
|---|---|
--offline(任意命令) |
完全不碰网络——不刷索引、不下载、不自动装工具链,也不发 git ls-remote/clone。已安装的东西照常构建,包括 commit 已在 mcpp.lock、克隆已在缓存里的 git 依赖 |
MCPP_OFFLINE=1 |
同上,作用于整个 shell 会话或 CI job |
~/.mcpp/config.toml 里 [index] auto_refresh = false |
永不自动刷新索引,但下载仍然可用 |
MCPP_NO_AUTO_INSTALL=1 作为 --offline 的旧式窄化拼写仍然有效(它只管工具链的
自动安装)。
任意命令加 -v 可以看到每个依赖的判定结果与原因。
capability(能力) 是一个共享的抽象名字(如 blas)。包可以 provide(提供)
一种能力;feature 可以 require(需要)一种能力而非点名某个具体包,解析器会从依赖
图中绑定恰好一个 provider。这样就能在多个可互换后端(OpenBLAS / MKL / …)中选其
一,而不必把选择写死进库里。
# provider 包为任何 require 它的依赖方满足某能力。
[package]
name = "compat.openblas"
version = "0.3.0"
provides = ["blas", "lapack"]# 消费方经由自己的某个 feature 来 require 这个抽象能力。
[features]
use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] }
# 图中有 >1 个 provider 时,选其一(否则构建报错并列出候选)。
[capabilities]
blas = "compat.openblas" # 等价于:mcpp build --cap blas=compat.openblas
[dependencies]
compat.openblas = "0.3.0" # provider 必须是图中真实存在的依赖绑定是确定性的:
| 图中某被需要能力的 provider 数量 | 结果 |
|---|---|
| 恰好一个 | 自动绑定(无需配置) |
[capabilities] pin / --cap 指定了一个 |
以 pin 为准 |
| 零个 | 报错:没有包提供 <cap> |
| 两个及以上且未 pin | 报错并列出候选——绝不静默猜测 |
被绑定 provider 的链接/头文件旗标经由常规依赖机制流到消费方;capability 层是那道 选择与校验 步骤,把"静默选错后端 / 缺后端"变成构建期的显式报错。
在 [feature-deps.<name>] 下声明的依赖是可选的:仅当该 feature 激活时(根 --features,
或某依赖 spec 的 features = [...])才会被解析。[dependencies] 中的依赖始终被解析;
可选性由声明的位置表达,而非某个标志位。
[features]
use_blas = { defines = ["EIGEN_USE_BLAS"], requires = ["blas"] }
backend-openblas = { implies = ["use_blas"] }
# 仅当 `backend-openblas` 激活时才拉取。每个条目都是完整的依赖 spec
#(version/path/git + 其自身的 features)。
[feature-deps.backend-openblas]
compat.openblas = "0.3.x"该机制与能力(§2.8.1)组合:单个 backend-openblas feature 既拉取 provider
(compat.openblas,其 provides = ["blas"]),又开启消费方开关
(implies = ["use_blas"],其 requires = ["blas"])。当图中只有一个 provider 时,
能力自动绑定——消费方只需写 features = ["backend-openblas"]。
在索引包的 Lua 描述符中,等价写法为内联形式:
features = {
use_blas = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } },
["backend-openblas"] = {
implies = { "use_blas" },
deps = { ["compat.openblas"] = "0.3.x" },
},
}[profile.dist]
opt = 3 # -O 级别(数字或 "s"/"z" 字符串)
debug = false # -g
lto = true # -flto(注意:部分打包 gcc 未启用 LTO 插件)
strip = true # 链接期 -s
# passthrough 逃生口(固定键、开放值):
cflags = ["-fno-plt"]
cxxflags = ["-fno-plt"]
ldflags = []- 选择与默认:裸
mcpp build走dev档(-O0 -g)——主流惯例(参照 Cargo/Meson/CMake/Zig/Bazel)。release 为 opt-in:mcpp build --release(短写)或--profile release;--dev是 dev 的显式短写。mcpp test --profile <name>同理 (被测代码与测试二进制都在该 profile 下编译)。 - 项目级默认 ——
[build].default-profile = "<name>"(别名profile)设置该项目在不带 flag 时的默认。典型用途是"以发布优化为常态"的工具/库:[build] default-profile = "release"。 优先级:--profile/--release/--devflag >[build].default-profile> 全局dev。 (默认 dev 的项目在产出可分发物时应显式--release。) - 内置档案:
release(-O2)/dev、debug(-O0 -g)/dist(-O3 + strip; 不默认开 lto)。[profile.<内置名>]可整体覆盖内置定义。 - 每个 profile 各占一个构建目录。 解析后的 profile 开关参与指纹,所以
target/<triple>/下每个 profile 一个哈希目录,来回切换是增量而不是全量重编; 代价是磁盘占用随实际使用的 profile 数量增长。
从索引获取的依赖,其编译产物按包缓存在 $MCPP_HOME/build-cache/v1/ 下,跨工程共享。
依赖的产物与"谁在消费它"无关,所以工具链、profile、依赖版本相同的两个工程复用同一条目。
[build]
cache = "global" # "global"(默认)| "local" | "off"| 模式 | 读缓存 | 写缓存 | 先清构建目录 |
|---|---|---|---|
global(默认) |
是 | 是 | 否 |
local |
否 | 否 | 否 |
off |
否 | 否 | 是 |
local 把所有依赖都编在本工程 target/ 内 —— 排障时一次性排除"是不是缓存的问题",
也给 CI 一个无共享的可复现基线。off 额外清掉本次的 target/<triple>/<fp>/ 做冷构建;
--no-cache 是它的兼容别名。
优先级:--cache <mode> > MCPP_BUILD_CACHE > [build] cache > global。
无法识别的值会被报出来(--strict 下为错误),而不是静默回落到 global。
不进缓存的:path 与 git 依赖(任意深度)以及 workspace 成员。它们的源码可以在
name@version 不变的情况下改变,任何基于该身份的键都看不见这种变化。
查看与回收:
mcpp cache dir # 缓存在哪
mcpp cache list [--json] # 条目、体积、最后使用时间
mcpp cache info <pkg>@<ver> # 单条目详情,含它是用什么键输入编出来的
mcpp cache verify # 逐条目校验清单与磁盘
mcpp cache gc --max-size 5GiB # 按 LRU 收到容量预算内
mcpp cache gc --older-than 30d # 或按"多久没用过"回收
mcpp cache clean [--deps|--std|--all|--legacy]
条目的磁盘布局是带版本的。改动布局的 mcpp 版本会一次性作废全部旧条目,
所以升级后的第一次构建会重编依赖并重新填充 —— 不需要手工清理。
2026.8.3.4 就是这样一次:条目里对象的地址现在相对包自身,
而不再相对"最先填充这个条目的那个工程"的构建目录。
mcpp cache verify 另外会报告任何逃出条目的记录地址,
使这条不变量可以离线审计。
[runtime]
library_dirs = ["vendor/lib"] # 烤进产物 RUNPATH 的目录(相对包根)
dlopen_libs = ["libGL.so.1"] # 运行期 dlopen 的 soname(doctor 校验)
capabilities = ["opengl.glx.driver"] # 需要的主机能力(开放命名空间)
provides = ["opengl.glx.driver"] # 显式声明本包兑现的能力(强 provider)
# 显式 provider 覆盖(三档旋钮的"显式"档)
[runtime."opengl.glx.driver"]
provider = "compat.glx-runtime"- provider 选择:声明
provides的包(强)优先于仅在capabilities列出 能力的包(弱,向后兼容);[runtime.<cap>] provider=显式覆盖最优先, 指向依赖图中不存在的 provider 时给出 warning。 - 解析结果可经
mcpp why runtime、mcpp self doctor与构建产物target/<triple>/<fp>/resolution.json查看(默认不是魔法)。 - 能力命名约定:分层小写
domain.sub.role(如opengl.glx.driver、x11.display)与前缀类abi:<name>(如abi:glibc,参与工具链 ABI 强制)。
[package]
platforms = ["linux", "macos", "windows"]声明包支持的平台(CI 矩阵提示,经 mcpp why 展示)。词表由 mcpp 固定
(它拥有 target/triple 体系):linux | macos | windows;未知值 warning,
--strict 下报错。
[xlings]
deps = ["make@4.4", "cmake@3.28", "python@3.13"] # 要供给的 host 构建工具
subos = "dev" # 命名的项目级沙箱
[xlings.workspace] # 固定工具版本([toolchain] 的通用形式)
clang = "20.1.7"
[xlings.envs] # 应用到工具环境的环境变量
OPENBLAS_NUM_THREADS = "1"声明项目的构建环境,经 xlings(mcpp 的底座)供给。子段名与 xlings 自身的
.xlings.json schema 1:1 对齐,因此 mcpp 把它们原样物化进
<项目>/.mcpp/.xlings.json(无翻译层):deps(host 构建工具)、[xlings.workspace]
(工具→版本固定)、subos(命名沙箱)、[xlings.envs](环境变量)。用它声明构建所需的
host 工具(make/cmake/protoc…)、按项目固定工具版本、或设构建期环境变量——无需手改
.xlings.json。[toolchain](§2.7)仍是编译器的便捷简写;[xlings.workspace] 是其通用形式。
在 Linux 上 subos 还有第二重含义:它决定构建绑定到哪个 C 运行时。subos 会自述其
runtime(xlings 2026.8.5.1 起),mcpp 以此为权威,而不是去四处找一个 libc——所以同一台机器
上 subos = "el8" 与 subos = "trixie" 两个项目各自产出面向自己 glibc 的产物,且编译期与
运行期保证一致。该绑定计入工具链指纹,故切换它会重新构建,而不会复用另一个 subos 的目标文件。
更旧的 subos(或没有 subos)则回落到工具链自身安装时所对应的运行时;mcpp build -v 会打印
实际走了哪一条。参见 docs/08-toolchain-internals.md §2.1。
一个包能构建出消费者在构建期需要的二进制 —— protoc、grpc_cpp_plugin、
flatc、moc、转译器。在依赖上声明:
[dependencies]
protobuf = { version = "35.1", tools = ["protoc"] }
grpc = { version = "1.83.0", tools = ["grpc_cpp_plugin"] }每个名字必须是该包的一个 kind = "bin" target。mcpp 会为构建机器构建它,
并把绝对路径以 MCPP_DEP_<PKG>_BIN_<TOOL> 交给 build.mcpp —— 用
mcpp::dep_bin("protobuf", "protoc") 读取(见 07 — build.mcpp)。
四条值得知道的性质:
- 永远是 host 二进制。 即使
mcpp build --target <triple>,工具依然为本机 构建 —— 代码生成器必须在这里跑。它是一次独立的、面向 host 的子构建:工具包 自己的[toolchain]、自己的依赖解析生效,不需要与你的构建一致。之所以安全, 是因为可执行文件与你的代码零 ABI 接触。 - 单一版本轴。 工具的版本就是依赖的版本,所以「protoc 与其运行时不匹配」 这种情况不可表达。(把工具单独打包正是会出这个问题,而且它在运行期才咬人, 不是编译期。)
- 默认关闭。 没人要就什么都不构建,成本由消费者付。包用
[features]+required_features给昂贵的部分加门(protobuf 的protoc需要 libprotoc 的 ~157 个额外 TU,只用运行时的人绝不该编译它)。 - 全局缓存,按 包版本 × host 工具链 × feature × 自身依赖闭包 键控 —— 每台机器 构建一次,而不是每个工程一次。
[tools.overrides]
"compat.protobuf:protoc" = "/usr/bin/protoc"或者不改 manifest(CI、发行版打包):
MCPP_TOOL_PROTOBUF_PROTOC=/usr/bin/protoc mcpp build命中 override 会完全跳过构建。每个同类系统都提供这条逃生舱(vcpkg 的
VCPKG_HOST_TRIPLET、CMake 的 LLVM_NATIVE_TOOL_DIR、Qt 的 QT_HOST_PATH),
理由一样:一个在本机构建不出来的工具不能是死路。它刻意不进 cache key ——
逃生舱不是可复现输入。
一条规则(比如「对这些 .proto 跑 protoc」)应该写一次,而不是复制进每个
消费者的 build.mcpp。把它做成普通的 mcpp 库包再 import:
[dependencies]
protobufgen = { version = "0.1.0", host-module = true }// build.mcpp
import mcpp;
import protobufgen;
int main() { return protobufgen::generate({"schema"}) ? 0 : 1; }mcpp 会把该包的 lib 根模块为 host 编译,且与 build.mcpp 在同一条命令里 ——
这正是 BMI 能用的前提:一个模块接口只对「在 standard / dialect / 编译器身份上与
它一致」的编译可导入。
于是规则有版本、能测试、能通过你已有的包管理器分发,而且是用 C++ 写的
—— 不引入第二门语言,这正是 build.mcpp 存在的理由。
模块名就是包的 name。 mcpp 用依赖的裸 package.name(而不是
<namespace>.<name>)注册这个 host 模块,所以规则包的名字必须是合法的 C++ 模块名:
grpcgen 可以,grpc-rules 不行 —— 连字符在包名里合法、在模块名里非法,而且报出来
的是 module 'grpc_rules' not found,不会提示你名字有问题。
lib 根必须在 src/<name>.cppm(或 [lib] path 指向的位置);缺失时报
"host module 'x': no interface unit at …"。
限制: 规则接口是单独编译的,因此可以 import std 与内置 mcpp 模块,
但不能 import 第三个包。规则包按构造是叶子。
仅构建期: host-module = true 的依赖不会被编进、也不会被链进你的 target。
它只在 build.mcpp 期间运行,别处都不出现 —— 与 Cargo 用 [build-dependencies]
划出的是同一条界线。(2026.8.5.2 之前它还会被当作普通库再编一遍,这正是规则里
import mcpp; 失败的原因:在那第二次编译里内置模块并不存在。)
上面这些都由使用工具的人声明。当知识本来属于库时,这个位置就错了:gRPC
的代码生成需要 protobuf 的 protoc,而 gRPC 包的任何使用者都不应该知道这件事。
reexport = true 把一条边上的构建期提供物 —— 它的 tools、它的
host-module、以及该依赖的目录 —— 交给本包自己的消费者:
# 写在 grpc 包自己的 manifest 里
[feature-deps.codegen]
"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true }
grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"], reexport = true }
grpcgen = { version = "1.83.0", host-module = true, reexport = true }于是使用者只写一行,再 import 那个规则:
[dependencies]
grpc = { version = "1.83.0", features = ["codegen"] }// build.mcpp
import mcpp;
import grpcgen;
int main() { return grpcgen::generate_all() ? 0 : 1; }- 默认关闭,并且刻意不复用边上的
visibility。visibility本身默认就是"public",搭它的车意味着任意深度的依赖都能不声不响地往你的构建程序的工具 命名空间里塞东西。「把某样东西交给消费者」是一条供应链主张,必须写下来。 - 一次声明只走一跳。 被再导出的提供物到达声明它的那个包的消费者;要继续
往上走,下一个包必须自己也写
reexport。每个包只决定它交出什么。 - feature 可以往一条已经声明过的依赖上追加请求。 gRPC 无条件依赖 protobuf,
而它的
codegenfeature 往同一条边加tools = ["protoc"], reexport = true。tools与features取并集,host-module与reexport取或;version/path/git不合并 —— feature 仍然无法静默覆盖无条件条目的身份。 - 传播的是可见性,不是执行。
dep_bin()只返回路径,跑不跑仍由消费者的build.mcpp决定;谁构建了这个工具、tool store 怎么做键,都不改变。 - 裸名由阶梯决定,而不是靠运气。 一旦两个库都能再导出,它们可能同时提供
尾名
protobuf。全限定的MCPP_DEP_<NS>_<NAME>_BIN_<TOOL>总是发布;裸名 依次绑定到mcpplibs.<x>、compat.<x>、无命名空间的<x>,最后才是「剩下 的唯一候选」——存在争用时 mcpp 会说出来,而不是默默选一个。
不认识的依赖键会被记为降级并忽略(mcpp 2026.8.6.2+),因此一份为更新的 mcpp 写的包仍然能加载,这个读取器认识的部分照常生效。在那之前它是整份加载 失败且报错误导,这正是「已发布的包永远无法采用新键」的原因——与索引下限确立 的是同一条性质:数据不得决定程序是否可用。
因此,一个依赖 reexport 才有那套人机工程的包,仍然需要足够新的客户端;
变化在于该包的其余部分在旧客户端上不再一起失效。
一个包可能只在部分平台声明 bin 目标。既然现在是库决定请求什么,无条件的
请求就会把「不支持的平台」变成用户改不掉的硬错。用条件段裁剪:
[target.'cfg(not(windows))'.feature-deps.codegen]
"compat.protobuf" = { version = "35.1", tools = ["protoc"], reexport = true }[target.<sel>.feature-deps.<feature>](2026.8.6.2+)与 [target.<sel>] 下的
其余依赖表(dependencies / dev-dependencies / build-dependencies)遵循同
一套谓词规则,针对解析后的 target 求值。feature 本身在所有平台都注册
—— 只有它拉进来的东西是条件性的 —— 因此在没有任何谓词匹配的平台上请求它,不是
「未知 feature」错误。
exe 图标,以及 Windows 在文件「属性」里显示的版本信息,就是 mcpp.toml 里的一个路径:
[resources]
icon = "assets/app.ico"常见场景到此为止。FILEVERSION、ProductName、FileDescription、CompanyName、
LegalCopyright 全部从 [package] 取默认值,资源脚本由 mcpp 生成。
| 键 | 类型 | 含义 |
|---|---|---|
icon |
路径 | 作为应用图标嵌入(资源序号 1) |
files |
路径列表 | 你自己的 .rc 脚本,mcpp 编译并跟踪为构建输入 |
extra-inputs |
路径列表 | .rc 扫描器看不见的输入(见下) |
version-info |
布尔 | false 表示不要生成版本资源 |
[resources.version-info] |
表 | company、product、description、copyright、original-filename、internal-name |
只有 PE 目标会编译这一节。 在 Linux/macOS 上它不适用:不产资源单元、
不出诊断、构建逐字节不变。你不需要(也不能)加 cfg(windows) 谓词 ——
无条件写一次即可。
声明了却不存在的文件会让构建失败 —— 在每个目标上都是。 资源和源码一样是
构建输入;mcpp 不会悄悄产出一个缺了它的二进制。校验刻意不按 PE 设门:
路径存不存在是关于你工作树的事实,不是关于目标的事实,所以 icon = "assets/app.ico"
里的拼写错误由你的 Linux/macOS 构建(以及它们的 CI job)当场抓住,而不是等
Windows 那条。不想要图标,把那一行删掉。
版本字段。 FILEVERSION 取 [package].version 的四段数值,每段必须放得进
16 位;字符串字段保留版本原文,所以数值字段装不下的形态(1.0.0-rc1)在属性
对话框里照样看得到。
[resources]
files = ["res/app.rc"]写了 files,mcpp 就不再生成版本资源 —— 资源 ID 空间归你。想两者都要就同时写
version-info = true(注意冲突:序号 1 的 RT_VERSION 只能有一个)。
想从生成的脚本起步而不是从空文件起步:把它从构建目录里拷出来
(target/<triple>/<fp>/res/<target>.mcpp.rc)填进 files。结果字节相同,
所以从「生成」走到「手写」不会改变产物。
VS_VERSION_INFO需要<windows.h>。 手写脚本里如果写VS_VERSION_INFO VERSIONINFO而没有#include <windows.h>,版本资源会被存成 字符串名而不是序号 1。所有工具依然报告Type: VERSIONINFO,但GetFileVersionInfo查的是序号,于是 PowerShell 的FileVersionInfo里每个字段 都是空的。要么 include<windows.h>,要么直接写1 VERSIONINFO。mcpp 见到这个 形状会警告;它自己生成的脚本用的是字面1。
mcpp 会读 .rc,把引号形式的 #include 和资源语句(ICON、RCDATA、
MANIFEST …)点名的文件都变成构建输入,所以改图标会重链。尖括号形式
(<windows.h>)属于工具链,由工具链 fingerprint 覆盖。
通过宏间接引用的文件名(1 ICON APP_ICON)扫描看不见。mcpp 会指名它没能解析
的东西,并要求你显式声明:
extra-inputs = ["assets/app.ico"]不是资源脚本的输入 —— objcopy 嵌入的 blob、生成的 .def、预编译对象 ——
可以由构建程序声明一个产出接到链接的图节点:
mcpp::action o;
o.id = "blob"; o.role = "object";
o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o")
.input("blob.bin")
.output("${mcpp.out_dir}/blob.o")
.target("myapp") // 省略:接到每个镜像,含测试二进制
.submit();见 07 — build.mcpp。把这类文件写进 [build].ldflags 也「能用」,
但 ldflags 是链接命令里的一串字符:没有任何东西跟踪它,改了它得到的是
ninja: no work to do。
语法封闭,词汇开放:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。
- mcpp 只定义机制(features 并集/闭包、capability require/provide/override、 profile→编译器旗标、platform→triple),键与形状固定;feature 名、能力名、 后端名等领域词汇只出现在值里,不进 mcpp 代码。
- 不支持包自定义 toml 键:键合法性不得依赖"先解析目标包",否则 manifest 失去静态可解析性(lockfile/LSP/审计的前提)。包的扩展点 = 固定机制内的开放值域。
- 包级旋钮统一收敛进 features;糖键(如
backend=)进入核心语法须满足: ① 领域中立(跨生态通用模式)② 1:1 脱糖、零新增解析语义。 - 字段归属总表与定型决策见
.agents/docs/2026-06-04-manifest-schema-ownership.md。
[package]
name = "hello"
version = "0.1.0"// src/main.cpp
import std;
int main() { std::println("Hello, mcpp!"); }mcpp build && mcpp run[package]
name = "mymath"
version = "1.0.0"
[targets.mymath]
kind = "lib"
[dev-dependencies]
gtest = "1.15.2"// src/mymath.cppm
export module mymath;
export int add(int a, int b) { return a + b; }// tests/test_add.cpp
#include <gtest/gtest.h>
import mymath;
TEST(Math, Add) { EXPECT_EQ(add(1, 2), 3); }mcpp build # 编译库
mcpp test # 编译 + 跑测试[package]
name = "myapp"
version = "0.1.0"
[dependencies]
ftxui = "6.1.9"
[dependencies.mcpplibs]
cmdline = "0.0.2"
llmapi = "0.2.5"mcpp 自动:
- 从 mcpp-index 下载源码 tarball
- 按
[build].include_dirs传播头文件路径 - 传递依赖自动入图(llmapi → tinyhttps → mbedtls 全自动)
[package]
name = "myc"
version = "0.1.0"
[build]
c_standard = "c99"
include_dirs = ["include"]
sources = ["src/**/*.c"]
[targets.myc]
kind = "lib"[package]
name = "hybrid"
version = "0.1.0"
[build]
include_dirs = ["include"]
c_standard = "c11"
[dependencies]
lua = "5.4.7" # 纯 C 库,mcpp 自动用 C 编译器编译 .c 文件
[targets.hybrid]
kind = "bin"[package]
name = "mytool"
version = "1.0.0"
[toolchain]
default = "gcc@16.1.0"
[target.x86_64-linux-musl]
toolchain = "gcc@16.1.0"
linkage = "static"mcpp build --target x86_64-linux-musl
# → 产出完全静态链接的二进制,可直接 scp 到任意 Linux x86_64 机器运行| 项目 | 默认值 | 说明 |
|---|---|---|
| 源文件 | src/**/*.{cppm,cpp,cc,c,S,s,asm} |
自动递归扫描 |
| 入口 | src/main.cpp |
有这个文件就推断为 bin 目标 |
| 库根 | src/<pkg-tail>.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 自动发现 |
| 依赖命名空间 | mcpp(默认) |
平铺写法走默认 ns |
旧配置仍可读取:
[language]
standard = "c++26"新项目请使用 [package].standard。如果两个位置都出现,[package].standard 是权威配置。