Skip to content

Commit 37eaf6f

Browse files
committed
fix(build): stage BMIs through mcpp instead of an in-place shell copy (#311)
Windows-only symptom, three overlapping defects. 1. `rule cp_bmi` overwrote the destination IN PLACE (`powershell Copy-Item -Force` / `cp -f`). mcpp writes the staged std BMI path into compile_commands.json so clangd can resolve `import std;`, clangd memory-maps that very file, and Windows refuses to replace a file with an open user-mapped section — error 1224, reported as a bare "build failed". Staging now runs through `mcpp stage` (new internal subcommand, same shape as `mcpp dyndep`): skip when the destination is already equivalent, else temp-file + rename, else in-place, with retries, and a failure message that names the file and the likely holder. Never downgraded to a warning — a stale BMI turns into a confusing "module 'std' not found" or a silently mismatched link. The rule is shared with Windows runtime-DLL deployment, which had the same hazard against a program still running from a previous `mcpp run`. 2. `default_cache_root()` was a private copy of the home resolution, unchanged since v0.0.1: no %USERPROFILE% branch, no self-contained detection. On Windows PowerShell (no $HOME) the std BMI cache landed in the *current working directory* as `.mcpp-bmi/` while dep BMIs went to %USERPROFILE%\.mcpp\bmi — two roots for one cache, the second cwd-dependent, which is what made a re-stage a routine event. New leaf module `mcpp.home` is now the single resolver (config, stdmod, prepare's git cache and two more copies in doctor all route through it). 3. The staging rule carried no `restat`, so any re-stage recompiled everything that imports std even when the bytes were identical. Adding `restat = 1` on top of the no-write path fixes that — measured: touching the cache-side BMI now runs the stage edge alone, and the next build is "no work to do". Note for the record: aligning the destination's mtime with the source defeats restat and re-triggers the cascade, so a skipped stage touches no timestamps at all. Also: `FAILED: <target>` survives the ninja output filter (normalized to `failed: <target>`) — dropping it is why the report couldn't tell a staging failure from a compile error; `mcpp new` ignores `.mcpp/`; `mcpp doctor` points at a leftover `.mcpp-bmi/`. Design + plan: .agents/docs/2026-07-30-issue311-*.md Tests: unit test_home (4), test_build_stage (10), test_ninja_backend (+4), e2e 170 (no-cascade + cache root), e2e 171 (held destination, PowerShell MemoryMappedFile on Windows — reproduces #311 without needing clangd).
1 parent c765e83 commit 37eaf6f

18 files changed

Lines changed: 1575 additions & 109 deletions

.agents/docs/2026-07-30-issue311-bmi-staging-and-cache-root-design.md

Lines changed: 361 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# BMI staging 原语 + BMI 缓存根收敛 — 实施计划
2+
3+
配套设计:`2026-07-30-issue311-bmi-staging-and-cache-root-design.md`
4+
关联 issue:#311
5+
建议目标版本:**2026.7.30.1**(常规迭代)
6+
7+
单 PR 交付。阶段有依赖顺序:**P1 → P2 → P3**(P3 的 ninja 文本依赖 P2 的子命令存在,
8+
P2 的判据依赖 P1 收敛后的缓存根不再随 cwd 漂移)。P4/P5/P6 可与 P3 并行收尾。
9+
10+
---
11+
12+
## P1 — 单一 home 解析器(`mcpp.home`
13+
14+
**新建** `src/home.cppm`
15+
16+
```cpp
17+
export module mcpp.home;
18+
import std;
19+
import mcpp.platform;
20+
21+
export namespace mcpp::home {
22+
std::filesystem::path root(); // MCPP_HOME
23+
std::filesystem::path bmi_root(); // root()/bmi
24+
}
25+
```
26+
27+
- `root()` = 把 `src/config.cppm:313-353` 的 `default_mcpp_home()` + `home_dir()`
28+
**逐字搬迁**(含 `USERPROFILE` 分支、self-contained 探测、`target/` 与 `data/xpkgs/`
29+
两条 disqualify)。两个函数都是纯函数、无副作用,搬迁是安全的。
30+
- `bmi_root()` = `root() / "bmi"`。
31+
32+
**改动点(4 处调用方 + 3 处旧实现)**:
33+
34+
| 文件 | 位置 | 改动 |
35+
|---|---|---|
36+
| `src/config.cppm` | :313-353 | 删除两个本地函数 |
37+
| `src/config.cppm` | :500 / :510 | `cfg.mcppHome = mcpp::home::root()`;`cfg.bmiCacheDir = mcpp::home::bmi_root()` |
38+
| `src/toolchain/stdmod.cppm` | :179-187 | 整体替换为 `return mcpp::home::bmi_root();`(新增 `import mcpp.home;`) |
39+
| `src/build/prepare.cppm` | :2826-2832 | 内联 lambda → `mcpp::home::root()` |
40+
41+
**编译系统**:`mcpp.toml` 若显式列源文件需同步;当前是 `src/**` 推导(`mcpp build -v`
42+
里 `Inferred sources`),无需改。
43+
44+
**无环性核对**(改前必须自查一次):`mcpp.home` 只 import `std` + `mcpp.platform`。
45+
`mcpp.config` 的闭包内无 `mcpp.toolchain.*`,故 `mcpp.toolchain.stdmod → mcpp.home` 不成环。
46+
47+
**测试**(`tests/unit/test_config.cpp` 扩写,或新建 `tests/unit/test_home.cpp`):
48+
49+
| 用例 | 断言 |
50+
|---|---|
51+
| `MCPP_HOME` 优先 | 设环境变量 → `root()` == 该值;`bmi_root()` == `<it>/bmi` |
52+
| Windows 无 HOME | `is_windows` 下 `USERPROFILE` 生效(用 `if constexpr` 分支或跳过非 Windows) |
53+
| 兜底形态 | 两个变量都不设时,路径以 `.mcpp` 结尾、**不再**以 `.mcpp-bmi` 结尾 |
54+
| 与 config 一致 | `load()` 后 `cfg.bmiCacheDir == mcpp::home::bmi_root()`(同一进程同一环境下必须相等) |
55+
56+
最后一条是这次的核心不变量,**必须机器校验**——它就是 D2 的回归闸。
57+
58+
---
59+
60+
## P2 — `mcpp stage` 子命令
61+
62+
**新建** `src/build/stage.cppm`(`export module mcpp.build.stage;`),承载纯逻辑:
63+
64+
```cpp
65+
struct StageResult { bool copied; }; // copied=false ⇒ 判等跳过
66+
struct StageError { std::string message; }; // 已含 hint 文案
67+
std::expected<StageResult, StageError> stage_file(
68+
const std::filesystem::path& src,
69+
const std::filesystem::path& dst,
70+
bool verify_hash);
71+
```
72+
73+
实现按设计 §S1 语义表:
74+
75+
1. `src` 不存在 → error。
76+
2. `create_directories(dst.parent_path())`
77+
3. `dst` 存在 && `file_size` 相等 && (verify == Size || 逐字节相等)
78+
**不写字节、不碰时间戳**`StageOutcome{.copied = false}`
79+
(对齐 mtime 会让 `restat` 失效并重新引发级联——实测过,见设计 §S1 的表)
80+
4. 否则:`copy_file(src, dst.tmp.<pid>)``rename(tmp, dst)`
81+
5. rename 失败 → `copy_file(src, dst, overwrite_existing)`
82+
6. 4/5 均失败 → 睡 100 / 300 / 900 ms 重试整个 4-5 序列,共 3 轮。
83+
7. 仍失败 → `StageError`,文案照设计 §S4 的模板(file / from / os error / hint 四段,
84+
hint 必须点名 clangd 与 `compile_commands.json` 的因果)。
85+
8. 每次退出前清理残留 `dst.tmp.<pid>`
86+
87+
`Verify::Content``MCPP_STAGE_VERIFY=content``--verify content` 打开;实现是分块
88+
逐字节比较(同 I/O 成本、无碰撞面、可提前退出),不引入 hash 依赖。
89+
90+
**CLI 接线**(照 `dyndep` 的形状):
91+
92+
| 文件 | 改动 |
93+
|---|---|
94+
| `src/cli/cmd_build.cppm` | 新增 `export int cmd_stage(const ParsedArgs&)`,就近放在 `cmd_dyndep`(:187)旁 |
95+
| `src/cli.cppm` | :496 附近新增 `.subcommand(cl::App("stage") ...)`,描述以 `(internal: invoked by ninja)` 开头,选项 `--output/-o``--verify``.action(wrap_rc(cmd_stage))` |
96+
| `src/cli.cppm` | :547-551 的 `known` 白名单加 `"stage"`**数组长度 22 → 23**(写死的模板实参,漏改即编译失败/静默拒命令) |
97+
98+
**测试**(新建 `tests/unit/test_build_stage.cpp`):
99+
100+
| 用例 | 断言 |
101+
|---|---|
102+
| 目标不存在 | 复制发生,`copied == true`,内容一致 |
103+
| 目标已存在且等长等内容 | `copied == false`,且**目标 mtime 一点没变**(不是"未变成 now",是完全不变) |
104+
| 等长但内容不同 + `Verify::Content` | `copied == true` |
105+
| 等长但内容不同 + `Verify::Size` | `copied == false`(这是**有意的** fp 判据取舍,注释写清) |
106+
| `src` 缺失 | error,message 含 src 路径 |
107+
| 目标目录不存在 | 自动创建 |
108+
| 只读目标(POSIX `chmod 444`| 走 rename 分支成功;断言最终内容正确 |
109+
| 错误文案 | 人为构造失败(目标是个目录)→ message 含 `clangd``hint:` |
110+
111+
---
112+
113+
## P3 — ninja 后端切到新 rule
114+
115+
**文件**`src/build/ninja_backend.cppm`
116+
117+
1. **`mcpp` 变量提取**:把 :404 的
118+
`append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path())))`
119+
移出 `if (dyndep)`(:403),改为无条件绑定;`scan_deps` 仍留在 `if (dyndep)` 内。
120+
2. **rule 重写**(:411-419),跨平台单一形态、不再分叉 PowerShell/`cp`
121+
122+
```
123+
rule stage_file
124+
command = $mcpp stage --output $out $in
125+
description = STAGE $out
126+
restat = 1
127+
```
128+
129+
3. **rule 更名**`cp_bmi``stage_file`,四处 staging edge(:793/:795/:807/:810)与
130+
DLL 部署(:1080)同步改名。
131+
4. `command_prefixes()`(:278-291)追加 `mcpp_exe_path()`
132+
5. `filter_ninja_output()`(:323-345):`FAILED:` 不再整行丢弃,改为归一成
133+
`failed: <target>` 保留。
134+
135+
**文件**`src/build/execute.cppm`
136+
137+
6. `read_ninja_command_prefixes()`(:181-205)白名单 key 加 `"mcpp"`
138+
139+
**测试**`tests/unit/test_ninja_backend.cpp`):
140+
141+
| 用例 | 断言 |
142+
|---|---|
143+
| rule 文本 |`rule stage_file``$mcpp stage --output $out $in``restat = 1`**不含** `Copy-Item``cp -f` |
144+
| `mcpp` 绑定 | dyndep 开/关两种 plan 下都出现 `mcpp = `|
145+
| staging edge | 四条 edge 的 rule 名是 `stage_file``std.compat` 仍有 `| pcm.cache/std.pcm` order-only 前置 |
146+
| DLL 部署 | `runtimeDeployFiles` 非空时用同一 rule 名 |
147+
| 过滤器 | `filter_ninja_output` 对含 `<mcpp路径> stage ...` 的回显行过滤掉、对 `failed:``hint:` 正文保留 |
148+
149+
---
150+
151+
## P4 — 兜底路径的可见性
152+
153+
| 文件 | 改动 |
154+
|---|---|
155+
| `src/scaffold/create.cppm` | :284-287 的 `.gitignore` 模板:`target/` + `.mcpp/` |
156+
| `src/doctor.cppm` | :155/:192 附近:若 cwd 或工程根存在 `.mcpp-bmi/`,输出一行 `legacy BMI cache at <path> — safe to delete`**不自动删**|
157+
158+
`.gitignore` 模板变更需同步 e2e 中断言过 scaffold 产物的用例(`grep -rn "gitignore" tests/e2e`
159+
先扫一遍)。
160+
161+
---
162+
163+
## P5 — e2e
164+
165+
**新建 `tests/e2e/170_bmi_staging_no_cascade.sh`**(全平台跑,锁 D3 + S1 步 2):
166+
167+
1. `mcpp new` 一个 bin 工程 → `mcpp build`(产出 staged BMI);
168+
2.`mcpp build -v` 的 STAGE 行或 `build.ninja` 解析出 staging edge 的 `$in`
169+
**不要**`awk '{print $NF}'` 直接切 `rule` 行——本次调查里就踩过,取到的是 `cp_bmi`
170+
这个字面量;正确做法是匹配 `^build .*: (stage_file|cp_bmi) ` 的行再取最后一个字段);
171+
3. `touch "$in"` 让 edge 变脏;
172+
4. `mcpp build -v` 断言:
173+
- 退出 0;
174+
- 输出**不含** `src/main.cpp` 的编译行(反级联,今天会失败);
175+
- staged BMI 的内容与 `$in` 一致。
176+
177+
**新建 `tests/e2e/171_bmi_staging_locked_dest.sh`**
178+
179+
- **Windows 分支**`case "$(uname -s)" in *NT*|MINGW*|MSYS*)`):用 PowerShell 在子进程里
180+
映射住 staged BMI,**不依赖 clangd**
181+
182+
```powershell
183+
$f = [System.IO.MemoryMappedFiles.MemoryMappedFile]::CreateFromFile(
184+
$path, [System.IO.FileMode]::Open)
185+
Start-Sleep -Seconds 30 # 持有期覆盖被测构建
186+
```
187+
188+
然后 `touch` 缓存侧 BMI(保持内容不变)→ `mcpp build` 必须**成功**(走判等跳过)。
189+
这就是 #311 的最小复现,且不需要装 clangd。
190+
- **POSIX 分支**`chmod 444` staged BMI + 让缓存侧内容真的不同(改用另一个 fingerprint 的
191+
BMI 或人为构造一份等长-不同内容的假文件)→ 断言走 rename 分支成功。
192+
- 负例(两个平台):把 staged BMI 换成一个**目录**同名占位 → 断言构建失败且 stderr 含
193+
`hint:``clangd`
194+
195+
`tests/e2e/run_all.sh` 若是显式清单则登记两个新脚本;编号接 169(上游 169 已被 semver 用例占用)。
196+
197+
---
198+
199+
## P6 — 收尾
200+
201+
1. `CHANGELOG.md`
202+
- fix(#311):Windows 上被 clangd 映射的 std BMI 不再让构建失败;
203+
- **behavior change**:BMI 缓存根统一为 `$MCPP_HOME/bmi`(Windows 从 `<cwd>\.mcpp-bmi`
204+
self-contained 安装从 `~/.mcpp/bmi` 迁走);首次构建会重编一次 std(10–60 s),
205+
遗留目录可手动删除。
206+
2. 版本号:`mcpp.toml``2026.7.30.1`(注意 `git status``mcpp.toml` 已有本地改动,
207+
提交前先核对那处改动是否该一起进)。
208+
3. 发布闭环按既有 runbook 走(release → 镜像 xlings-res 双端 → xim-pkgindex → 真装验证 →
209+
bootstrap pin)。**bootstrap pin 与本次发布版本是两组,不要一起 bump**
210+
4. 不要在 issue #311 下评论(按本次任务要求);发布后再回复。
211+
212+
---
213+
214+
## 自查清单(提交前逐条打勾)
215+
216+
- [ ] `grep -rn "\.mcpp-bmi" src/` 只剩 doctor 的遗留提示与注释,无路径构造
217+
- [ ] `grep -rn "getenv(\"HOME\")" src/` 不再出现在 BMI/home 解析路径上
218+
- [ ] `grep -rn "Copy-Item" src/` 归零
219+
- [ ] `grep -rn "cp_bmi" src/` 归零
220+
- [ ] `cli.cppm``known` 数组长度与元素数一致(22 → 23)
221+
- [ ] `if (dyndep)` 之外能拿到 `$mcpp`(用 GCC 非 dyndep plan 生成一次 build.ninja 目视核对)
222+
- [ ] 单测全绿 + `tests/e2e/170``171` 全绿(Linux 本机 + Windows CI)
223+
- [ ] Windows CI 上确认 STAGE 行不再 spawn PowerShell(顺带的启动开销收益)

src/build/execute.cppm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,11 @@ std::vector<std::string> read_ninja_command_prefixes(const std::filesystem::path
190190
auto key = line.substr(0, eq);
191191
while (!key.empty() && std::isspace(static_cast<unsigned char>(key.back())))
192192
key.pop_back();
193-
if (key != "cxx" && key != "cc" && key != "ar" && key != "scan_deps")
193+
// `mcpp` drives the dyndep + stage_file rules; treating it as a command
194+
// prefix filters the echoed command line while keeping the diagnostic
195+
// mcpp itself printed (#311).
196+
if (key != "cxx" && key != "cc" && key != "ar" && key != "scan_deps"
197+
&& key != "mcpp")
194198
continue;
195199

196200
std::string value = line.substr(eq + 1);

src/build/ninja_backend.cppm

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ std::string escape_ninja_path(const std::filesystem::path& p) {
6767
// a response file that gcc/clang/GNU ar tokenize GNU-style, where
6868
// backslash is an ESCAPE character — `obj\cli.o` would arrive as
6969
// `objcli.o`. Every Windows consumer of these strings (CreateProcess
70-
// path resolution, cl.exe/link.exe, PowerShell Copy-Item, ninja itself)
70+
// path resolution, cl.exe/link.exe, `mcpp stage`, ninja itself)
7171
// accepts forward slashes; POSIX output is byte-identical.
7272
std::string s = p.generic_string();
7373
std::string out;
@@ -288,6 +288,10 @@ std::vector<std::string> command_prefixes(const CompileFlags& flags,
288288
add(flags.ccBinary);
289289
add(flags.arBinary);
290290
add(plan.scanDepsPath);
291+
// mcpp itself drives the dyndep and stage_file rules; its echoed command
292+
// line is noise, while the message it prints on failure is the diagnostic
293+
// we want to keep.
294+
add(mcpp_exe_path());
291295
return prefixes;
292296
}
293297

@@ -331,11 +335,26 @@ std::string filter_ninja_output(std::string_view output,
331335
auto trimmed = ltrim_copy(line);
332336
if (trimmed.starts_with("ninja: Entering directory")
333337
|| trimmed.starts_with("ninja: build stopped")
334-
|| trimmed.starts_with("FAILED:")
335338
|| is_ninja_progress_line(trimmed)
336339
|| is_command_line(trimmed, commandPrefixes)) {
337340
continue;
338341
}
342+
// Keep WHICH output failed. Dropping this line entirely (as we used to)
343+
// is why #311's report couldn't tell a BMI staging failure from a
344+
// compile error. Normalized to lowercase `failed:` so it reads as part
345+
// of mcpp's own diagnostics, and `[code=N]` is dropped as noise.
346+
if (trimmed.starts_with("FAILED:")) {
347+
auto target = ltrim_copy(trimmed.substr(std::string_view("FAILED:").size()));
348+
if (target.starts_with("[code=")) {
349+
if (auto close = target.find(']'); close != std::string::npos)
350+
target = ltrim_copy(target.substr(close + 1));
351+
}
352+
if (target.empty()) continue;
353+
filtered += "failed: ";
354+
filtered += target;
355+
filtered.push_back('\n');
356+
continue;
357+
}
339358
filtered += line;
340359
filtered.push_back('\n');
341360
}
@@ -400,23 +419,28 @@ std::string emit_ninja_string(const BuildPlan& plan) {
400419
flags.ldBinary.empty() ? std::string("link.exe")
401420
: escape_ninja_path(flags.ldBinary)));
402421
}
422+
// `$mcpp` is needed by stage_file in EVERY configuration (dyndep or not),
423+
// so the binding cannot live inside the `if (dyndep)` below.
424+
append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path())));
403425
if (dyndep) {
404-
append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path())));
405426
if (!plan.scanDepsPath.empty()) {
406427
append(std::format("scan_deps = {}\n", escape_ninja_path(plan.scanDepsPath)));
407428
}
408429
}
409430
append("\n");
410431

