Skip to content

Commit c545045

Browse files
fix(deps): staging a cross-major copy drops the headers beside its sources (#502)
When two majors of one package are in a graph the resolver stages one copy under `target/.mangled/` with its module declarations rewritten. Staging moves the sources and, by design, leaves headers where they are: those are reached through `[build].include_dirs`, which is absolutized so the staged copy still finds them. A header written BESIDE a source is a different case and was not handled. `#include "detail.h"` resolves relative to the directory of the file holding the directive, so moving the source moves the search — and no `include_dirs` entry is involved, because a package with its header beside its source never declared a path it never needed. target/.mangled/openkal-opensbi/__self__/src/time.cpp:44:10: fatal error: 'sbi.h' file not found ⚠️ Everything that diagnosis points at is wrong. The path is a staging directory the author never wrote, the header sits exactly where the source expects it, and the build that triggered it asked for nothing unusual: two majors of one dependency is a supported arrangement and this is its most ordinary consequence. It cost two repositories a red CI run each, diagnosed first as a missing include path. Every file in a directory that holds a staged source and is not itself staged is now copied across verbatim — rewriting applies to module declarations and a header has none. Directories with no staged source are not visited. Proved by two binaries rather than by one binary and an assumption: mcpp-unfixed 33_multi_version_mangling.sh → fatal error: libB_detail.h mcpp-fixed 33_multi_version_mangling.sh → ok, staged beside the source The fixture gains a private header and a non-module translation unit that includes it, and asserts the staged copy carries it on disk as well as building — a build that found the header by some other path would otherwise pass while leaving this broken. Green beside it: 32, 119, 167, 188. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 784a299 commit c545045

3 files changed

Lines changed: 111 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [未发布]
7+
8+
### 修复
9+
10+
- **暂存跨主版本副本时,与源码相邻的私有头没有跟着走。**
11+
12+
当同一个包的两个主版本同时出现在一张图里,解析器会把其中一份改名暂存到
13+
`target/.mangled/` 下。暂存只搬源码 —— 这对经 `[build].include_dirs`
14+
找到的头是对的,它们靠绝对化后的路径仍指回原处;但对**写在源码旁边**的私有头
15+
不对:`#include "detail.h"` 是相对**包含它的那个文件所在目录**解析的,搬走源码
16+
就搬走了搜索起点。包从来没有声明过这条路径,因为它从来不需要。
17+
18+
```
19+
target/.mangled/openkal-opensbi/__self__/src/time.cpp:44:10:
20+
fatal error: 'sbi.h' file not found
21+
```
22+
23+
⚠️ 这条诊断指向的东西全是错的:路径是作者没写过的暂存目录,头文件就躺在源码
24+
期待的位置,而触发它的构建没有要求任何不寻常的事情 —— 一张图里有两个主版本是
25+
受支持的安排,这是它最普通的后果。
26+
27+
现在,凡是包含了被暂存源码的目录,其中未被暂存的文件一并原样带过去。没有被
28+
暂存源码的目录不会被访问,所以代价与暂存量成正比。
29+
630
## [2026.8.24.5] — 2026-08-25
731

832
### 改进

src/build/prepare.cppm

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3912,9 +3912,30 @@ prepare_build(bool print_fingerprint,
39123912
// version mangling fallback (Level 1) so two cross-major copies of
39133913
// the same package can coexist with distinct module names.
39143914
//
3915-
// Headers (referenced via `[build].include_dirs`) are NOT staged —
3916-
// those keep pointing at the original install dir via absolutized
3917-
// include paths.
3915+
// Headers reached through `[build].include_dirs` are NOT staged — those
3916+
// keep pointing at the original install dir via absolutized include paths.
3917+
//
3918+
// ⭐ HEADERS BESIDE A SOURCE ARE A DIFFERENT CASE, AND THEY ARE STAGED.
3919+
//
3920+
// `#include "detail.h"` is resolved relative to the directory of the file
3921+
// holding the directive, so moving the source moves the search. No
3922+
// `include_dirs` entry is involved and absolutizing one cannot help: the
3923+
// package never declared a path because it never needed one. Measured
3924+
// before this, on a package whose `src/time.cpp` includes `src/sbi.h`:
3925+
//
3926+
// target/.mangled/openkal-opensbi/__self__/src/time.cpp:44:10:
3927+
// fatal error: 'sbi.h' file not found
3928+
//
3929+
// ⚠️ THE DIAGNOSIS THIS PRODUCES POINTS AT THE WRONG THING. The path in it
3930+
// is a staging directory the author never wrote, for a header sitting
3931+
// exactly where the source expects it, and the build that triggered it
3932+
// asked for nothing unusual — two majors of one dependency is a supported
3933+
// arrangement, and this is its most ordinary consequence.
3934+
//
3935+
// What is copied is every file in a directory that contains a staged
3936+
// source and is not itself staged, verbatim: rewriting applies to module
3937+
// declarations, and a header has none. Directories with no staged source
3938+
// are not visited, so this stays proportional to what is being staged.
39183939
auto stage_with_rewrite = [&](const std::filesystem::path& srcRoot,
39193940
const std::filesystem::path& dstRoot,
39203941
const mcpp::manifest::Manifest& depManifest,
@@ -3948,6 +3969,29 @@ prepare_build(bool print_fingerprint,
39483969
"stage: cannot write '{}'", dst.string()));
39493970
os << out;
39503971
}
3972+
3973+
// The files beside those sources, carried across unchanged so a quoted
3974+
// include still finds what it named.
3975+
std::set<std::filesystem::path> sourceDirs;
3976+
for (auto const& f : *sources) sourceDirs.insert(f.parent_path());
3977+
for (auto const& dir : sourceDirs) {
3978+
for (auto const& entry : std::filesystem::directory_iterator(dir, ec)) {
3979+
if (ec) break;
3980+
if (!entry.is_regular_file()) continue;
3981+
if (sources->contains(entry.path())) continue;
3982+
auto rel = std::filesystem::relative(entry.path(), srcRoot, ec);
3983+
if (ec) continue;
3984+
auto dst = dstRoot / rel;
3985+
std::filesystem::create_directories(dst.parent_path(), ec);
3986+
std::filesystem::copy_file(
3987+
entry.path(), dst,
3988+
std::filesystem::copy_options::overwrite_existing, ec);
3989+
if (ec) return std::unexpected(std::format(
3990+
"stage: cannot copy '{}': {}",
3991+
entry.path().string(), ec.message()));
3992+
}
3993+
ec.clear();
3994+
}
39513995
return {};
39523996
};
39533997

tests/e2e/33_multi_version_mangling.sh

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,39 @@ mkdir -p "$TMP/libB" && cd "$TMP/libB"
6262
cd libB
6363
rm -f src/main.cpp
6464
cat > src/libB.cppm <<'EOF'
65+
module;
66+
// Declared in the global module fragment, so it has the same linkage as its
67+
// definition in libB_impl.cpp — a non-module translation unit. Declaring it
68+
// after `export module` would attach it to libB and leave the definition
69+
// unattached, which is a mistake in a fixture rather than in what it tests.
70+
int libB_impl_v();
6571
export module libB;
6672
import mcpplibs.cmdline; // resolver rewrites this to the mangled secondary
6773
import std;
68-
export int libB_v() { return 2; }
74+
export int libB_v() { return libB_impl_v(); }
75+
EOF
76+
# ⭐ A PRIVATE HEADER BESIDE A SOURCE, WHICH IS WHAT STAGING USED TO DROP.
77+
#
78+
# libB is the copy the resolver stages under `target/.mangled/`, and a quoted
79+
# include is resolved relative to the directory of the file holding it — so
80+
# moving the source moves the search. No `include_dirs` entry is involved and
81+
# absolutizing one cannot help: a package with its header beside its source
82+
# never declared a path because it never needed one.
83+
#
84+
# Measured before the fix, on this fixture and on a real package alike:
85+
#
86+
# .mangled/libB/__self__/src/libB_impl.cpp:1:10:
87+
# fatal error: libB_detail.h: No such file or directory
88+
#
89+
# The failure names a staging directory the author never wrote, for a header
90+
# sitting exactly where the source expects it.
91+
cat > src/libB_detail.h <<'EOF'
92+
#pragma once
93+
inline int libB_detail_v() { return 2; }
94+
EOF
95+
cat > src/libB_impl.cpp <<'EOF'
96+
#include "libB_detail.h"
97+
int libB_impl_v() { return libB_detail_v(); }
6998
EOF
7099
cat > mcpp.toml <<'EOF'
71100
[package]
@@ -122,6 +151,16 @@ find target -name 'mcpplibs.cmdline.gcm' | grep -q . || {
122151
find target -name '*.gcm' | head -10
123152
exit 1; }
124153

154+
# The staged copy carries the header beside the staged source. Asserted on
155+
# disk as well as through the build, because a build that happened to find the
156+
# header through some other path would pass while leaving this broken.
157+
staged="$(find target/.mangled -name 'libB_detail.h' | head -1)"
158+
[ -n "$staged" ] || {
159+
echo "the header beside the staged source was not staged with it"
160+
find target/.mangled -type f | head -20
161+
exit 1; }
162+
echo " ok the header beside the staged source came with it ($staged)"
163+
125164
out="$("$MCPP" run 2>&1 | tail -1)"
126165
[[ "$out" == "a=1 b=2" ]] || { echo "unexpected output: $out"; exit 1; }
127166

0 commit comments

Comments
 (0)