Skip to content

Commit 93c4174

Browse files
committed
fix(pm): install_path 跨命名空间返回了别的包的目录 (2026.8.6.1)
`Fetcher::install_path(ns, shortName, version)` 回答的是"**这个**包装在哪"。它最后 那道 legacy 扫描却匹配任何以 `-x-<shortName>` 结尾的目录、不看命名空间,于是查 `ocornut:imgui@1.92.8` 会返回 `compat-x-imgui/1.92.8` —— 另一个仅仅短名相同的包。 调用方随后(a)认为"已安装"而跳过安装,(b)把那个包的源码树当作本包读取。 **危险形态是静默的。** 我是在一个 Form B 邻居上撞见的,所以错误的 verdir 里没有 mcpp.toml,构建停在 "index entry has no `mcpp = ...` field" —— 一个指向错误原因的 诊断。而当两个包的源码都在 verdir 里时,没有任何报错:构建**编译了另一个包的源码 并成功**。e2e 192 在旧二进制上正是这么红的: Compiling acme.widget v1.5.0 Finished dev [unoptimized + debuginfo] in 0.07s FAIL: build succeeded, so acme:widget was satisfied from somewhere — the only widget payload on disk belongs to compat. **为什么一直够不到。** install_path 同时匹配版本,所以同短名的两个包只有在版本也相 同的时候才会撞;而生态里 module 层用的是打包计数(`imgui@0.0.6`),compat 用的是上 游版本(`compat.imgui@1.92.8`),永远不撞。把 module 层的版本对齐到上游 (mcpp-index#163)之后它们就重合了 —— 这个 bug 是被那次对齐**逼出来**的,不是新引入的。 修法:bare `-x-<shortName>` 这一支现在要求目录自身的命名空间前缀是调用方确实问过的 (请求的 ns,或旧式 index-prefixed 布局里的 index 名)。`-x-<ns>.<name>` 那一支把命 名空间带在后缀里,无需前缀约束。这保住了它存在的两种旧布局,拒掉的正是它从不该服务 的那一种。 顺带修诊断:`<verdir>` 是个字面占位符,无法区分"包是 Form B 而你忘了写 mcpp 字段" 与"mcpp 解析到的 verdir 根本不是这个包的"。现在打印真实路径,并在后一种情况下点名。 验证: * 单测 4 条(tests/unit/test_fallback_legacy_dirs.cpp),含"仍要找得到自己命名空间的 短名布局"与"index-prefixed 旧布局"两条防止修过头。 * e2e 192,实测旧二进制红、修复后绿。 * 真实复现同环境 A/B:store 里有 compat-x-imgui/1.92.8 时,ocornut:imgui@1.92.8 在 旧二进制上失败、在修复后正常安装并构建。 * 59 个单测全过。
1 parent ea3cd18 commit 93c4174

7 files changed

Lines changed: 277 additions & 16 deletions

File tree

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.8.5.4"
3+
version = "2026.8.6.1"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

‎src/build/prepare.cppm‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,12 +2602,20 @@ prepare_build(bool print_fingerprint,
26022602
matches = mcpp::modgraph::expand_glob(verRoot, pat);
26032603
if (!matches.empty()) break;
26042604
}
2605+
// Name the directory actually searched. `<verdir>` was a literal
2606+
// placeholder, so the message could not distinguish "the package
2607+
// is Form B and you forgot the mcpp field" from "the verdir mcpp
2608+
// resolved is not this package's at all" — the second is what a
2609+
// cross-namespace install_path hit produces, and it sent this
2610+
// investigation down the wrong path for a while.
26052611
if (matches.empty()) return std::unexpected(std::format(
26062612
"dependency '{}': index entry has no `mcpp = ...` field, "
2607-
"and no mcpp.toml was found at <verdir>/mcpp.toml or "
2608-
"<verdir>/*/mcpp.toml — add an explicit `mcpp = \"<path>\"` "
2609-
"or `mcpp = {{ ... }}` block to the .lua descriptor.",
2610-
depName));
2613+
"and no mcpp.toml was found at '{}/mcpp.toml' or "
2614+
"'{}/*/mcpp.toml' — add an explicit `mcpp = \"<path>\"` "
2615+
"or `mcpp = {{ ... }}` block to the .lua descriptor. "
2616+
"(If that directory belongs to a DIFFERENT package, the "
2617+
"install step resolved the wrong verdir.)",
2618+
depName, verRoot.string(), verRoot.string()));
26112619
if (matches.size() > 1) return std::unexpected(std::format(
26122620
"dependency '{}': default mcpp.toml lookup matched {} "
26132621
"files; pin one with explicit `mcpp = \"<path>\"`.",

‎src/fallback/legacy_dirs.cppm‎

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,73 @@
11
// mcpp.fallback.legacy_dirs — legacy xpkg directory scan.
22
//
3-
// Last-resort fallback scan (COMPAT, remove in 1.0.0): walk xpkgs/
4-
// for any directory ending with -x-<qualifiedName> or -x-<shortName>.
3+
// Last-resort fallback scan (COMPAT, remove in 1.0.0): walk xpkgs/ for a
4+
// directory that holds the requested package under an older naming layout.
5+
//
6+
// WHY THE SCAN IS NAMESPACE-BOUND
7+
//
8+
// The bare `-x-<shortName>` arm used to match ANY prefix, so a lookup for
9+
// `ocornut:imgui` happily returned `compat-x-imgui` — a DIFFERENT package that
10+
// merely shares a short name. `Fetcher::install_path` then treats that verdir
11+
// as the requested package's, which means:
12+
//
13+
// * the install is skipped (the package "already exists"), and
14+
// * whatever is inside the other namespace's verdir is what gets read.
15+
//
16+
// It stayed invisible while the two packages carried unrelated versions —
17+
// `compat:imgui@1.92.8` next to `mcpplibs:imgui@0.0.6` never collide, because
18+
// install_path also matches on version. Aligning package versions to upstream
19+
// (mcpp-index#163: `imgui@0.0.6` really was ImGui 1.92.8) makes them coincide,
20+
// and the bug becomes reachable.
21+
//
22+
// The observed failure was loud only by luck: `compat.imgui` is a Form B
23+
// descriptor, so the wrong verdir had no mcpp.toml and the build stopped with
24+
// "index entry has no `mcpp = ...` field" — a diagnostic naming the wrong
25+
// cause. Between two Form A packages the wrong verdir DOES have a manifest,
26+
// and the build would have silently compiled the wrong package.
27+
//
28+
// So the shortName arm now requires the directory's own namespace prefix to be
29+
// one the caller actually asked for. That keeps the legacy layouts it exists
30+
// for (`<ns>-x-<shortName>`, `<index>-x-<shortName>`) and refuses the one it
31+
// was never meant to serve: some other namespace's package.
532

633
export module mcpp.fallback.legacy_dirs;
734

835
import std;
936

1037
export namespace mcpp::fallback {
1138

12-
// Scan the xpkgs base directory for a legacy install directory whose
13-
// name ends with "-x-<qualifiedName>" or "-x-<shortName>".
14-
// Returns the matching directory name (not the full path) if found.
39+
// Scan the xpkgs base directory for a legacy install directory holding
40+
// (namespace, shortName). Returns the matching directory name (not the full
41+
// path) if found.
42+
//
43+
// `acceptedPrefixes` are the directory prefixes (the part before `-x-`) that
44+
// may satisfy a bare short-name match — the requested namespace, and the index
45+
// name for the old index-prefixed layout. A fully-qualified `-x-<ns>.<name>`
46+
// match carries the namespace in the suffix itself and needs no prefix check.
1547
std::optional<std::string>
1648
scan_legacy_install_dirs(const std::filesystem::path& xpkgsBase,
1749
std::string_view qualifiedName,
18-
std::string_view shortName) {
50+
std::string_view shortName,
51+
const std::vector<std::string>& acceptedPrefixes) {
1952
std::error_code ec;
2053
std::string suffix1 = std::format("-x-{}", qualifiedName);
2154
std::string suffix2 = std::format("-x-{}", shortName);
2255

56+
auto prefix_accepted = [&](const std::string& dirname) {
57+
auto cut = dirname.size() - suffix2.size();
58+
std::string_view prefix{dirname.data(), cut};
59+
for (auto& p : acceptedPrefixes)
60+
if (prefix == p) return true;
61+
return false;
62+
};
63+
2364
for (auto& entry : std::filesystem::directory_iterator(xpkgsBase, ec)) {
2465
if (!entry.is_directory()) continue;
2566
auto dirname = entry.path().filename().string();
2667
if (dirname.ends_with(suffix1))
2768
return dirname;
28-
if (suffix2 != suffix1 && dirname.ends_with(suffix2))
69+
if (suffix2 != suffix1 && dirname.ends_with(suffix2)
70+
&& prefix_accepted(dirname))
2971
return dirname;
3072
}
3173
return std::nullopt;

‎src/pm/package_fetcher.cppm‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,10 +1173,22 @@ Fetcher::install_path(std::string_view ns, std::string_view shortName,
11731173
if (auto p = try_dir(dirName)) return *p;
11741174
}
11751175

1176-
// Last-resort fallback scan (COMPAT, remove in 1.0.0): walk xpkgs/ for
1177-
// any directory ending with -x-<qname> or -x-<shortName>.
1176+
// Last-resort fallback scan (COMPAT, remove in 1.0.0): walk xpkgs/ for a
1177+
// directory holding this package under an older layout.
1178+
//
1179+
// The bare `-x-<shortName>` arm is bound to the namespaces the caller
1180+
// actually asked for. Unbound, it returned ANY namespace's directory with
1181+
// a matching short name — `ocornut:imgui` resolving to `compat-x-imgui` —
1182+
// and install_path's contract is "the verdir for THIS package", so the
1183+
// install was skipped and another package's tree read in its place. See
1184+
// the header of mcpp.fallback.legacy_dirs for why version alignment is
1185+
// what made it reachable.
11781186
auto qname = mcpp::pm::compat::qualified_name(ns, shortName);
1179-
if (auto legacy = mcpp::fallback::scan_legacy_install_dirs(base, qname, shortName)) {
1187+
std::vector<std::string> acceptedPrefixes;
1188+
if (!ns.empty()) acceptedPrefixes.emplace_back(ns);
1189+
if (!cfg_.defaultIndex.empty()) acceptedPrefixes.emplace_back(cfg_.defaultIndex);
1190+
if (auto legacy = mcpp::fallback::scan_legacy_install_dirs(
1191+
base, qname, shortName, acceptedPrefixes)) {
11801192
if (auto p = try_dir(*legacy)) return *p;
11811193
}
11821194
return std::nullopt;

‎src/version.cppm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.8.5.4";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.8.6.1";
3535

3636
} // namespace mcpp
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc fresh-sandbox
3+
# 192_install_path_namespace.sh — a package must never be satisfied from
4+
# ANOTHER namespace's install directory.
5+
#
6+
# `Fetcher::install_path(ns, shortName, version)` answers "where is THIS
7+
# package installed". Its last-resort legacy scan matched any directory ending
8+
# in `-x-<shortName>`, whatever namespace that directory belonged to, so a
9+
# lookup for `acme:widget@1.5.0` returned `compat-x-widget/1.5.0`. The caller
10+
# then skips the install ("already present") and reads the other package's
11+
# tree.
12+
#
13+
# WHY IT WAS UNREACHABLE UNTIL NOW
14+
#
15+
# install_path also matches on version, so two packages sharing a short name
16+
# collided only if they also shared a version — and the ecosystem's module
17+
# layers carried packaging counters (`imgui@0.0.6`) while the compat packages
18+
# carried upstream versions (`compat.imgui@1.92.8`). Aligning the module layers
19+
# to upstream (mcpp-index#163) makes them coincide, which is how this surfaced.
20+
#
21+
# WHY THE SILENT SHAPE IS THE ONE THAT MATTERS
22+
#
23+
# Discovered against a Form B neighbour, so the wrong verdir had no mcpp.toml
24+
# and the build stopped — with a diagnostic naming the wrong cause. Between two
25+
# packages whose sources live in the verdir, there is no error at all: the
26+
# build compiles the wrong package's source. That is what this test pins.
27+
set -e
28+
29+
TMP=$(mktemp -d)
30+
trap "rm -rf $TMP" EXIT
31+
32+
export MCPP_HOME="$TMP/mcpp-home"
33+
source "$(dirname "$0")/_inherit_toolchain.sh"
34+
35+
mkdir -p "$TMP/proj"
36+
cd "$TMP/proj"
37+
38+
# ── Two packages, same short name, same version, different namespaces ─────
39+
mkdir -p local-index/pkgs/a
40+
cat > local-index/pkgs/a/acme.widget.lua <<'EOF'
41+
package = {
42+
spec = "1",
43+
namespace = "acme",
44+
name = "widget",
45+
description = "acme's widget",
46+
licenses = {"MIT"},
47+
type = "package",
48+
xpm = {
49+
linux = { ["1.5.0"] = { url = "https://example.invalid/w.tar.gz", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" } },
50+
macosx = { ["1.5.0"] = { url = "https://example.invalid/w.tar.gz", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" } },
51+
windows = { ["1.5.0"] = { url = "https://example.invalid/w.zip", sha256 = "0000000000000000000000000000000000000000000000000000000000000000" } },
52+
},
53+
mcpp = {
54+
language = "c++23",
55+
import_std = false,
56+
sources = { "src/widget.cppm" },
57+
targets = { ["widget"] = { kind = "lib" } },
58+
deps = {},
59+
},
60+
}
61+
EOF
62+
63+
# ── Only the FOREIGN namespace's payload is on disk ───────────────────────
64+
# `compat:widget@1.5.0` is installed; `acme:widget@1.5.0` is not. Nothing may
65+
# hand acme's lookup this directory.
66+
#
67+
# It goes in the GLOBAL store ($MCPP_HOME/registry/data/xpkgs), which is what
68+
# `Fetcher::install_path` scans — the project's own .mcpp/.xlings tree is a
69+
# different root reached by `install_path_from_project_data`. Seeding the wrong
70+
# one makes this test pass on a broken binary, which is exactly what the first
71+
# draft did.
72+
mkdir -p "$MCPP_HOME/registry/data/xpkgs/compat-x-widget/1.5.0/src"
73+
cat > "$MCPP_HOME/registry/data/xpkgs/compat-x-widget/1.5.0/src/widget.cppm" <<'EOF'
74+
export module widget;
75+
// If this ever reaches a build that asked for acme:widget, the wrong package
76+
// was compiled. The value is the tell.
77+
export int widget_value() { return 1; }
78+
EOF
79+
80+
mkdir -p src
81+
cat > src/main.cpp <<'EOF'
82+
import widget;
83+
int main() { return widget_value() == 2 ? 0 : 1; }
84+
EOF
85+
86+
cat > mcpp.toml <<'EOF'
87+
[package]
88+
name = "consumer"
89+
version = "0.1.0"
90+
91+
[dependencies.acme]
92+
widget = "1.5.0"
93+
94+
[indices]
95+
acme = { path = "local-index" }
96+
EOF
97+
98+
# The build must NOT succeed: acme:widget is not installed and its url is
99+
# unreachable by design. What it must never do is satisfy the dependency from
100+
# compat's directory.
101+
if "$MCPP" build > b.log 2>&1; then
102+
echo "--- build log ---"; cat b.log
103+
echo "FAIL: build succeeded, so acme:widget was satisfied from somewhere —"
104+
echo " the only widget payload on disk belongs to compat."
105+
exit 1
106+
fi
107+
108+
# Distinguish "correctly refused" from "compiled the wrong package and then
109+
# failed for an unrelated reason": compat's source must never be compiled.
110+
if grep -qE "compat-x-widget/1\.5\.0/src/widget\.cppm" b.log; then
111+
grep -nE "compat-x-widget" b.log | head -5
112+
echo "FAIL: the build reached compat's source while resolving acme:widget"
113+
exit 1
114+
fi
115+
echo " ok: acme:widget was not satisfied from compat-x-widget"
116+
117+
# And the diagnostic has to name the directory it looked in — `<verdir>` as a
118+
# literal placeholder cannot distinguish a missing mcpp field from a wrong
119+
# verdir, which is what made this take a while to find.
120+
if grep -q "<verdir>" b.log; then
121+
echo "FAIL: diagnostic still prints the literal placeholder '<verdir>'"
122+
exit 1
123+
fi
124+
echo " ok: diagnostics name a real path"
125+
126+
echo "OK"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include <gtest/gtest.h>
2+
3+
import std;
4+
import mcpp.fallback.legacy_dirs;
5+
6+
// The legacy install-dir scan must not cross namespaces.
7+
//
8+
// `Fetcher::install_path` contract is "the verdir for THIS package". Its
9+
// last-resort scan used to match any directory ending in `-x-<shortName>`,
10+
// regardless of which namespace that directory belongs to. A lookup for
11+
// `ocornut:imgui` therefore returned `compat-x-imgui` — a different package
12+
// that merely shares a short name — and the caller then (a) skipped the
13+
// install because the package "already exists" and (b) read the other
14+
// package's tree.
15+
//
16+
// It was unreachable while the two carried unrelated versions
17+
// (`compat:imgui@1.92.8` vs `mcpplibs:imgui@0.0.6`), because install_path also
18+
// matches on version. Aligning package versions to upstream (mcpp-index#163)
19+
// makes them coincide.
20+
21+
namespace {
22+
23+
std::filesystem::path make_xpkgs(std::initializer_list<std::string_view> dirs) {
24+
auto base = std::filesystem::temp_directory_path()
25+
/ std::format("legacy-dirs-{}",
26+
std::chrono::steady_clock::now()
27+
.time_since_epoch().count());
28+
for (auto d : dirs) std::filesystem::create_directories(base / std::string(d));
29+
return base;
30+
}
31+
32+
} // namespace
33+
34+
TEST(FallbackLegacyDirs, DoesNotReturnAnotherNamespacesDirectory) {
35+
auto base = make_xpkgs({"compat-x-imgui"});
36+
// Asking for ocornut:imgui. Only compat's directory exists, and it is NOT
37+
// an answer to this question.
38+
auto hit = mcpp::fallback::scan_legacy_install_dirs(
39+
base, "ocornut.imgui", "imgui", {"ocornut", "mcpplibs"});
40+
EXPECT_FALSE(hit.has_value())
41+
<< "returned '" << hit.value_or("") << "' for ocornut:imgui";
42+
std::filesystem::remove_all(base);
43+
}
44+
45+
TEST(FallbackLegacyDirs, StillFindsOwnNamespaceShortNameLayout) {
46+
auto base = make_xpkgs({"compat-x-imgui", "ocornut-x-imgui"});
47+
auto hit = mcpp::fallback::scan_legacy_install_dirs(
48+
base, "ocornut.imgui", "imgui", {"ocornut", "mcpplibs"});
49+
ASSERT_TRUE(hit.has_value());
50+
EXPECT_EQ(*hit, "ocornut-x-imgui");
51+
std::filesystem::remove_all(base);
52+
}
53+
54+
TEST(FallbackLegacyDirs, StillFindsIndexPrefixedLegacyLayout) {
55+
// The old layout this arm exists for: <index>-x-<shortName>.
56+
auto base = make_xpkgs({"mcpplibs-x-tinyhttps"});
57+
auto hit = mcpp::fallback::scan_legacy_install_dirs(
58+
base, "mcpplibs.tinyhttps", "tinyhttps", {"mcpplibs", "mcpplibs"});
59+
ASSERT_TRUE(hit.has_value());
60+
EXPECT_EQ(*hit, "mcpplibs-x-tinyhttps");
61+
std::filesystem::remove_all(base);
62+
}
63+
64+
TEST(FallbackLegacyDirs, QualifiedSuffixNeedsNoPrefixAllowance) {
65+
// `-x-<ns>.<name>` carries the namespace in the suffix itself, so it is
66+
// accepted whatever the prefix is — that is the legacy FQN layout.
67+
auto base = make_xpkgs({"someindex-x-ocornut.imgui"});
68+
auto hit = mcpp::fallback::scan_legacy_install_dirs(
69+
base, "ocornut.imgui", "imgui", {"ocornut"});
70+
ASSERT_TRUE(hit.has_value());
71+
EXPECT_EQ(*hit, "someindex-x-ocornut.imgui");
72+
std::filesystem::remove_all(base);
73+
}

0 commit comments

Comments
 (0)