411-
append("rule cp_bmi\n");
412-
if constexpr (mcpp::platform::is_windows) {
413-
// Use PowerShell Copy-Item which handles both forward and back slashes.
414-
// cmd.exe `copy` breaks on forward-slash paths from ninja.
415-
append(" command = powershell -NoProfile -Command \"Copy-Item -Force '$in' -Destination '$out'\"\n");
416-
} else {
417-
append(" command = mkdir -p $$(dirname $out) && cp -f $in $out\n");
418-
}
419-
append(" description = STAGE $out\n\n");
432+
// Staging (cache → build dir) runs through mcpp itself instead of a
433+
// per-platform shell copy: it skips the write when the destination is
434+
// already equivalent, writes out-of-place + renames when it isn't, retries
435+
// transient sharing violations, and fails with a diagnostic that names the
436+
// likely holder. #311: `Copy-Item -Force` overwrote in place, so a std BMI
437+
// that clangd had memory-mapped failed the whole build with error 1224.
438+
// `restat = 1` is what makes the no-write path actually pay off — a skipped
439+
// stage must not dirty every importer of the staged BMI.
440+
append("rule stage_file\n");
441+
append(" command = $mcpp stage --output $out $in\n");
442+
append(" description = STAGE $out\n");
443+
append(" restat = 1\n\n");
420444

