From 01346cfe7788b387ad6b7003ff6746104ccafe42 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 2 Jun 2026 11:15:31 +0800 Subject: [PATCH 1/3] Fix independent root dependency selection --- .../2026-06-02-imgui-mcpp-dependency-fixes.md | 65 +++++++++++- src/cli.cppm | 57 +++++++++-- src/manifest.cppm | 63 ++++++++---- src/pm/dependency_selector.cppm | 4 + src/pm/lock_io.cppm | 2 +- .../63_bare_dependency_peer_root_priority.sh | 99 +++++++++++++++++++ tests/unit/test_pm_compat.cpp | 13 +++ 7 files changed, 268 insertions(+), 35 deletions(-) create mode 100755 tests/e2e/63_bare_dependency_peer_root_priority.sh diff --git a/.agents/docs/2026-06-02-imgui-mcpp-dependency-fixes.md b/.agents/docs/2026-06-02-imgui-mcpp-dependency-fixes.md index 8192bf1..3acaade 100644 --- a/.agents/docs/2026-06-02-imgui-mcpp-dependency-fixes.md +++ b/.agents/docs/2026-06-02-imgui-mcpp-dependency-fixes.md @@ -59,6 +59,18 @@ The imgui validation exposed two separate mcpp-side problems. `imgui.backend.*` modules. The lock file also obscured the real dependency source. +4. Bare dependency selectors could not fall back to independent root packages. + + `resolve_dependency_selector("imgui")` produced only `mcpplibs/imgui`. + That missed the intended rule that bare names first try the omitted + `mcpplibs` root and then fall back to an independent root package. + + The failure had a second layer: the default-namespace strict lookup uses + `imgui.lua` as its canonical filename, which is the same descriptor filename + used by an independent root `imgui` package. Without checking the xpkg + descriptor's declared `package.name` / `package.namespace`, the first + candidate could consume the independent package as `mcpplibs.imgui`. + ## Changes In This Branch - `src/build/flags.cppm` @@ -94,6 +106,28 @@ The imgui validation exposed two separate mcpp-side problems. - Added a branch dependency regression that verifies `mcpp update ` refreshes a moved branch and that lock source metadata is marked as git. +- `src/pm/dependency_selector.cppm` + - Bare selectors now produce ordered candidates: + `mcpplibs/`, then independent root ``. + +- `src/manifest.cppm` + - Added `extract_xpkg_name()` alongside `extract_xpkg_namespace()` so resolver + code can validate package identity from the xpkg descriptor. + +- `src/cli.cppm` + - Candidate selection now validates that a matched xpkg descriptor belongs to + the candidate coordinate before selecting it. + - Independent root packages keep an empty package namespace in `mcpp.lock` + instead of being rewritten as `mcpplibs`. + +- `tests/unit/test_pm_compat.cpp` + - Added a bare selector regression for `imgui -> mcpplibs/imgui, then imgui`. + +- `tests/e2e/63_bare_dependency_peer_root_priority.sh` + - Added a no-network regression that provides only an independent root + `imgui` package in a temporary default index and verifies `imgui = "1.0.0"` + builds/runs without locking the package as `mcpplibs`. + ## Evidence So Far - Red test for the include bug: @@ -116,7 +150,7 @@ The imgui validation exposed two separate mcpp-side problems. - Green e2e after rebuilding the mcpp CLI with the resolver fix: - Command: - `MCPP=target/x86_64-linux-gnu/85da010ca4e7d6e2/bin/mcpp bash tests/e2e/60_stale_xpkg_cache_reinstall.sh` + `MCPP= bash tests/e2e/60_stale_xpkg_cache_reinstall.sh` - Result: `OK` - Boundary regression caught and fixed: @@ -143,7 +177,7 @@ The imgui validation exposed two separate mcpp-side problems. - Green e2e after rebuilding the mcpp CLI with the git dependency fix: - Command: - `MCPP=target/x86_64-linux-gnu/ea28c45f9dcd4fed/bin/mcpp bash tests/e2e/24_git_dependency.sh` + `MCPP= bash tests/e2e/24_git_dependency.sh` - Result: `OK` - Focused regression after the final git dependency change: @@ -152,9 +186,31 @@ The imgui validation exposed two separate mcpp-side problems. - `tests/e2e/24_git_dependency.sh`: `OK` - `tests/e2e/62_dotted_dependency_selector_priority.sh`: `OK` +- Red unit test for bare selector fallback before the fix: + - `DependencySelector.BareSelectorBuildsOmittedMcpplibsThenPeerRootCandidates` + failed because `selector.candidates.size()` was `1`, expected `2`. + +- Green focused verification after the bare selector and candidate identity fix: + - `mcpp test -- --gtest_filter=DependencySelector.BareSelectorBuildsOmittedMcpplibsThenPeerRootCandidates`: + `18 passed; 0 failed` + - `tests/e2e/62_dotted_dependency_selector_priority.sh`: `OK` + - `tests/e2e/63_bare_dependency_peer_root_priority.sh`: `OK` + +- Full local verification after the final edits: + - `mcpp test`: `18 passed; 0 failed` + - `tests/e2e/run_all.sh`: `67 passed, 0 failed, 0 skipped` + - `tests/e2e/62_dotted_dependency_selector_priority.sh`: `OK` + - `tests/e2e/63_bare_dependency_peer_root_priority.sh`: `OK` + +- External mcpp-index imgui smoke with the rebuilt mcpp CLI: + - Command shape: `MCPP= bash tests/smoke_imgui_module.sh` + - Result: `Dear ImGui 1.92.8 module package ok` + - Observed install target: `mcpplibs:imgui@0.0.1`, meaning default index + package `imgui`; not `mcpplibs.imgui`. + - Fresh external `imgui-private` git consumer with rebuilt mcpp CLI: - Command: - `MCPP_HOME=/tmp/imgui-private-fixed-mcpp-home target/x86_64-linux-gnu/ea28c45f9dcd4fed/bin/mcpp run` + `MCPP_HOME= run` - Result: `external git consumer imported Dear ImGui 1.92.8` - Lock source: @@ -181,3 +237,6 @@ The imgui validation exposed two separate mcpp-side problems. corrected. - The mcpp fixes should be submitted as a separate PR from imgui package/index updates. +- Public `imgui` package identity is independent root `imgui`, not + `mcpplibs.imgui`; mcpp's omitted-`mcpplibs` priority must not rewrite that + identity after the fallback candidate is selected. diff --git a/src/cli.cppm b/src/cli.cppm index d32b66b..ec83ad0 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1714,6 +1714,42 @@ prepare_build(bool print_fingerprint, return std::nullopt; }; + auto candidateQualifiedName = + [](std::string_view ns, std::string_view shortName) { + if (ns.empty()) return std::string(shortName); + return std::format("{}.{}", ns, shortName); + }; + + auto xpkgLuaMatchesCandidate = + [&](const mcpp::pm::DependencyCoordinate& coord, + std::string_view luaContent, + bool allowLegacyBareDefault) { + auto luaName = mcpp::manifest::extract_xpkg_name(luaContent); + if (luaName.empty()) return true; + + auto luaNs = mcpp::manifest::extract_xpkg_namespace(luaContent); + auto qname = candidateQualifiedName(coord.namespace_, coord.shortName); + + if (coord.namespace_.empty()) { + return luaNs.empty() && luaName == coord.shortName; + } + + if (coord.namespace_ == mcpp::pm::kDefaultNamespace) { + if (luaNs == coord.namespace_) { + return luaName == coord.shortName || luaName == qname; + } + if (luaNs.empty() && luaName == qname) return true; + return allowLegacyBareDefault + && luaNs.empty() + && luaName == coord.shortName; + } + + if (luaNs == coord.namespace_) { + return luaName == coord.shortName || luaName == qname; + } + return luaNs.empty() && luaName == qname; + }; + auto dependencyCoordinates = [](const mcpp::manifest::DependencySpec& spec, const std::string& depName) { @@ -1741,7 +1777,9 @@ prepare_build(bool print_fingerprint, auto selected = candidates.front(); if (spec.isVersion() && candidates.size() > 1) { for (auto& candidate : candidates) { - if (readStrictLuaForCandidate(candidate)) { + auto lua = readStrictLuaForCandidate(candidate); + if (lua && xpkgLuaMatchesCandidate( + candidate, *lua, /*allowLegacyBareDefault=*/false)) { selected = candidate; break; } @@ -1904,9 +1942,7 @@ prepare_build(bool print_fingerprint, if (!childSpec.isVersion()) continue; ResolvedKey childKey{ - childSpec.namespace_.empty() - ? std::string{mcpp::manifest::kDefaultNamespace} - : childSpec.namespace_, + childSpec.namespace_, childSpec.shortName.empty() ? childName : childSpec.shortName, }; if (auto child = loadVersionDep( @@ -2344,9 +2380,7 @@ prepare_build(bool print_fingerprint, } ResolvedKey key{ - spec.namespace_.empty() - ? std::string{mcpp::manifest::kDefaultNamespace} - : spec.namespace_, + spec.namespace_, spec.shortName.empty() ? name : spec.shortName, }; const std::string sourceKind = @@ -3041,17 +3075,20 @@ prepare_build(bool print_fingerprint, } } else { lp.namespace_ = spec.namespace_.empty() - ? std::string(mcpp::pm::kDefaultNamespace) + ? std::string{} : spec.namespace_; lp.version = spec.version; // Use the namespace and resolved version as the source identifier. // For custom indices, include the index name for traceability. - lp.source = std::format("index+{}@{}", lp.namespace_, lp.version); + auto sourceIndex = lp.namespace_.empty() + ? std::string(mcpp::pm::kDefaultNamespace) + : lp.namespace_; + lp.source = std::format("index+{}@{}", sourceIndex, lp.version); // Use a deterministic hash based on namespace + name + version. // A future PR can replace this with a real content hash from the // xpkg.lua's declared sha256 or from the install plan. std::hash hasher; - auto hashInput = std::format("{}:{}@{}", lp.namespace_, name, lp.version); + auto hashInput = std::format("{}:{}@{}", sourceIndex, name, lp.version); lp.hash = std::format("fnv1a:{:016x}", hasher(hashInput)); } lock.packages.push_back(std::move(lp)); diff --git a/src/manifest.cppm b/src/manifest.cppm index d9aa022..86424d7 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -246,6 +246,10 @@ list_xpkg_versions(std::string_view luaContent, std::string_view platform); // Returns empty string if the field is absent (legacy descriptors). std::string extract_xpkg_namespace(std::string_view luaContent); +// Extract the `name` field from an xpkg .lua's `package = { ... }` block. +// Returns empty string if the field is absent. +std::string extract_xpkg_name(std::string_view luaContent); + // Resolve the lib-root path for a manifest: // 1. `[lib].path` if explicitly set (cargo-style override), // 2. otherwise the convention `src/.cppm`, where @@ -885,7 +889,7 @@ std::expected parse_string(std::string_view content, // Accepted forms: // acme = "git@gitlab.example.com:platform/mcpp-index.git" # short: value = url // acme-stable = { url = "git@...", tag = "v2.0" } # long: inline table - // local-dev = { path = "/home/user/my-packages" } # local path + // local-dev = { path = "/my-packages" } # local path // mcpplibs = { url = "https://...", rev = "abc123" } # pin built-in if (auto* indices_t = doc->get_table("indices")) { for (auto& [k, v] : *indices_t) { @@ -1180,6 +1184,34 @@ std::string top_level_table_body_for_key(std::string_view body, std::string_view return {}; } +std::string top_level_string_value_for_key(std::string_view body, std::string_view wantedKey) { + LuaCursor cur { body }; + cur.skip_ws_and_comments(); + while (!cur.eof()) { + auto key = cur.read_key(); + if (key.empty()) { + cur.skip_ws_and_comments(); + if (cur.eof()) break; + ++cur.pos; + continue; + } + cur.skip_ws_and_comments(); + if (!cur.consume('=')) { + cur.skip_ws_and_comments(); + continue; + } + cur.skip_ws_and_comments(); + if (key == wantedKey && (cur.peek() == '"' || cur.peek() == '\'')) { + return cur.read_string(); + } + if (cur.peek() == '{') cur.skip_table(); + else if (cur.peek() == '"' || cur.peek() == '\'') (void)cur.read_string(); + else (void)cur.read_bareword(); + cur.skip_ws_and_comments(); + } + return {}; +} + // Strip Lua line comments (`-- ...\n`) and string contents from text, // replacing them with spaces of the same length so positions are // preserved. This is a simple-but-correct way to make the scanner @@ -1315,26 +1347,15 @@ McppField extract_mcpp_field(std::string_view luaContent) { } std::string extract_xpkg_namespace(std::string_view luaContent) { - // Look for `namespace = "..."` inside the `package = { ... }` block. - // Use sanitized text (comments/strings stripped) for key search, - // then read the quoted value from the original text. - auto sanitized = strip_lua_comments_and_strings(luaContent); - auto pos = sanitized.find("namespace"); - if (pos == std::string::npos) return {}; - // Walk past "namespace" + optional whitespace + "=" - auto p = pos + 9; // strlen("namespace") - while (p < sanitized.size() && (sanitized[p] == ' ' || sanitized[p] == '\t')) ++p; - if (p >= sanitized.size() || sanitized[p] != '=') return {}; - ++p; - while (p < sanitized.size() && (sanitized[p] == ' ' || sanitized[p] == '\t')) ++p; - // Read the quoted string from ORIGINAL text at the same offset. - if (p >= luaContent.size() || luaContent[p] != '"') return {}; - ++p; - std::string result; - while (p < luaContent.size() && luaContent[p] != '"') { - result.push_back(luaContent[p++]); - } - return result; + auto packageBody = top_level_table_body_for_key(luaContent, "package"); + if (packageBody.empty()) return {}; + return top_level_string_value_for_key(packageBody, "namespace"); +} + +std::string extract_xpkg_name(std::string_view luaContent) { + auto packageBody = top_level_table_body_for_key(luaContent, "package"); + if (packageBody.empty()) return {}; + return top_level_string_value_for_key(packageBody, "name"); } std::vector diff --git a/src/pm/dependency_selector.cppm b/src/pm/dependency_selector.cppm index d448b8c..a6920a0 100644 --- a/src/pm/dependency_selector.cppm +++ b/src/pm/dependency_selector.cppm @@ -71,6 +71,10 @@ inline DependencySelector resolve_dependency_selector( .namespace_ = std::string(kDefaultNamespace), .shortName = segments.front(), }); + out.candidates.push_back(DependencyCoordinate{ + .namespace_ = {}, + .shortName = segments.front(), + }); return out; } diff --git a/src/pm/lock_io.cppm b/src/pm/lock_io.cppm index a5b4e17..3684b7c 100644 --- a/src/pm/lock_io.cppm +++ b/src/pm/lock_io.cppm @@ -27,7 +27,7 @@ struct LockedIndex { struct LockedPackage { std::string name; - std::string namespace_; // index namespace (v2+); empty = "mcpplibs" + std::string namespace_; // package namespace (v2+); empty = independent root std::string version; std::string source; // e.g. "index+mcpplibs@abc123def..." std::string hash; // "sha256:..." or "fnv1a:..." diff --git a/tests/e2e/63_bare_dependency_peer_root_priority.sh b/tests/e2e/63_bare_dependency_peer_root_priority.sh new file mode 100755 index 0000000..61ccf31 --- /dev/null +++ b/tests/e2e/63_bare_dependency_peer_root_priority.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# requires: gcc fresh-sandbox +# Bare selectors try mcpplibs first, then an independent peer-root package. +# This test provides only an independent root `imgui` package in the default +# index and verifies `imgui = "1.0.0"` does not resolve as mcpplibs.imgui. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +INDEX_DIR="$MCPP_HOME/registry/data/mcpplibs" +PKG_ROOT="$MCPP_HOME/registry/data/xpkgs/mcpplibs-x-imgui/1.0.0" +mkdir -p "$INDEX_DIR/pkgs/i" "$PKG_ROOT/src" +printf 'ok\n' > "$INDEX_DIR/.mcpp-index-updated" + +cat > "$INDEX_DIR/pkgs/i/imgui.lua" <<'EOF' +package = { + spec = "1", + name = "imgui", + description = "Independent bare selector fallback test package", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/imgui-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/imgui.cppm" }, + targets = { ["imgui"] = { kind = "lib" } }, + deps = {}, + }, +} +EOF + +cat > "$PKG_ROOT/src/imgui.cppm" <<'EOF' +export module imgui; + +export int imgui_value() { + return 42; +} +EOF +printf 'ok\n' > "$PKG_ROOT/.mcpp_ok" + +mkdir -p "$TMP/project/app/src" +cd "$TMP/project/app" + +cat > src/main.cpp <<'EOF' +import imgui; + +int main() { + return imgui_value() == 42 ? 0 : 1; +} +EOF + +cat > mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[dependencies] +imgui = "1.0.0" + +[targets.app] +kind = "bin" +main = "src/main.cpp" +EOF + +"$MCPP" build > build.log 2>&1 || { + cat build.log + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + exit 1 +} + +grep -q '\[package."imgui"\]' mcpp.lock || { + cat mcpp.lock + echo "expected imgui package lock entry" + exit 1 +} + +if grep -q 'namespace = "mcpplibs"' mcpp.lock; then + cat mcpp.lock + echo "bare independent package should not lock as mcpplibs" + exit 1 +fi + +echo "OK" diff --git a/tests/unit/test_pm_compat.cpp b/tests/unit/test_pm_compat.cpp index 2ea4f45..6a55319 100644 --- a/tests/unit/test_pm_compat.cpp +++ b/tests/unit/test_pm_compat.cpp @@ -60,6 +60,19 @@ TEST(DependencySelector, DottedSelectorBuildsOmittedMcpplibsPriorityCandidates) EXPECT_EQ(selector.candidates[1].shortName, "glfw_opengl3"); } +TEST(DependencySelector, BareSelectorBuildsOmittedMcpplibsThenPeerRootCandidates) { + auto selector = mcpp::pm::resolve_dependency_selector( + "imgui", + mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + + EXPECT_EQ(selector.stableMapKey, "imgui"); + ASSERT_EQ(selector.candidates.size(), 2u); + EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs"); + EXPECT_EQ(selector.candidates[0].shortName, "imgui"); + EXPECT_EQ(selector.candidates[1].namespace_, ""); + EXPECT_EQ(selector.candidates[1].shortName, "imgui"); +} + TEST(DependencySelector, ExplicitMcpplibsPrefixDoesNotAddPeerFallback) { auto selector = mcpp::pm::resolve_dependency_selector( "mcpplibs.capi.lua", From a9a7665d028cb82b2a37784a45b15b8b0f6d5139 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 2 Jun 2026 20:52:01 +0800 Subject: [PATCH 2/3] chore: bump mcpp to 0.0.45 --- CHANGELOG.md | 11 +++++++++++ mcpp.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b26d9..db5d03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.45] — 2026-06-02 + +### 修复 + +- 修复裸依赖选择器无法 fallback 到独立 root 包的问题。现在 + `imgui = "0.0.1"` 会先尝试省略前缀的 `mcpplibs/imgui`,若候选包身份不匹配, + 会继续匹配独立 root `imgui`,避免把非 `mcpplibs` 体系的包误解析为 + `mcpplibs.imgui`。 +- 选择候选 xpkg 描述时校验 `package.name` / `package.namespace`,并在 lockfile + 中保留独立 root 包的空 namespace 身份。 + ## [0.0.44] — 2026-06-02 ### 修复 diff --git a/mcpp.toml b/mcpp.toml index 5ae4710..525e207 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.44" +version = "0.0.45" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] From 90702f57e54a781f2eee42d7e12f01820b51383a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Tue, 2 Jun 2026 21:06:55 +0800 Subject: [PATCH 3/3] chore: sync mcpp version constant to 0.0.45 --- src/toolchain/fingerprint.cppm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index d649732..a5b41d9 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.44"; +inline constexpr std::string_view MCPP_VERSION = "0.0.45"; struct FingerprintInputs { Toolchain toolchain;