Skip to content

Commit f2030f5

Browse files
authored
fix(pm): identity-first candidate selection — resolve custom-ns pkgs filed under non-canonical filenames (v0.0.67) (#176)
A dependency with a namespace prefix whose package declares the split form (`name="tensorvia-cpu"`, `namespace="aimol"`) and is filed under a non-canonical filename in a shared index (`pkgs/t/tensorvia-cpu.lua`, not the canonical `pkgs/a/aimol.tensorvia-cpu.lua`) failed to resolve: error: dependency 'aimol.tensorvia-cpu': index entry not found in local clone while the bare form `tensorvia-cpu` resolved and installed correctly. Root cause: `selectDependencyCandidate` disambiguated the dotted-selector candidate ladder [(mcpplibs.aimol, tensorvia-cpu), (aimol, tensorvia-cpu)] by probing whether each candidate's CANONICAL FILENAME `<ns>.<short>.lua` exists on disk. The descriptor's bytes live under a non-canonical name, so the correct peer-root candidate (aimol, tensorvia-cpu) was invisible, the request stayed pinned to the wrong front candidate (mcpplibs.aimol, …), and the load-path identity gate rejected it. Fix: candidate selection is now identity-first. It locates each candidate via the same identity-verified readers the load path uses (`read_xpkg_lua*`), which key on the descriptor's DECLARED (ns, name) and already cover non-canonical filenames — so selection and loading can never disagree. The canonical-filename-only strict reader (`readStrictLuaFromPkgsDir` / `canonicalXpkgLuaFilename`) is removed; the filename is no longer an identity key. This is Step 0 of the identity-first architecture; the §5 PackageLocator/IdentityIndex choke-point consolidation remains follow-up. Design + research + the complete (ns,name) matching-rules table + test-coverage-gap analysis: .agents/docs/2026-06-26-identity-first-resolution-no-filename.md Tests: - unit PmPackageFetcher.ResolvesCustomNamespaceDescriptorUnderNonCanonicalFilename (locks the read-layer identity-not-filename invariant) - e2e 76_qualified_custom_ns_noncanonical_filename.sh (the full repro: builtin index × non-canonical filename × qualified request; asserts resolve + lock ns="aimol" + clean not-found for a genuinely wrong namespace)
1 parent 43d275d commit f2030f5

7 files changed

Lines changed: 747 additions & 57 deletions

File tree

.agents/docs/2026-06-26-identity-first-resolution-no-filename.md

Lines changed: 534 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.67] — 2026-06-26
7+
8+
### 修复
9+
10+
- **带命名空间前缀的依赖解析失败 `index entry not found in local clone`(自定义 ns + 非规范文件名)**:
11+
当一个包以「裸 `name` + 独立 `namespace` 字段」形态声明(如 `aimol.tensorvia-cpu`:
12+
`name="tensorvia-cpu"``namespace="aimol"`),并以**非规范文件名**落盘在共享索引里
13+
(`pkgs/t/tensorvia-cpu.lua` 而非 `pkgs/a/aimol.tensorvia-cpu.lua`)时,限定请求
14+
`aimol.tensorvia-cpu` 报「索引条目缺失」,而裸名 `tensorvia-cpu` 却能解析。根因是
15+
**候选消歧 `selectDependencyCandidate` 用「规范文件名 `<ns>.<short>.lua` 是否存在」当身份
16+
判据**——描述符以非规范文件名落盘时,正确的 peer-root 候选 `(aimol, tensorvia-cpu)` 对消歧
17+
器隐形,请求被钉死在错误的首选候选 `(mcpplibs.aimol, …)` 上并被身份门拒绝。修复:候选消歧
18+
改为**身份优先**,经由加载路径同款的身份校验读取器(`read_xpkg_lua*`)按描述符**声明的
19+
`(ns, name)`** 定位候选,文件名不再参与身份判定——选择层与加载层从此不可能对同一候选产生
20+
分歧。详见 `.agents/docs/2026-06-26-identity-first-resolution-no-filename.md`
21+
622
## [0.0.66] — 2026-06-26
723

824
### 修复

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 = "0.0.66"
3+
version = "0.0.67"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/prepare.cppm

Lines changed: 20 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -899,32 +899,20 @@ prepare_build(bool print_fingerprint,
899899
return nullptr;
900900
};
901901