421445
// P1: per-file dyndep rule. Converts one .ddi → .dd independently.
422446
append(std::format(
@@ -790,9 +814,9 @@ std::string emit_ninja_string(const BuildPlan& plan) {
790814

791815
bool has_std_artifacts = !plan.stdBmiPath.empty() && !plan.stdObjectPath.empty();
792816
if (has_std_artifacts) {
793-
append(std::format("build {} : cp_bmi {}\n", escape_ninja_path(std_bmi_dst),
817+
append(std::format("build {} : stage_file {}\n", escape_ninja_path(std_bmi_dst),
794818
escape_ninja_path(plan.stdBmiPath)));
795-
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(std_o_dst),
819+
append(std::format("build {} : stage_file {}\n\n", escape_ninja_path(std_o_dst),
796820
escape_ninja_path(plan.stdObjectPath)));
797821
}
798822

@@ -804,10 +828,10 @@ std::string emit_ninja_string(const BuildPlan& plan) {
804828
if (has_std_compat) {
805829
// std.compat.pcm depends on std.pcm — ensure std.pcm is staged first
806830
// so clang can resolve the transitive dependency when loading std.compat.pcm.
807-
append(std::format("build {} : cp_bmi {} | {}\n", escape_ninja_path(compat_bmi_dst),
831+
append(std::format("build {} : stage_file {} | {}\n", escape_ninja_path(compat_bmi_dst),
808832
escape_ninja_path(plan.stdCompatBmiPath),
809833
escape_ninja_path(std_bmi_dst)));
810-
append(std::format("build {} : cp_bmi {}\n\n", escape_ninja_path(compat_o_dst),
834+
append(std::format("build {} : stage_file {}\n\n", escape_ninja_path(compat_o_dst),
811835
escape_ninja_path(plan.stdCompatObjectPath)));
812836
}
813837

@@ -1074,10 +1098,13 @@ std::string emit_ninja_string(const BuildPlan& plan) {
10741098
append("\n");
10751099

10761100
// Windows runtime-DLL deployment: one copy edge per staged dep DLL. Emitted
1077-
// once (deduped by dest in BuildPlan), reusing the generic cp_bmi copy rule.
1101+
// once (deduped by dest in BuildPlan), reusing the generic stage_file rule
1102+
// — which also means a DLL still loaded by a running program from a
1103+
// previous `mcpp run` gets the skip-if-equivalent treatment instead of a
1104+
// hard "cannot copy" failure.
10781105
// Inert on RPATH platforms where runtimeDeployFiles is empty.
10791106
for (auto const& d : plan.runtimeDeployFiles) {
1080-
append(std::format("build {} : cp_bmi {}\n",
1107+
append(std::format("build {} : stage_file {}\n",
10811108
escape_ninja_path(d.dest),
10821109
escape_ninja_path(d.source)));
10831110
}

src/build/prepare.cppm

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export module mcpp.build.prepare;
1111

1212
import std;
1313
import mcpp.diag;
14+
import mcpp.home;
1415
import mcpp.platform.axis;
1516
import mcpp.libs.json;
1617
import mcpp.log;
@@ -2823,13 +2824,7 @@ prepare_build(bool print_fingerprint,
28232824
// them to a commit before forming the cache key; this lets
28242825
// `mcpp update <dep>` pick up a moved branch without deleting
28252826
// unrelated git caches.
2826-
auto mcppHome = [] {
2827-
if (auto* e = std::getenv("MCPP_HOME"); e && *e)
2828-
return std::filesystem::path(e);
2829-
if (auto* e = std::getenv("HOME"); e && *e)
2830-
return std::filesystem::path(e) / ".mcpp";
2831-
return std::filesystem::current_path() / ".mcpp";
2832-
}();
2827+
auto mcppHome = mcpp::home::root(); // single resolver (#311)
28332828
std::string resolvedGitRev = spec.gitRev;
28342829
if (spec.gitRefKind == "branch") {
28352830
auto ref = std::format("refs/heads/{}", spec.gitRev);

0 commit comments

Comments
 (0)