diff --git a/CHANGELOG.md b/CHANGELOG.md index ad23ec93..626d5fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### 改进 diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index cc48dcbc..bac66cd9 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -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, @@ -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 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 {}; }; diff --git a/tests/e2e/33_multi_version_mangling.sh b/tests/e2e/33_multi_version_mangling.sh index 56537a16..f84495cb 100755 --- a/tests/e2e/33_multi_version_mangling.sh +++ b/tests/e2e/33_multi_version_mangling.sh @@ -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] @@ -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; }