902-
auto canonicalXpkgLuaFilename =
903-
[](std::string_view ns, std::string_view shortName) {
904-
if (ns.empty() || ns == mcpp::pm::kDefaultNamespace) {
905-
return std::string(shortName) + ".lua";
906-
}
907-
return std::format("{}.{}.lua", ns, shortName);
908-
};
909-
910-
auto readStrictLuaFromPkgsDir =
911-
[&](const std::filesystem::path& pkgsDir,
912-
std::string_view ns,
913-
std::string_view shortName) -> std::optional<std::string>
914-
{
915-
auto fname = canonicalXpkgLuaFilename(ns, shortName);
916-
if (fname.empty()) return std::nullopt;
917-
char first = static_cast<char>(std::tolower(
918-
static_cast<unsigned char>(fname.front())));
919-
auto candidate = pkgsDir / std::string(1, first) / fname;
920-
if (!std::filesystem::exists(candidate)) return std::nullopt;
921-
922-
std::ifstream is(candidate);
923-
std::stringstream ss;
924-
ss << is.rdbuf();
925-
return ss.str();
926-
};
927-
902+
// Identity-first candidate probe. A candidate is located by the DECLARED
903+
// (namespace, name) of whatever descriptor the index holds — never by whether
904+
// a canonically-named file `<ns>.<short>.lua` happens to exist on disk. It
905+
// routes through the same identity-verified readers the load path uses
906+
// (`read_xpkg_lua*`, which gate every hit on the descriptor's declared
907+
// identity and already cover non-canonical filenames), so candidate selection
908+
// and loading can never disagree about what a candidate resolves to.
909+
//
910+
// Before this, selection probed the canonical filename only, so a descriptor
911+
// filed under a non-canonical name (e.g. `aimol.tensorvia-cpu` declared in the
912+
// mcpplibs index as bare `pkgs/t/tensorvia-cpu.lua`) was invisible to its own
913+
// peer-root candidate `(aimol, tensorvia-cpu)`, leaving the request pinned to
914+
// the wrong front candidate `(mcpplibs.aimol, …)`. See
915+
// .agents/docs/2026-06-26-identity-first-resolution-no-filename.md.
928916
auto readStrictLuaForCandidate =
929917
[&](const mcpp::pm::DependencyCoordinate& coord)
930918
-> std::optional<std::string>
@@ -935,38 +923,15 @@ prepare_build(bool print_fingerprint,
935923
auto* idxSpec = findIndexForNs(coord.namespace_);
936924
if (idxSpec && idxSpec->is_local()) {
937925
auto indexPath = mcpp::config::resolve_project_index_path(*root, *idxSpec);
938-
return readStrictLuaFromPkgsDir(indexPath / "pkgs",
939-
coord.namespace_,
940-
coord.shortName);
926+
return mcpp::fetcher::Fetcher::read_xpkg_lua_from_path(
927+
indexPath, coord.namespace_, coord.shortName);
941928
}
942929
if (idxSpec && !idxSpec->is_builtin()) {
943-
std::error_code ec;
944-
for (auto& data : mcpp::config::project_xlings_data_roots(*root)) {
945-
if (!std::filesystem::exists(data)) continue;
946-
for (auto& entry : std::filesystem::directory_iterator(data, ec)) {
947-
if (!entry.is_directory()) continue;
948-
auto pkgsDir = entry.path() / "pkgs";
949-
if (auto lua = readStrictLuaFromPkgsDir(
950-
pkgsDir, coord.namespace_, coord.shortName)) {
951-
return lua;
952-
}
953-
}
954-
}
955-
return std::nullopt;
956-
}
957-
958-
auto data = (*cfg)->xlingsHome() / "data";
959-
if (!std::filesystem::exists(data)) return std::nullopt;
960-
std::error_code ec;
961-
for (auto& entry : std::filesystem::directory_iterator(data, ec)) {
962-
if (!entry.is_directory()) continue;
963-
auto pkgsDir = entry.path() / "pkgs";
964-
if (auto lua = readStrictLuaFromPkgsDir(
965-
pkgsDir, coord.namespace_, coord.shortName)) {
966-
return lua;
967-
}
930+
return mcpp::fetcher::Fetcher::read_xpkg_lua_from_project_data(
931+
*root, coord.namespace_, coord.shortName);
968932
}
969-
return std::nullopt;
933+
mcpp::fetcher::Fetcher fetcher(**cfg);
934+
return fetcher.read_xpkg_lua(coord.namespace_, coord.shortName);
970935
};
971936

