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
65 changes: 62 additions & 3 deletions .agents/docs/2026-06-02-imgui-mcpp-dependency-fixes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -94,6 +106,28 @@ The imgui validation exposed two separate mcpp-side problems.
- Added a branch dependency regression that verifies `mcpp update <dep>`
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/<name>`, then independent root `<name>`.

- `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:
Expand All @@ -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=<mcpp-bin> bash tests/e2e/60_stale_xpkg_cache_reinstall.sh`
- Result: `OK`

- Boundary regression caught and fixed:
Expand All @@ -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=<mcpp-bin> bash tests/e2e/24_git_dependency.sh`
- Result: `OK`

- Focused regression after the final git dependency change:
Expand All @@ -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=<mcpp-bin> 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=<tmp-home> <mcpp-bin> run`
- Result:
`external git consumer imported Dear ImGui 1.92.8`
- Lock source:
Expand All @@ -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.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 修复
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
57 changes: 47 additions & 10 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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<std::string> 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));
Expand Down
63 changes: 42 additions & 21 deletions src/manifest.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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/<package-tail>.cppm`, where
Expand Down Expand Up @@ -885,7 +889,7 @@ std::expected<Manifest, ManifestError> 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 = "<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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<std::string>
Expand Down
4 changes: 4 additions & 0 deletions src/pm/dependency_selector.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/pm/lock_io.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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:..."
Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading