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

## [未发布]

### 修复

- **暂存跨主版本副本时,与源码相邻的私有头没有跟着走。**

当同一个包的两个主版本同时出现在一张图里,解析器会把其中一份改名暂存到
`target/.mangled/` 下。暂存只搬源码 —— 这对经 `[build].include_dirs`
找到的头是对的,它们靠绝对化后的路径仍指回原处;但对**写在源码旁边**的私有头
不对:`#include "detail.h"` 是相对**包含它的那个文件所在目录**解析的,搬走源码
就搬走了搜索起点。包从来没有声明过这条路径,因为它从来不需要。

```
target/.mangled/openkal-opensbi/__self__/src/time.cpp:44:10:
fatal error: 'sbi.h' file not found
```

⚠️ 这条诊断指向的东西全是错的:路径是作者没写过的暂存目录,头文件就躺在源码
期待的位置,而触发它的构建没有要求任何不寻常的事情 —— 一张图里有两个主版本是
受支持的安排,这是它最普通的后果。

现在,凡是包含了被暂存源码的目录,其中未被暂存的文件一并原样带过去。没有被
暂存源码的目录不会被访问,所以代价与暂存量成正比。

## [2026.8.24.5] — 2026-08-25

### 改进
Expand Down
50 changes: 47 additions & 3 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -3912,9 +3912,30 @@ prepare_build(bool print_fingerprint,
// version mangling fallback (Level 1) so two cross-major copies of
// the same package can coexist with distinct module names.
//
// Headers (referenced via `[build].include_dirs`) are NOT staged —
// those keep pointing at the original install dir via absolutized
// include paths.
// Headers reached through `[build].include_dirs` are NOT staged — those
// keep pointing at the original install dir via absolutized include paths.
//
// ⭐ HEADERS BESIDE A SOURCE ARE A DIFFERENT CASE, AND THEY ARE STAGED.
//
// `#include "detail.h"` is resolved relative to the directory of the file
// holding the directive, so moving the source moves the search. No
// `include_dirs` entry is involved and absolutizing one cannot help: the
// package never declared a path because it never needed one. Measured
// before this, on a package whose `src/time.cpp` includes `src/sbi.h`:
//
// target/.mangled/openkal-opensbi/__self__/src/time.cpp:44:10:
// fatal error: 'sbi.h' file not found
//
// ⚠️ THE DIAGNOSIS THIS PRODUCES POINTS AT THE WRONG THING. The path in it
// is a staging directory the author never wrote, for a header sitting
// 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.
//
// What is copied is every file in a directory that contains a staged
// source and is not itself staged, verbatim: rewriting applies to module
// declarations, and a header has none. Directories with no staged source
// are not visited, so this stays proportional to what is being staged.
auto stage_with_rewrite = [&](const std::filesystem::path& srcRoot,
const std::filesystem::path& dstRoot,
const mcpp::manifest::Manifest& depManifest,
Expand Down Expand Up @@ -3948,6 +3969,29 @@ prepare_build(bool print_fingerprint,
"stage: cannot write '{}'", dst.string()));
os << out;
}

// The files beside those sources, carried across unchanged so a quoted
// include still finds what it named.
std::set<std::filesystem::path> sourceDirs;
for (auto const& f : *sources) sourceDirs.insert(f.parent_path());
for (auto const& dir : sourceDirs) {
for (auto const& entry : std::filesystem::directory_iterator(dir, ec)) {
if (ec) break;
if (!entry.is_regular_file()) continue;
if (sources->contains(entry.path())) continue;
auto rel = std::filesystem::relative(entry.path(), srcRoot, ec);
if (ec) continue;
auto dst = dstRoot / rel;
std::filesystem::create_directories(dst.parent_path(), ec);
std::filesystem::copy_file(
entry.path(), dst,
std::filesystem::copy_options::overwrite_existing, ec);
if (ec) return std::unexpected(std::format(
"stage: cannot copy '{}': {}",
entry.path().string(), ec.message()));
}
ec.clear();
}
return {};
};

Expand Down
41 changes: 40 additions & 1 deletion tests/e2e/33_multi_version_mangling.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,39 @@ mkdir -p "$TMP/libB" && cd "$TMP/libB"
cd libB
rm -f src/main.cpp
cat > src/libB.cppm <<'EOF'
module;
// Declared in the global module fragment, so it has the same linkage as its
// definition in libB_impl.cpp — a non-module translation unit. Declaring it
// after `export module` would attach it to libB and leave the definition
// unattached, which is a mistake in a fixture rather than in what it tests.
int libB_impl_v();
export module libB;
import mcpplibs.cmdline; // resolver rewrites this to the mangled secondary
import std;
export int libB_v() { return 2; }
export int libB_v() { return libB_impl_v(); }
EOF
# ⭐ A PRIVATE HEADER BESIDE A SOURCE, WHICH IS WHAT STAGING USED TO DROP.
#
# libB is the copy the resolver stages under `target/.mangled/`, and a quoted
# include is resolved relative to the directory of the file holding it — so
# moving the source moves the search. No `include_dirs` entry is involved and
# absolutizing one cannot help: a package with its header beside its source
# never declared a path because it never needed one.
#
# Measured before the fix, on this fixture and on a real package alike:
#
# .mangled/libB/__self__/src/libB_impl.cpp:1:10:
# fatal error: libB_detail.h: No such file or directory
#
# The failure names a staging directory the author never wrote, for a header
# sitting exactly where the source expects it.
cat > src/libB_detail.h <<'EOF'
#pragma once
inline int libB_detail_v() { return 2; }
EOF
cat > src/libB_impl.cpp <<'EOF'
#include "libB_detail.h"
int libB_impl_v() { return libB_detail_v(); }
EOF
cat > mcpp.toml <<'EOF'
[package]
Expand Down Expand Up @@ -122,6 +151,16 @@ find target -name 'mcpplibs.cmdline.gcm' | grep -q . || {
find target -name '*.gcm' | head -10
exit 1; }

# The staged copy carries the header beside the staged source. Asserted on
# disk as well as through the build, because a build that happened to find the
# header through some other path would pass while leaving this broken.
staged="$(find target/.mangled -name 'libB_detail.h' | head -1)"
[ -n "$staged" ] || {
echo "the header beside the staged source was not staged with it"
find target/.mangled -type f | head -20
exit 1; }
echo " ok the header beside the staged source came with it ($staged)"

out="$("$MCPP" run 2>&1 | tail -1)"
[[ "$out" == "a=1 b=2" ]] || { echo "unexpected output: $out"; exit 1; }

Expand Down
Loading