972937
auto xpkgLuaMatchesCandidate =

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.66";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.67";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc fresh-sandbox
3+
# Regression for the identity-first resolution gap
4+
# (.agents/docs/2026-06-26-identity-first-resolution-no-filename.md).
5+
#
6+
# Production trigger: package `aimol.tensorvia-cpu` declares
7+
# namespace = "aimol", name = "tensorvia-cpu"
8+
# and is hosted in the BUILTIN mcpplibs index (NOT a [indices] entry), filed under
9+
# the NON-canonical bare filename `pkgs/t/tensorvia-cpu.lua` (canonical would be
10+
# `pkgs/a/aimol.tensorvia-cpu.lua`). A qualified request `aimol.tensorvia-cpu`
11+
# must resolve by the descriptor's DECLARED (ns, name) — the filename is not a key.
12+
#
13+
# This is the exact intersection the prior suite never crossed at once:
14+
# builtin index × non-canonical filename × qualified multi-candidate request.
15+
# Fixed by making selectDependencyCandidate identity-first (it now locates each
16+
# candidate by the descriptor's declared (ns, name) via read_xpkg_lua* instead of
17+
# probing the canonical filename `<ns>.<short>.lua`).
18+
set -e
19+
20+
TMP=$(mktemp -d)
21+
trap "rm -rf $TMP" EXIT
22+
23+
export MCPP_HOME="$TMP/mcpp-home"
24+
source "$(dirname "$0")/_inherit_toolchain.sh"
25+
26+
INDEX_DIR="$MCPP_HOME/registry/data/mcpplibs"
27+
# Observed real install layout is <ns>-x-<short>: aimol-x-tensorvia-cpu.
28+
PKG_ROOT="$MCPP_HOME/registry/data/xpkgs/aimol-x-tensorvia-cpu/0.1.1"
29+
mkdir -p "$INDEX_DIR/pkgs/t" "$PKG_ROOT/src"
30+
printf 'ok\n' > "$INDEX_DIR/.mcpp-index-updated"
31+
32+
# Custom namespace "aimol", bare name "tensorvia-cpu", filed under the BARE
33+
# filename in the mcpplibs index — filename does not encode the namespace.
34+
cat > "$INDEX_DIR/pkgs/t/tensorvia-cpu.lua" <<'EOF'
35+
package = {
36+
spec = "1",
37+
namespace = "aimol",
38+
name = "tensorvia-cpu",
39+
description = "Custom-namespace package filed under a non-canonical filename",
40+
licenses = {"MIT"},
41+
type = "package",
42+
xpm = {
43+
linux = {
44+
["0.1.1"] = {
45+
url = "https://example.invalid/tensorvia-cpu-0.1.1.tar.gz",
46+
sha256 = "0000000000000000000000000000000000000000000000000000000000000000",
47+
},
48+
},
49+
},
50+
mcpp = {
51+
language = "c++23",
52+
import_std = false,
53+
sources = { "src/tensorvia.cppm" },
54+
targets = { ["tensorvia-cpu"] = { kind = "lib" } },
55+
deps = {},
56+
},
57+
}
58+
EOF
59+
60+
cat > "$PKG_ROOT/src/tensorvia.cppm" <<'EOF'
61+
export module tensorvia.cpu;
62+
63+
export int tensorvia_value() {
64+
return 42;
65+
}
66+
EOF
67+
printf 'ok\n' > "$PKG_ROOT/.mcpp_ok"
68+
69+
mkdir -p "$TMP/project/app/src"
70+
cd "$TMP/project/app"
71+
72+
cat > src/main.cpp <<'EOF'
73+
import tensorvia.cpu;
74+
75+
int main() {
76+
return tensorvia_value() == 42 ? 0 : 1;
77+
}
78+
EOF
79+
80+
cat > mcpp.toml <<'EOF'
81+
[package]
82+
name = "app"
83+
version = "0.1.0"
84+
85+
[dependencies]
86+
aimol.tensorvia-cpu = "0.1.1"
87+
88+
[targets.app]
89+
kind = "bin"
90+
main = "src/main.cpp"
91+
EOF
92+
93+
# (1) Qualified custom-ns request must resolve despite the non-canonical filename.
94+
"$MCPP" build > build.log 2>&1 || {
95+
echo "FAIL: aimol.tensorvia-cpu did not resolve (identity-first regression)"
96+
cat build.log
97+
exit 1
98+
}
99+
"$MCPP" run > run.log 2>&1 || { cat run.log; exit 1; }
100+
101+
# (2) The resolved identity must record the declared namespace, not mcpplibs.aimol.
102+
grep -q 'namespace = "aimol"' mcpp.lock || {
103+
cat mcpp.lock
104+
echo "FAIL: lock must record resolved namespace aimol"
105+
exit 1
106+
}
107+
if grep -q 'mcpplibs.aimol' mcpp.lock; then
108+
cat mcpp.lock
109+
echo "FAIL: front candidate mcpplibs.aimol leaked into the lock"
110+
exit 1
111+
fi
112+
113+
# (3) A genuinely wrong namespace must be a clean not-found, not a silent match.
114+
cat > mcpp.toml <<'EOF'
115+
[package]
116+
name = "app"
117+
version = "0.1.0"
118+
119+
[dependencies]
120+
mcpplibs.tensorvia-cpu = "0.1.1"
121+
122+
[targets.app]
123+
kind = "bin"
124+
main = "src/main.cpp"
125+
EOF
126+
rm -f mcpp.lock
127+
if "$MCPP" build > wrong.log 2>&1; then
128+
echo "FAIL: mcpplibs.tensorvia-cpu must NOT resolve (package is aimol-namespaced)"
129+
cat wrong.log
130+
exit 1
131+
fi
132+
133+
echo "OK"

