Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1ef03f9
docs: 0.0.97 issue triage + architectural remediation implementation …
Sunrisepeak Jul 18, 2026
991dc2a
docs(plan): lock 4 decisions (G=1 commit, R6=default, A=token-list, s…
Sunrisepeak Jul 18, 2026
33e6d9a
perf(scanner): bound glob walk to literal prefix + exclude vcs/build …
Sunrisepeak Jul 18, 2026
10edb40
docs(plan): C1 (簇E #225) done — scanner scope + run cache
Sunrisepeak Jul 18, 2026
2230872
feat(manifest): support [[build.flags]] array-of-tables + glob brace …
Sunrisepeak Jul 18, 2026
69b8b33
docs(plan): C2 (簇B #227 #228) done — manifest syntax + AOT guard
Sunrisepeak Jul 18, 2026
b71d93c
fix(flags): rewrite full -I/-iquote/-isystem include family to projec…
Sunrisepeak Jul 18, 2026
4189c82
docs(plan): C3 (簇A #226 #234) done — flag model + MSVC include_dirs fix
Sunrisepeak Jul 18, 2026
d6b3b6c
fix(build): evaluate cfg-conditional sources for every package via on…
Sunrisepeak Jul 18, 2026
3b4f18e
fix(build): mirror source relative path in object paths + track compi…
Sunrisepeak Jul 18, 2026
f5ed2e0
docs(plan): C4 (簇C) done on α + C5 (簇D) done on β
Sunrisepeak Jul 18, 2026
482b428
fix(nasm): provision nasm through the toolchain's synchronous resolve…
Sunrisepeak Jul 18, 2026
c044f8e
feat(workspace): root-anchored path/index inheritance + default-names…
Sunrisepeak Jul 18, 2026
69e38ee
docs(plan): C6+C7 (簇F/簇G) done on β — β track complete
Sunrisepeak Jul 18, 2026
c7a2b61
merge(β): 簇D/F/G into 0.0.97 remediation — cfg funnel #229, nasm gate…
Sunrisepeak Jul 18, 2026
964f9d0
chore(release): bump mcpp 0.0.96 -> 0.0.97 (scanner scope, flag model…
Sunrisepeak Jul 18, 2026
f0c19fb
docs(plan): β→α merged, C8 released (0.0.97), verification green
Sunrisepeak Jul 18, 2026
d3ad223
fix(flags): do not re-quote mcpp-generated link flags — restore rpath…
Sunrisepeak Jul 18, 2026
179f83c
docs(plan): full e2e passed (rpath regression fixed), final review READY
Sunrisepeak Jul 18, 2026
3340e3c
fix(flags): shell-quote only define-values, pass raw/link flags verbatim
Sunrisepeak Jul 18, 2026
24fb8d4
test(ninja): make 3 emission-format tests host-aware (fix windows CI)
Sunrisepeak Jul 18, 2026
78bb1c9
fix(build): scope #235 compile-edge depfile to GCC; e2e 118 requires gcc
Sunrisepeak Jul 18, 2026
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.97] — 2026-07-18

> 架构级修复批次(单 PR,逐簇 commit):ffmpeg-m/opencv-m 全源码直编暴露的第二层缺口 + workspace 测试基建语法空洞。

### 新增

- **`[[build.flags]]` 数组表写法**(#227):`[build].flags` 现同时接受 TOML 标准数组表 `[[build.flags]]` 与内联表数组两种等价写法(声明顺序=应用顺序不变),长 per-glob flags 条目不再挤单行。纯解析层扩展(自研 TOML parser 补全 array-of-tables);并加清单层 closed-grammar 守卫——非白名单段落误写 `[[x]]`(如 `[[dependencies]]` 手滑)现**硬报错**而非静默丢数据。
- **glob 花括号交替 `{a,b}`**(#228):sources/flags glob 支持 `libavcodec/{aac,bsf,hevc}/**` 笛卡尔展开(嵌套/多组均可),vendored 大库 per-glob 声明不再重复。
- **默认命名空间索引重定向**(R6):`[indices] default = { path = "..." }`(亦接受空引号键 `""`)可把默认命名空间(`namespace = ""` 的模块包)指向本地 checkout,补上此前只能重定向具名命名空间的语法空洞——使 index 仓能以 `mcpp test --workspace` 声明式验证 imgui/ffmpeg/opencv 等模块包,替代逐包 smoke shell。(`url` 形式的默认命名空间重定向暂不支持,解析期显式报错而非静默失效。)
- **`mcpp run -p <member>`**:run 命令支持选择 workspace 成员并运行其二进制(与 `build`/`test` 的 `-p` 对齐)。

### 修复

- **源发现全树遍历致 `mcpp run` 前慢 + 缓存复用**(#225):glob 遍历改从字面前缀起(`src/**` 从 `src/` 起走,不再从项目根扫全树),并排除 `.git`/`target`/git 子模块边界;`mcpp run` 复用 `mcpp build` 已解析的缓存,不再每次重扫大子模块(此前含大 `compat/` 子模块时每次 ~9.5s)。
- **相对 include 族 flag 未按项目根重写 + 含空格值未转义**(#226 #234):`-iquote`/`-isystem`/`-idirafter`/`-iprefix`/`-L` 现与 `-I` 一样按项目根绝对化(joined 与 separated 两拼写);`defines = ["T=long long"]` 等含空格值发射时 shell 引号保护,不再被拆成孤立参数。含 `[build] include_dirs` 在 MSVC 方言下的相对路径绝对化亦一并修正。
- **对象路径按父目录名折叠致同名源冲突**(#233):不同目录同名源(`a/src/util.cpp` vs `b/src/util.cpp`,OpenCV/LLVM 式 `modules/<mod>/src/*` 布局)对象路径改按碰撞时镜像源**相对路径**消歧 + 构建后唯一性断言;非碰撞项路径字节不变。
- **模块 purview 内文本 `#include` 改动不触发重编**(#235):编译边补 depfile 追踪(过滤 GCC `-fmodules` 注入的反向规则以规避 ninja `inputs may not also have inputs`),purview/GMF/普通头改动均正确触发重编——顺带根治此前非 MSVC 下普通头改动也不重编的潜伏问题。
- **依赖包 cfg 条件 sources 被消费时不展开**(#229):path/git 依赖的 `[target.'cfg(...)'.build].sources` 现与 root/version-dep 同样按已解析 target 求值(`mcpp build` 与 `mcpp test` 双路径),不再 undefined reference(与 #218 同类,收敛为统一 per-package 求值)。
- **nasm 冷环境惰性自举时序**(#232):nasm 供给改走工具链同款同步门 `Fetcher::resolve_xpkg_path("xim:nasm@3.02", autoInstall=true)`(索引刷新前置 + 硬报错 + payload 校验),不再先判死后台补装;config 自举错误不再被 `if(cfg)` 门吞成误导性的 "no usable nasm"。
- **workspace 根配置无法一次声明全员可用**(#224):`[workspace.dependencies]` 的 path 依赖可被成员经 `.workspace = true` 继承;根 `[indices]` 的相对 `path` 按 **workspace 根**解析(而非消费成员目录),成员不再需重复声明各自 `../` 相对路径。

### 备注

- #230(windows workspace 崩溃)已于 0.0.96 修复。#215(cppfly Clang 反射行)待上游 Clang 落地 P2996,不排期。

## [0.0.96] — 2026-07-18

### 修复
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.96"
version = "0.0.97"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
367 changes: 295 additions & 72 deletions src/build/execute.cppm

Large diffs are not rendered by default.

74 changes: 70 additions & 4 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export module mcpp.build.flags;

import std;
import mcpp.build.plan;
import mcpp.modgraph.scanner;
import mcpp.platform;
import mcpp.toolchain.clang;
import mcpp.toolchain.detect;
Expand Down Expand Up @@ -66,6 +67,21 @@ CompileFlags compute_flags(const BuildPlan& plan);
std::string atomic_link_flag(const std::vector<std::filesystem::path>& linkDirs,
bool staticLink);

// mcpp#234: quote a single flag-vector token for safe embedding in a shell
// command line. Every element of a flags `vector<string>` is already one
// argv token (e.g. `apply_glob_flags` pushes `"-D" + d`, so a define like
// `T=long long` arrives as the single element `-DT=long long`) — but the
// emission choke points (`join_flags` in ninja_backend.cppm, and the global
// blob assembly below) historically joined tokens with a bare space and no
// quoting, so a token containing a space silently split into two shell
// words once ninja handed the resolved command line to the shell. Only
// tokens that actually contain whitespace or a shell-significant character
// are quoted — plain framework flags (`-std=c++23`, `-O2`, `-I/abs/path`)
// come back unchanged, byte-for-byte. POSIX: wrap in single quotes (embedded
// `'` escaped as `'\''`). Windows: wrap in double quotes (embedded `"`
// escaped as `\"`) — cmd.exe/CreateProcess argv convention.
std::string shell_quote_arg(std::string_view arg);

} // namespace mcpp::build

namespace mcpp::build {
Expand Down Expand Up @@ -123,6 +139,39 @@ std::string atomic_link_flag(const std::vector<std::filesystem::path>& linkDirs,
return {};
}

std::string shell_quote_arg(std::string_view arg) {
// Characters that split/alter a word when unquoted in POSIX sh or
// cmd.exe: whitespace plus the common shell metacharacters. Anything
// NOT in this set (e.g. `-std=c++23`, `-O2`, `-I/abs/path`, `-DFOO=1`)
// returns untouched — no quoting where none is needed.
constexpr std::string_view kNeedsQuote = " \t\n\"'\\$`;&|<>()*?[]#~!{}";
if (arg.find_first_of(kNeedsQuote) == std::string_view::npos)
return std::string(arg);

if constexpr (mcpp::platform::is_windows) {
// cmd.exe / CreateProcess argv convention: wrap in double quotes,
// escape embedded `"` as `\"`.
std::string out = "\"";
for (char c : arg) {
if (c == '"') out += "\\\"";
else out.push_back(c);
}
out += "\"";
return out;
} else {
// POSIX sh: wrap in single quotes (nothing is special inside single
// quotes except `'` itself), escaping an embedded `'` as `'\''`
// (close quote, literal quote, reopen quote).
std::string out = "'";
for (char c : arg) {
if (c == '\'') out += "'\\''";
else out.push_back(c);
}
out += "'";
return out;
}
}

CompileFlags compute_flags(const BuildPlan& plan) {
CompileFlags f;

Expand Down Expand Up @@ -157,11 +206,28 @@ CompileFlags compute_flags(const BuildPlan& plan) {
}
std::string pic_flag = (need_pic && !isMsvcDialect) ? " -fPIC" : "";

// Include dirs
std::string include_flags;
// Include dirs — this is the TYPED PATH channel (bare paths from the
// manifest; the dialect prefix is applied here at emission), not the
// FLAG-STRING channel that `normalize_include_flags` serves (cflags/
// cxxflags, where the -I/-iquote/... prefix is already embedded in the
// string by the scanner). `normalize_include_flags`'s prefix table only
// knows GNU spellings, so routing dialect-prefixed tokens through it
// silently no-ops under MSVC (`/Iinclude` matches nothing and is never
// rewritten against plan.projectRoot — but ninja runs with cwd = output
// dir, so a relative include dir stops resolving). Absolutize the path
// directly instead (dialect-agnostic), then prepend the prefix, then
// ninja-$-escape and shell-quote per token (#234) so an include dir
// whose name contains a space can't silently split into two shell words
// once ninja hands the resolved command line to the shell.
std::vector<std::string> includeTokens;
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);
include_flags += std::format(" {}{}", d.includePrefix, escape_path(abs));
std::filesystem::path p = inc.has_root_path() ? inc : (plan.projectRoot / inc);
includeTokens.push_back(std::string(d.includePrefix) + p.string());
}
std::string include_flags;
for (auto& t : includeTokens) {
include_flags += ' ';
include_flags += shell_quote_arg(escape_path(std::filesystem::path(t)));
}

// Sysroot / payload paths — resolved ONCE by the toolchain link model
Expand Down
116 changes: 105 additions & 11 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,33 @@ std::string local_include_flags(const CompileUnit& cu) {
}

std::string join_flags(const std::vector<std::string>& flags) {
// mcpp#234: a manifest `defines = ["T=long long"]` arrives as the single
// element `-DT=long long` (pushed whole by apply_glob_flags) — a space
// that is genuinely PART of one argv token. Joining with a bare space let
// the shell split it into two words (`-DT=long` + `long`) once ninja
// handed the command line to the shell, so a define value with a space
// must be shell-quoted.
//
// But quote ONLY that case. Every OTHER flag element is passed through
// verbatim, because two other kinds of element legitimately contain a
// space that MUST stay a token boundary, and quoting them breaks the build:
// • raw descriptor flags pack multiple argv tokens into one string
// (compat.lua's `-include mcpp_lua_platform_config.h` — the space
// separates `-include` from its argument; quoting made gcc see one
// malformed arg → "No such file"; broke aarch64/macos/windows builds);
// • mcpp-generated link flags are already shell-quoted + ninja-escaped
// (`-Wl,-rpath,'$$ORIGIN'` — single quotes stop shell $-expansion,
// `$$` is ninja's literal `$`); re-quoting baked a literal `'$ORIGIN'`
// into RUNPATH so dependency .so's next to the exe couldn't resolve.
// The `defines` channel is the ONLY producer of a `-D`/`/D`-prefixed token
// whose space is intra-value, so gate the quoting on exactly that shape.
std::string out;
for (auto const& flag : flags) {
out += ' ';
out += flag;
const bool defineWithSpace =
(flag.starts_with("-D") || flag.starts_with("/D"))
&& flag.find(' ') != std::string::npos;
out += defineWithSpace ? shell_quote_arg(flag) : flag;
}
return out;
}
Expand Down Expand Up @@ -396,6 +419,69 @@ std::string emit_ninja_string(const BuildPlan& plan) {
auto append_deps = [&] {
if (msvcDeps) append(" deps = msvc\n");
};
// mcpp#235: cxx_module/cxx_object had NO depfile at all on non-MSVC —
// only the msvcDeps branch tracked header deps (via /showIncludes). So
// editing a file #include'd inside a module's purview (or a plain
// header pulled in by a .cpp) never invalidated the compile edge: the
// P1689 scan already emits a `.dep`/`.d`-shaped list of textual
// includes, but it was generated and discarded.
//
// Naively mirroring the nasm rule below (`-MD $out.d` + `deps = gcc` +
// `depfile = $out.d`) does NOT work here: GCC's `-fmodules` bolts extra
// "reversed" rules onto ANY -M*/-MF depfile for a TU that imports or
// provides a module — e.g. for a module interface unit:
// obj/m.m.o gcm.cache/m.gcm: src/m.cppm src/vals.inc
// m.c++-module: gcm.cache/m.gcm
// .PHONY: m.c++-module
// gcm.cache/m.gcm:| obj/m.m.o
// and for an importing TU:
// obj/main.o: src/main.cpp gcm.cache/std.gcm gcm.cache/m.gcm
// obj/main.o: m.c++-module std.c++-module
// CXX_IMPORTS += m.c++-module std.c++-module
// Those extra records describe a Make-style dependency graph WITHIN the
// depfile itself (e.g. gcm.cache/m.gcm "having its own inputs"), which
// collides with gcm.cache/m.gcm already being a declared ninja-graph
// OUTPUT of this same edge (`| gcm.cache/m.gcm`) — ninja's depfile
// loader rejects that outright ("inputs may not also have inputs"),
// confirmed empirically (e2e 118 failed the fresh build this way before
// the filter below was added). GCC has no flag to suppress this.
//
// Fix: compile to a scratch `$out.d.raw`, then keep only the FIRST
// record (target + its indented continuation lines — the plain textual
// #include graph, which is all #235 needs) as `$out.d`; module BMI
// deps stay tracked independently via the existing per-edge `dyndep`
// binding, so dropping the reversed/module lines here loses nothing.
// POSIX-only (`awk`): native Windows has no POSIX shell/toolset here
// (see the existing "Windows: skip BMI restat optimization" branch
// below), so a non-MSVC Windows toolchain keeps the pre-#235
// behavior (no depfile) rather than depend on an unavailable filter —
// msvcDeps (cl.exe) is unaffected either way (deps=msvc, no -MMD).
//
// GCC-only: the awk filter strips the "reversed" make-rules that GCC's
// `-fmodules -MMD` bolts onto a module-TU depfile (`gcm.cache/<m>.gcm: |
// <obj>` etc., which ninja rejects as "inputs may not also have inputs").
// Clang's module system (.pcm) does not emit those and its module-TU
// depfile shape is different; applying the GCC-shaped filter there is
// untested and could yield a wrong dep set. Until Clang is verified,
// scope this to GCC — Clang keeps the pre-#235 behavior (no compile-edge
// depfile; header/purview rebuild tracking on Clang is a follow-up, same
// as it was on 0.0.96, so this is not a regression). e2e 118 declares
// `# requires: gcc` and skips where GCC is not the toolchain.
const bool posixDepfile = !msvcDeps && !mcpp::platform::is_windows
&& plan.toolchain.compiler == mcpp::toolchain::CompilerId::GCC;
const std::string mmd_flag = posixDepfile ? "-MMD -MF $out.d.raw " : "";
const std::string mmd_filter = posixDepfile
? " && awk 'NR==1{print;next} /^[^ ]/{exit} {print}' "
"\"$out.d.raw\" > \"$out.d\" && rm -f \"$out.d.raw\""
: "";
auto append_cxx_deps = [&] {
if (posixDepfile) {
append(" deps = gcc\n");
append(" depfile = $out.d\n");
} else {
append_deps();
}
};
// cl.exe needs /TP (our module interfaces are .cppm, unknown to cl) and
// /interface to treat the TU as a module interface unit.
const std::string module_src_flags = msvcDeps ? " /interface /TP" : "";
Expand All @@ -405,31 +491,38 @@ std::string emit_ninja_string(const BuildPlan& plan) {
append(std::format(" command = "
"$cxx $local_includes $cxxflags $unit_cxxflags{}{} {}\n",
module_output_flag, module_src_flags, compile_tail));
append_deps();
append_cxx_deps();
} else {
append(std::format(" command = "
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
"fi && "
"$cxx $local_includes $cxxflags $unit_cxxflags{} {} && "
"$cxx $local_includes $cxxflags $unit_cxxflags{} {}{}{} && "
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
"else "
"rm -f \"$bmi_out.bak\"; "
"fi\n", module_output_flag, compile_tail));
"fi\n", module_output_flag, mmd_flag, compile_tail, mmd_filter));
append_cxx_deps();
}
append(" description = MOD $out\n");
if (dyndep)
append(" restat = 1\n");
append("\n");

append("rule cxx_object\n");
append(std::format(
" command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n",
compile_tail));
if constexpr (mcpp::platform::is_windows) {
append(std::format(
" command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n",
compile_tail));
} else {
append(std::format(
" command = $cxx $local_includes $cxxflags $unit_cxxflags {}{}{}\n",
mmd_flag, compile_tail, mmd_filter));
}
append(" description = OBJ $out\n");
append_deps();
append_cxx_deps();
if (dyndep)
append(" restat = 1\n");
append("\n");
Expand Down Expand Up @@ -606,9 +699,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// ── Phase 1: scan edges (one .ddi per TU). ──────────────────────
// .ddi is placed beside the object so multi-version mangling can
// namespace by package without producing two `build` rules with
// the same `.ddi` output (plan.cppm switches `cu.object` from
// `obj/<file>.o` to `obj/<pkg>/<file>.o` whenever a basename
// collides across packages — `.ddi` follows that placement).
// the same `.ddi` output (mcpp#233: plan.cppm switches `cu.object`
// from the flat `obj/<file>.o` to a path mirroring the source's
// relative directory under a sanitized-package prefix whenever a
// basename collides — `.ddi` follows that placement).
// Skip .c files: they have no `import`s and don't need P1689 scan;
// running them through cxx_scan would route them through g++ /
// -fmodules which is exactly what C support is here to avoid.
Expand Down
Loading
Loading