tests/unit/test_pm_package_fetcher.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,45 @@ TEST(PmPackageFetcher, LocalPathIndexAttributesOwnNamespaceToNoNsDescriptor) {
133133

134134
std::filesystem::remove_all(index);
135135
}
136+
137+
// Coverage gap closed: a custom-namespace descriptor filed under a NON-canonical
138+
// filename must still resolve for a qualified request, because identity is the
139+
// declared (namespace, name) — never the filename (design doc
140+
// 2026-06-26-identity-first-resolution-no-filename.md, P0/P2).
141+
//
142+
// Real-world trigger: `aimol.tensorvia-cpu` declares namespace="aimol",
143+
// name="tensorvia-cpu" but is filed in the mcpplibs index as the BARE
144+
// `pkgs/t/tensorvia-cpu.lua` (canonical would be `pkgs/a/aimol.tensorvia-cpu.lua`).
145+
// Every prior fetcher fixture sat at its canonical path, so this seam was untested.
146+
// `read_xpkg_lua*` already keys on declared identity, so this asserts the READ
147+
// layer is correct and the production failure is isolated to candidate SELECTION
148+
// (`selectDependencyCandidate`'s canonical-filename-only strict reader).
149+
TEST(PmPackageFetcher, ResolvesCustomNamespaceDescriptorUnderNonCanonicalFilename) {
150+
auto project = make_tempdir("mcpp-noncanonical-filename");
151+
auto dataRoot = project / ".mcpp" / "data";
152+
153+
// Declared identity (aimol, tensorvia-cpu), but filed under the bare short
154+
// name in the mcpplibs index — filename does NOT encode the namespace.
155+
write_file(dataRoot / "mcpplibs" / "pkgs" / "t" / "tensorvia-cpu.lua",
156+
R"(package = {
157+
namespace = "aimol",
158+
name = "tensorvia-cpu",
159+
version = "0.1.1",
160+
mcpp = { sources = { "*.cppm" } },
161+
})");
162+
163+
// Qualified request for the custom namespace must resolve, filename be damned.
164+
auto hit = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data(
165+
project, "aimol", "tensorvia-cpu");
166+
ASSERT_TRUE(hit.has_value())
167+
<< "declared (aimol, tensorvia-cpu) must resolve regardless of filename";
168+
EXPECT_NE(hit->find("tensorvia-cpu"), std::string::npos);
169+
170+
// A foreign namespace for the same short name must NOT match it.
171+
auto wrongNs = mcpp::pm::Fetcher::read_xpkg_lua_from_project_data(
172+
project, "mcpplibs", "tensorvia-cpu");
173+
EXPECT_FALSE(wrongNs.has_value())
174+
<< "the descriptor is (aimol, …), so a (mcpplibs, …) request must miss";
175+
176+
std::filesystem::remove_all(project);
177+
}

0 commit comments

Comments
 (0)