diff --git a/.agents/docs/2026-06-02-dotted-dependency-selectors.md b/.agents/docs/2026-06-02-dotted-dependency-selectors.md new file mode 100644 index 0000000..958c9dd --- /dev/null +++ b/.agents/docs/2026-06-02-dotted-dependency-selectors.md @@ -0,0 +1,303 @@ +# Dotted Dependency Selector Architecture + +Date: 2026-06-02 +Branch: `codex/dotted-dependency-analysis` +Status: locally verified on feature branch; PR/CI/release still pending. + +## Scope + +This document evaluates how `mcpp` should support dotted dependency selectors +such as: + +```toml +[dependencies] +cmdline = "0.0.1" +capi.lua = "0.0.3" +compat.gtest = "1.15.2" +imgui.core = "0.0.1" +imgui.backend.glfw_opengl3 = "0.0.1" +``` + +The goal is not only to make one manifest example pass. The goal is a coherent +dependency model for a module-oriented package manager while preserving current +manifests, xpkg compatibility, workspace behavior, and CLI ergonomics. + +Local verification environment is a separate concern. If the local shell picks +an old `mcpp`, switch it explicitly through xlings: + +```bash +xlings use mcpp 0.0.42 +``` + +That fixes local tool selection. It does not define the dependency selector +architecture. + +## Requirements + +1. A single `[dependencies]` table can express both package namespaces and + multi-level package/module names. +2. `mcpplibs` may be omitted by users as a priority alias, but it is not the + only root namespace. +3. Explicit namespaces such as `compat` still work. +4. Multi-level names such as `imgui.backend.glfw_opengl3` are first-class. +5. Root dependencies, dev/build dependencies, workspace dependencies, xpkg + `mcpp.deps`, and CLI add/remove must share the same selector rules. +6. Existing manifests and quoted legacy dotted keys remain accepted. +7. The resolver must be deterministic in candidate ordering. Package lookup may + then select the first available candidate from local/index metadata. + +## Current Code Findings + +The current implementation is close in some places but is not yet one +architecture: + +- `src/libs/toml.cppm` already parses unquoted TOML dotted keys into nested + tables. This is why `[dependencies] compat.gtest = "..."` can be distinguished + from a quoted flat key. +- `src/libs/toml.cppm` also tracks explicit tables. That distinction matters: + `[dependencies.imgui] core = "..."` is not the same user intent as + `[dependencies] imgui.core = "..."`. +- `src/manifest.cppm` has namespace-aware logic for part of root dependency + parsing, but other dependency surfaces still split strings differently. +- Workspace dependency parsing and synthesized xpkg `mcpp.deps` parsing do not + currently share the same selector model. +- `src/pm/compat.cppm` already owns package lookup compatibility and index/file + naming fallback logic. It is a reasonable temporary home for compatibility + helpers, but canonical selector parsing should be conceptually separate from + legacy fallback behavior. +- `src/pm/package_fetcher.cppm` and `src/pm/resolver.cppm` already consume a + structured package namespace plus package name. The missing piece is upstream + normalization before dependencies reach those layers. +- `src/pm/commands.cppm` still needs the same user selector rules for add/remove + so CLI behavior matches manifest behavior. + +## Design Decision + +Use one canonical dependency selector resolver at manifest/command ingestion +time. The resolver should produce ordered package coordinate candidates: + +```text +candidate(namespace + shortName)[] + stableMapKey +``` + +The important rule is that `mcpplibs` is an optional prefix with priority, not a +forced root. `imgui`, `compat`, `mcpplibs`, and custom index namespaces are +peer roots. For selector `a.b`, lookup should first try the omitted-mcpplibs +candidate, then fall back to the literal peer-root candidate. + +This keeps the interpretation explicit and ordered. The resolver itself should +not perform network work; package lookup can resolve the ordered candidates +against already available index metadata. + +## Selector Semantics + +Omitted `mcpplibs` priority: + +| selector | candidate priority | stable map key | +| --- | --- | --- | +| `cmdline` | `mcpplibs/cmdline` | `cmdline` | +| `capi.lua` | `mcpplibs.capi/lua`, then `capi/lua` | `capi.lua` | +| `imgui.core` | `mcpplibs.imgui/core`, then `imgui/core` | `imgui.core` | +| `imgui.backend.glfw_opengl3` | `mcpplibs.imgui.backend/glfw_opengl3`, then `imgui.backend/glfw_opengl3` | `imgui.backend.glfw_opengl3` | + +Fully explicit prefix and peer-root fallback: + +| selector | candidate priority | stable map key | +| --- | --- | --- | +| `mcpplibs.capi.lua` | `mcpplibs.capi/lua` | `mcpplibs.capi.lua` | +| `compat.gtest` | `mcpplibs.compat/gtest`, then `compat/gtest` | `compat.gtest` | + +Explicit subtable: + +```toml +[dependencies.compat] +gtest = "1.15.2" +``` + +This remains a direct explicit namespace form and should resolve to +`compat/gtest`, not to an omitted-mcpplibs candidate. Explicit subtables are the +clearest form for non-default or custom index namespaces when ambiguity matters. + +## Namespace Roots + +Peer namespace roots include: + +- `mcpplibs` +- `compat` +- `imgui` and future module family roots if they exist as package indexes +- explicit names declared in `[indices]` +- explicit dependency table roots such as `[dependencies.acme]` + +Unquoted dotted selectors in the single `[dependencies]` table do not create a +new namespace unconditionally. They create an ordered lookup: + +```text +a.b.c -> mcpplibs.a.b/c, then a.b/c +``` + +This is the key rule: `mcpplibs` has priority because it may be omitted, but +`a.b` remains a valid peer namespace fallback. + +For custom index names, the deterministic rule should be: + +- If an explicit subtable is used, treat the subtable name as the root. +- If a single-table dotted selector is used, try the omitted-mcpplibs candidate + first, then the literal peer-root candidate. +- If users need to bypass priority matching, they can use an explicit subtable + such as `[dependencies.compat]` or a fully explicit selector such as + `mcpplibs....`. + +This avoids unordered probing while still giving users an escape hatch. + +## Resolver Shape + +Recommended API shape: + +```cpp +struct DependencySelectorContext { + std::unordered_set explicitNamespaceRoots; + std::string defaultNamespace = "mcpplibs"; +}; + +struct DependencySelector { + std::vector candidates; + std::string stableMapKey; + bool explicitRoot = false; +}; + +DependencySelector resolve_dependency_selector( + std::span segments, + const DependencySelectorContext& context); +``` + +The important design point is not the exact C++ names. The important point is +that all dependency entry points call one resolver instead of each splitting on +`.` independently. + +## Ingestion Points + +Apply the resolver at the edges: + +- `src/manifest.cppm`: root dependencies, dev-dependencies, build-dependencies. +- `src/manifest.cppm`: workspace dependencies. +- `src/manifest.cppm`: synthesized dependencies from Lua xpkg `mcpp.deps`. +- `src/pm/commands.cppm`: `mcpp add`, `mcpp remove`, and related CLI parsing. +- Documentation examples in `docs/05-mcpp-toml.md`. + +After candidate generation, package fetch/build/resolution should select the +first available structured package coordinate. That keeps user-facing selector +syntax out of lower build layers while preserving the requested priority +matching behavior. + +## Compatibility Policy + +Keep all existing supported forms: + +- `[dependencies] cmdline = "..."` +- `[dependencies.mcpplibs] cmdline = "..."` +- `[dependencies.compat] gtest = "..."` +- quoted `"mcpplibs.cmdline" = "..."` +- quoted legacy dotted keys where currently accepted +- path/git inline specs +- visibility/use requirements from the existing dependency model + +For one-segment omitted-mcpplibs dependencies, keep the stable map key as the +old bare key (`cmdline`) to avoid unnecessary lockfile or update churn. For +multi-segment selectors, preserving user spelling such as `imgui.core` is more +appropriate than rewriting everything to `mcpplibs.imgui.core`, because the +selector is an ordered match rather than a forced namespace rewrite. + +Quoted flat dotted keys should remain compatibility input, not the primary new +syntax. The canonical user-facing syntax should be unquoted TOML dotted keys or +explicit dependency subtables. + +## Alternatives Considered + +### A. Canonical Selector Resolver + +Recommended. It gives one deterministic rule set and keeps compatibility logic +at the ingestion boundary. + +### B. Patch Each Parser Site Independently + +Rejected. It may fix the first failing test but preserves divergent behavior +between root deps, workspace deps, xpkg deps, and CLI commands. + +### C. Unordered Index-Probing Fallback + +Rejected. Arbitrary probing that changes priority based on current index +contents is fragile. Ordered candidate lookup is different: the selector has a +fixed priority list, and package resolution selects the first candidate that is +available. + +## Verification Plan + +Unit tests: + +- Selector matrix in the PM layer. +- Root dependency dotted selectors. +- Dev/build dependency dotted selectors. +- Workspace dependency dotted selectors. +- Synthesized xpkg `mcpp.deps` dotted selectors. +- CLI add/remove selector normalization. +- Quoted legacy dotted key compatibility. +- Explicit custom index namespace behavior. + +End-to-end tests: + +- A local-index package using `compat.gtest`. +- A local-index package using `imgui.core`. +- A local-index package using `imgui.backend.glfw_opengl3`. + +Before running local tests, pin the shell to the intended mcpp version: + +```bash +xlings use mcpp 0.0.42 +mcpp test +``` + +## Implementation Progress + +- 2026-06-02: Added `mcpp.pm.dependency_selector` with ordered candidates. +- 2026-06-02: Extended `DependencySpec` with ordered candidate coordinates. +- 2026-06-02: Updated root dependencies, dev/build dependencies, workspace + dependencies, and xpkg `mcpp.deps` synthesis to preserve dotted selector + spelling while recording candidate priority. +- 2026-06-02: Updated dependency resolution to select the first candidate whose + strict canonical xpkg.lua entry exists. This avoids legacy fallback lookup + accidentally treating `compat.gtest` as `mcpplibs.compat/gtest`. +- 2026-06-02: Added unit coverage for selector candidates and manifest parsing. +- 2026-06-02: Added e2e coverage for `imgui.core` falling back from + `mcpplibs.imgui/core` to peer-root `imgui/core`. +- 2026-06-02: Updated `mcpp add` so dotted input stays in the single + `[dependencies]` table; `ns:name` remains the explicit subtable syntax. +- 2026-06-02: Full unit tests and targeted local-index/preinstall e2e checks + pass locally. +- 2026-06-02: Bumped the pending release version to `0.0.43` and added the + changelog entry for dotted dependency selectors. +- 2026-06-02: CI exposed that `--version` also uses the hardcoded + `MCPP_VERSION` in `src/toolchain/fingerprint.cppm`; synchronized it to + `0.0.43`. + +Current local checks: + +```bash +xlings use mcpp 0.0.42 +mcpp test -- --gtest_filter='DependencySelector.*:Manifest.DependenciesDottedSelectorPreservesUserKeyAndCandidates:Manifest.DependenciesNamespacedSubtableNestedDottedKeyIsCanonical:SynthesizeFromXpkgLua.DepsDottedSelectorsUseManifestRules:Manifest.WorkspaceDependenciesUseDottedSelectorRules' +mcpp build +MCPP=target/.../bin/mcpp bash tests/e2e/62_dotted_dependency_selector_priority.sh +MCPP=target/.../bin/mcpp bash tests/e2e/12_add_command.sh +MCPP=target/.../bin/mcpp bash tests/e2e/52_local_path_namespaced_index.sh +MCPP=target/.../bin/mcpp bash tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh +``` + +## Resolved Decisions + +1. Declared custom `[indices]` roots do not turn single-table dotted selectors + into explicit roots. Single-table selectors still use omitted-mcpplibs + priority: try `mcpplibs.` first, then `.`. +2. The selector resolver lives in `src/pm/dependency_selector.cppm`. + `compat.cppm` remains focused on legacy dotted-key and xpkg filename + compatibility. +3. CLI add/remove preserve user spelling for dotted selectors. Explicit + namespace subtables remain available through `ns:name`. diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4bb3f..d369e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.43] — 2026-06-02 + +### 新增 + +- 支持在单个 `[dependencies]` / `[dev-dependencies]` / + `[build-dependencies]` / `[workspace.dependencies]` 表中使用多段 dotted + dependency selector,例如 `imgui.core = "..."` 会先尝试 + `mcpplibs.imgui/core`,未命中时再尝试同级根 `imgui/core`。 +- `xpkg.lua` 的 `mcpp.deps` 支持同样的 dotted selector 规则,方便 compat、 + imgui 等生态根和 `mcpplibs` 并列演进。 + +### 改进 + +- `mcpp add` 默认保留用户写入的 dotted selector,显式 namespace 仍可使用 + `ns:name` 写入 `[dependencies.]`。 + ## [0.0.42] — 2026-06-01 ### 新增 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index f87f9a7..a580992 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -118,18 +118,29 @@ path = "src/capi/lua.cppm" # 覆盖默认的 lib-root 位置 ### 2.5 `[dependencies]` — 运行时依赖 ```toml -# 默认命名空间(mcpp)下的包 +# 默认包空间(mcpplibs)下的包 [dependencies] gtest = "1.15.2" # 精确版本 mbedtls = "3.6.1" ftxui = "6.1.9" +# dotted selector: 先匹配 mcpplibs., 找不到再匹配同级 peer root。 +# 例如 imgui.core 会按顺序尝试 mcpplibs.imgui/core, imgui/core。 +[dependencies] +capi.lua = "0.0.3" +compat.gtest = "1.15.2" +imgui.core = "0.0.1" +imgui.backend.glfw_opengl3 = "0.0.1" + # 命名空间子表写法 [dependencies.mcpplibs] cmdline = "0.0.2" tinyhttps = "0.2.2" llmapi = "0.2.5" +[dependencies.compat] +glfw = "3.4" # 显式 namespace, 不走 mcpplibs 优先候选 + # 路径依赖(本地开发) [dependencies] mylib = { path = "../mylib" } diff --git a/docs/06-workspace.md b/docs/06-workspace.md index 058a63b..b5183d4 100644 --- a/docs/06-workspace.md +++ b/docs/06-workspace.md @@ -100,6 +100,7 @@ mbedtls.workspace = true # 根 mcpp.toml [workspace.dependencies] cmdline = "0.0.2" +capi.lua = "0.0.3" # dotted selector: mcpplibs.capi/lua, then capi/lua [workspace.dependencies.compat] mbedtls = "3.6.1" diff --git a/mcpp.toml b/mcpp.toml index b6a4865..a71b880 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.42" +version = "0.0.43" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/cli.cppm b/src/cli.cppm index e69fca4..254842f 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1628,12 +1628,127 @@ prepare_build(bool print_fingerprint, -> const mcpp::pm::IndexSpec* { if (ns.empty() || ns == std::string(mcpp::pm::kDefaultNamespace)) return nullptr; + if (auto it = m->indices.find(ns); it != m->indices.end()) { + return &it->second; + } + auto root = ns.substr(0, ns.find('.')); for (auto& [idxName, spec] : m->indices) { if (idxName == ns) return &spec; + if (idxName == root) return &spec; } return nullptr; }; + auto canonicalXpkgLuaFilename = + [](std::string_view ns, std::string_view shortName) { + if (ns.empty() || ns == mcpp::pm::kDefaultNamespace) { + return std::string(shortName) + ".lua"; + } + return std::format("{}.{}.lua", ns, shortName); + }; + + auto readStrictLuaFromPkgsDir = + [&](const std::filesystem::path& pkgsDir, + std::string_view ns, + std::string_view shortName) -> std::optional + { + auto fname = canonicalXpkgLuaFilename(ns, shortName); + if (fname.empty()) return std::nullopt; + char first = static_cast(std::tolower( + static_cast(fname.front()))); + auto candidate = pkgsDir / std::string(1, first) / fname; + if (!std::filesystem::exists(candidate)) return std::nullopt; + + std::ifstream is(candidate); + std::stringstream ss; + ss << is.rdbuf(); + return ss.str(); + }; + + auto readStrictLuaForCandidate = + [&](const mcpp::pm::DependencyCoordinate& coord) + -> std::optional + { + auto cfg = get_cfg(); + if (!cfg) return std::nullopt; + + auto* idxSpec = findIndexForNs(coord.namespace_); + if (idxSpec && idxSpec->is_local()) { + auto indexPath = mcpp::config::resolve_project_index_path(*root, *idxSpec); + return readStrictLuaFromPkgsDir(indexPath / "pkgs", + coord.namespace_, + coord.shortName); + } + if (idxSpec && !idxSpec->is_builtin()) { + std::error_code ec; + for (auto& data : mcpp::config::project_xlings_data_roots(*root)) { + if (!std::filesystem::exists(data)) continue; + for (auto& entry : std::filesystem::directory_iterator(data, ec)) { + if (!entry.is_directory()) continue; + auto pkgsDir = entry.path() / "pkgs"; + if (auto lua = readStrictLuaFromPkgsDir( + pkgsDir, coord.namespace_, coord.shortName)) { + return lua; + } + } + } + return std::nullopt; + } + + auto data = (*cfg)->xlingsHome() / "data"; + if (!std::filesystem::exists(data)) return std::nullopt; + std::error_code ec; + for (auto& entry : std::filesystem::directory_iterator(data, ec)) { + if (!entry.is_directory()) continue; + auto pkgsDir = entry.path() / "pkgs"; + if (auto lua = readStrictLuaFromPkgsDir( + pkgsDir, coord.namespace_, coord.shortName)) { + return lua; + } + } + return std::nullopt; + }; + + auto dependencyCoordinates = + [](const mcpp::manifest::DependencySpec& spec, + const std::string& depName) { + if (!spec.candidates.empty()) return spec.candidates; + std::vector out; + out.push_back({ + .namespace_ = spec.namespace_.empty() + ? std::string(mcpp::pm::kDefaultNamespace) + : spec.namespace_, + .shortName = spec.shortName.empty() ? depName : spec.shortName, + }); + return out; + }; + + auto selectDependencyCandidate = + [&](mcpp::manifest::DependencySpec& spec, + const std::string& depName) -> std::expected + { + auto candidates = dependencyCoordinates(spec, depName); + if (candidates.empty()) { + return std::unexpected( + std::format("dependency '{}' has no lookup candidates", depName)); + } + + auto selected = candidates.front(); + if (spec.isVersion() && candidates.size() > 1) { + for (auto& candidate : candidates) { + if (readStrictLuaForCandidate(candidate)) { + selected = candidate; + break; + } + } + } + + spec.namespace_ = std::move(selected.namespace_); + spec.shortName = std::move(selected.shortName); + spec.candidates = std::move(candidates); + return {}; + }; + // 0.0.10+: loadVersionDep accepts structured (ns, shortName) for // namespace-aware lookup. depName is the map key (qualified or bare), // kept for install() target formatting and error messages. @@ -1770,6 +1885,12 @@ prepare_build(bool print_fingerprint, childSpec.shortName, childSpec.legacyDottedKey); + if (auto r = selectDependencyCandidate( + childSpec, childName); !r) { + preinstallStack.erase(preinstallKey); + return std::unexpected(r.error()); + } + if (auto r = resolveSemver(childSpec, childName); !r) { preinstallStack.erase(preinstallKey); return std::unexpected(r.error()); @@ -1822,7 +1943,7 @@ prepare_build(bool print_fingerprint, // xlings resolves the package by the descriptor's name field while // still selecting the project-added index. if (useProjectEnv) { - target = std::format("{}:{}@{}", ns, fqname, version); + target = std::format("{}:{}@{}", idxSpec->name, fqname, version); } auto r = install_one(target); if (r && r->exitCode != 0 && @@ -2194,6 +2315,23 @@ prepare_build(bool print_fingerprint, mcpp::pm::compat::normalize_nested_namespace( spec.namespace_, spec.shortName, spec.legacyDottedKey); + if (spec.legacyDottedKey) { + spec.candidates = {{ + .namespace_ = spec.namespace_, + .shortName = spec.shortName, + }}; + } + + if (auto r = selectDependencyCandidate(spec, name); !r) { + return std::unexpected(r.error()); + } + if (item.consumerDepIndex == kMainConsumer) { + if (auto it = m->dependencies.find(name); it != m->dependencies.end()) { + it->second.namespace_ = spec.namespace_; + it->second.shortName = spec.shortName; + it->second.candidates = spec.candidates; + } + } // Pin SemVer constraint before dedup/fetch. if (auto r = resolveSemver(spec, name); !r) { diff --git a/src/manifest.cppm b/src/manifest.cppm index c98826e..d9aa022 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -6,6 +6,7 @@ import std; import mcpp.libs.toml; import mcpp.pm.dep_spec; // M5.x pm/ subsystem refactor: DependencySpec lives here import mcpp.pm.compat; // Legacy dependency-key compatibility helpers +import mcpp.pm.dependency_selector; import mcpp.pm.index_spec; // IndexSpec for [indices] section import mcpp.platform; @@ -550,28 +551,26 @@ std::expected parse_string(std::string_view content, return {}; }; - auto dependency_key = [](std::string_view ns, - std::string_view shortName, - bool bareDefaultKey) { - if (bareDefaultKey) return std::string{shortName}; - return std::format("{}.{}", ns, shortName); - }; - auto assign_dep = [&](std::string_view section, std::map& out, - std::string_view ns, - std::string_view shortName, + const mcpp::pm::DependencySelector& selector, const t::Value& value, - bool legacyDottedKey, - bool bareDefaultKey) + bool legacyDottedKey) -> std::expected { + if (selector.candidates.empty()) { + return std::unexpected(error(origin, std::format( + "[{}] dependency selector '{}' has no candidates", + section, selector.stableMapKey))); + } + DependencySpec spec; - spec.namespace_ = std::string{ns}; - spec.shortName = std::string{shortName}; + spec.namespace_ = selector.candidates.front().namespace_; + spec.shortName = selector.candidates.front().shortName; + spec.candidates = selector.candidates; spec.legacyDottedKey = legacyDottedKey; - auto key = dependency_key(ns, shortName, bareDefaultKey); + auto key = selector.stableMapKey; if (value.is_string()) { spec.version = value.as_string(); } else if (value.is_table()) { @@ -597,28 +596,32 @@ std::expected parse_string(std::string_view content, std::string_view key) { auto path = std::format("{}.{}", section, key); return doc->has_explicit_table(path) - || key == kDefaultNamespace - || key == "compat" - || key.find('.') != std::string_view::npos - || m.indices.find(std::string{key}) != m.indices.end(); + || key == kDefaultNamespace; }; std::function( std::string_view, std::map&, std::string, + std::string, const t::Table&)> load_nested_dep_table; load_nested_dep_table = [&](std::string_view section, std::map& out, std::string ns, + std::string mapPrefix, const t::Table& table) -> std::expected { for (auto& [k, v] : table) { if (v.is_string() || (v.is_table() && looks_like_inline_dep_spec(v.as_table()))) { - if (auto r = assign_dep(section, out, ns, k, v, false, false); !r) + auto mapKey = mapPrefix.empty() + ? k + : std::format("{}.{}", mapPrefix, k); + auto selector = mcpp::pm::make_direct_dependency_selector( + ns, k, mapKey); + if (auto r = assign_dep(section, out, selector, v, false); !r) return r; continue; } @@ -628,7 +631,48 @@ std::expected parse_string(std::string_view content, section, ns, k))); } auto childNs = std::format("{}.{}", ns, k); - if (auto r = load_nested_dep_table(section, out, childNs, v.as_table()); !r) + auto childMapPrefix = mapPrefix.empty() + ? k + : std::format("{}.{}", mapPrefix, k); + if (auto r = load_nested_dep_table( + section, out, childNs, childMapPrefix, v.as_table()); !r) + return r; + } + return {}; + }; + + std::function( + std::string_view, + std::map&, + std::string, + const t::Table&)> load_selector_dep_table; + + load_selector_dep_table = + [&](std::string_view section, + std::map& out, + std::string selectorPrefix, + const t::Table& table) -> std::expected + { + for (auto& [k, v] : table) { + auto selectorText = selectorPrefix.empty() + ? k + : std::format("{}.{}", selectorPrefix, k); + if (v.is_string() || + (v.is_table() && looks_like_inline_dep_spec(v.as_table()))) { + auto selector = mcpp::pm::resolve_dependency_selector( + selectorText, + mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + if (auto r = assign_dep(section, out, selector, v, false); !r) + return r; + continue; + } + if (!v.is_table()) { + return std::unexpected(error(origin, std::format( + "[{}].{} must be a string, inline dep table, or nested table", + section, selectorText))); + } + if (auto r = load_selector_dep_table( + section, out, selectorText, v.as_table()); !r) return r; } return {}; @@ -645,13 +689,16 @@ std::expected parse_string(std::string_view content, if (v.is_string()) { if (k.find('.') != std::string::npos) { auto legacyKey = mcpp::pm::compat::split_legacy_dependency_key(k); - if (auto r = assign_dep(section, out, - legacyKey.namespace_, legacyKey.shortName, - v, legacyKey.legacyDottedKey, false); !r) + auto selector = mcpp::pm::make_direct_dependency_selector( + legacyKey.namespace_, legacyKey.shortName, k); + if (auto r = assign_dep(section, out, selector, v, + legacyKey.legacyDottedKey); !r) return r; continue; } - if (auto r = assign_dep(section, out, kDefaultNamespace, k, v, false, true); !r) + auto selector = mcpp::pm::resolve_dependency_selector( + k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + if (auto r = assign_dep(section, out, selector, v, false); !r) return r; continue; } @@ -670,13 +717,16 @@ std::expected parse_string(std::string_view content, if (looks_like_inline_dep_spec(sub)) { if (k.find('.') != std::string::npos) { auto legacyKey = mcpp::pm::compat::split_legacy_dependency_key(k); - if (auto r = assign_dep(section, out, - legacyKey.namespace_, legacyKey.shortName, - v, legacyKey.legacyDottedKey, false); !r) + auto selector = mcpp::pm::make_direct_dependency_selector( + legacyKey.namespace_, legacyKey.shortName, k); + if (auto r = assign_dep(section, out, selector, v, + legacyKey.legacyDottedKey); !r) return r; continue; } - if (auto r = assign_dep(section, out, kDefaultNamespace, k, v, false, true); !r) + auto selector = mcpp::pm::resolve_dependency_selector( + k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + if (auto r = assign_dep(section, out, selector, v, false); !r) return r; continue; } @@ -684,13 +734,13 @@ std::expected parse_string(std::string_view content, // (2) namespaced or nested subtable. // // Explicit tables such as `[dependencies.acme]` are namespace - // roots. Dotted keys written inside a dependency table, such as - // `[dependencies] capi.lua = "0.0.3"`, are canonical nested - // packages under the default namespace: mcpplibs.capi:lua. - std::string ns = is_namespace_table(section, k) - ? k - : std::format("{}.{}", kDefaultNamespace, k); - if (auto r = load_nested_dep_table(section, out, std::move(ns), sub); !r) { + // roots. Dotted keys written inside the single dependency table, + // such as `[dependencies] capi.lua = "0.0.3"`, are ordered + // selectors: mcpplibs.capi/lua first, then capi/lua. + if (is_namespace_table(section, k)) { + if (auto r = load_nested_dep_table(section, out, k, k, sub); !r) + return r; + } else if (auto r = load_selector_dep_table(section, out, k, sub); !r) { return r; } } @@ -791,25 +841,40 @@ std::expected parse_string(std::string_view content, if (auto* wdeps = doc->get_table("workspace.dependencies")) { for (auto& [k, v] : *wdeps) { if (v.is_string()) { - DependencySpec spec; - spec.version = v.as_string(); - auto depKey = mcpp::pm::compat::split_legacy_dependency_key(k); - spec.namespace_ = std::move(depKey.namespace_); - spec.shortName = std::move(depKey.shortName); - spec.legacyDottedKey = depKey.legacyDottedKey; - m.workspace.dependencies[k] = std::move(spec); + if (k.find('.') != std::string::npos) { + auto depKey = mcpp::pm::compat::split_legacy_dependency_key(k); + auto selector = mcpp::pm::make_direct_dependency_selector( + depKey.namespace_, depKey.shortName, k); + if (auto r = assign_dep("workspace.dependencies", + m.workspace.dependencies, + selector, v, + depKey.legacyDottedKey); !r) { + return std::unexpected(r.error()); + } + continue; + } + auto selector = mcpp::pm::resolve_dependency_selector( + k, mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + if (auto r = assign_dep("workspace.dependencies", + m.workspace.dependencies, + selector, v, false); !r) { + return std::unexpected(r.error()); + } continue; } if (!v.is_table()) continue; - // Namespaced subtable: [workspace.dependencies.] - const std::string ns = k; - for (auto& [sk, sv] : v.as_table()) { - if (!sv.is_string()) continue; - DependencySpec spec; - spec.namespace_ = ns; - spec.shortName = sk; - spec.version = sv.as_string(); - m.workspace.dependencies[std::format("{}.{}", ns, sk)] = std::move(spec); + if (is_namespace_table("workspace.dependencies", k)) { + if (auto r = load_nested_dep_table("workspace.dependencies", + m.workspace.dependencies, + k, k, v.as_table()); !r) { + return std::unexpected(r.error()); + } + } else { + if (auto r = load_selector_dep_table("workspace.dependencies", + m.workspace.dependencies, + k, v.as_table()); !r) { + return std::unexpected(r.error()); + } } } } @@ -1553,11 +1618,15 @@ synthesize_from_xpkg_lua(std::string_view luaContent, if (!dname.empty()) { DependencySpec spec; spec.version = dver; - auto depKey = mcpp::pm::compat::split_legacy_dependency_key(dname); - spec.namespace_ = std::move(depKey.namespace_); - spec.shortName = std::move(depKey.shortName); - spec.legacyDottedKey = depKey.legacyDottedKey; - m.dependencies[dname] = std::move(spec); + auto selector = mcpp::pm::resolve_dependency_selector( + dname, + mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + if (!selector.candidates.empty()) { + spec.namespace_ = selector.candidates.front().namespace_; + spec.shortName = selector.candidates.front().shortName; + spec.candidates = std::move(selector.candidates); + m.dependencies[selector.stableMapKey] = std::move(spec); + } } cur.skip_ws_and_comments(); } diff --git a/src/pm/commands.cppm b/src/pm/commands.cppm index fa5ffe2..7f8d83b 100644 --- a/src/pm/commands.cppm +++ b/src/pm/commands.cppm @@ -56,16 +56,16 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { version = spec.substr(at + 1); } - // Split :. xpkg-style namespace separator. Bare `name` keeps - // the default namespace (mcpp); legacy `ns.name` is also accepted on - // input for ergonomics, but written out in the new subtable form. + // Split :. The colon form is explicit namespace syntax and is + // written as [dependencies.]. Without a colon, keep the user's + // selector spelling in the single [dependencies] table; dotted selectors + // are resolved later by the manifest parser's candidate rules. std::string ns, shortName; + bool explicitNamespace = false; if (auto col = nameSpec.find(':'); col != std::string::npos) { + explicitNamespace = true; ns = nameSpec.substr(0, col); shortName = nameSpec.substr(col + 1); - } else if (auto dot = nameSpec.find('.'); dot != std::string::npos) { - ns = nameSpec.substr(0, dot); - shortName = nameSpec.substr(dot + 1); } else { ns = std::string{mcpp::manifest::kDefaultNamespace}; shortName = nameSpec; @@ -94,25 +94,29 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) { // - Default namespace → `[dependencies] ... name = "version"` (no quotes). // - Other namespace → `[dependencies.] ... name = "version"`, // creating the subtable if absent. - const bool isDefaultNs = (ns == mcpp::manifest::kDefaultNamespace); + const bool isDefaultNs = !explicitNamespace + || ns == mcpp::manifest::kDefaultNamespace; const std::string section = isDefaultNs ? "[dependencies]" : std::format("[dependencies.{}]", ns); + const std::string key = explicitNamespace ? shortName : nameSpec; auto pos = text.find(section); if (pos == std::string::npos) { if (!text.empty() && text.back() != '\n') text += "\n"; - text += std::format("\n{}\n{} = \"{}\"\n", section, shortName, version); + text += std::format("\n{}\n{} = \"{}\"\n", section, key, version); } else { auto nl = text.find('\n', pos); if (nl == std::string::npos) nl = text.size(); - text.insert(nl, std::format("\n{} = \"{}\"", shortName, version)); + text.insert(nl, std::format("\n{} = \"{}\"", key, version)); } { std::ofstream os(manifestPath); os << text; } - std::string display = isDefaultNs ? shortName : std::format("{}:{}", ns, shortName); + std::string display = explicitNamespace + ? (isDefaultNs ? shortName : std::format("{}:{}", ns, shortName)) + : nameSpec; mcpp::ui::status("Adding", std::format("{} v{} to dependencies", display, version)); std::println(""); std::println("Run `mcpp build` to fetch and build with the new dependency."); @@ -134,19 +138,20 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { std::stringstream ss; ss << in.rdbuf(); std::string text = ss.str(); - // Accept the same forms as `mcpp add`: bare `name` (default ns), - // `:`, or legacy `.`. The line we want to delete - // depends on which form the user wrote in mcpp.toml — try every one. + // Accept the same forms as `mcpp add`: bare/dotted selector in the single + // [dependencies] table, or explicit `:` subtable syntax. std::string ns, shortName; + bool explicitNamespace = false; if (auto col = name.find(':'); col != std::string::npos) { + explicitNamespace = true; ns = name.substr(0, col); shortName = name.substr(col + 1); - } else if (auto dot = name.find('.'); dot != std::string::npos) { - ns = name.substr(0, dot); shortName = name.substr(dot + 1); } else { ns = std::string{mcpp::manifest::kDefaultNamespace}; shortName = name; } - const bool isDefaultNs = (ns == mcpp::manifest::kDefaultNamespace); + const bool isDefaultNs = !explicitNamespace + || ns == mcpp::manifest::kDefaultNamespace; + const std::string singleTableKey = explicitNamespace ? shortName : name; bool changed = false; auto erase_line_at = [&](std::size_t p) { @@ -161,8 +166,8 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { // Try bare ` = ` and quoted `"" = ` (default-ns flat form). if (isDefaultNs) { for (const auto& needle : { - std::format("\n{} = ", shortName), - std::format("\n\"{}\" = ", shortName), + std::format("\n{} = ", singleTableKey), + std::format("\n\"{}\" = ", singleTableKey), }) { if (auto p = text.find(needle); p != std::string::npos) { erase_line_at(p + 1); @@ -171,11 +176,9 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { } } - // Try the namespaced subtable form `[dependencies.] = `. - // After deleting the dep line, prune the `[dependencies.]` header - // if no entries remain under it. - if (!isDefaultNs) { - auto sectHeader = std::format("[dependencies.{}]", ns); + auto erase_from_subtable = [&](const std::string& tableNs, + const std::string& tableShort) { + auto sectHeader = std::format("[dependencies.{}]", tableNs); if (auto sp = text.find(sectHeader); sp != std::string::npos) { auto bodyStart = text.find('\n', sp); if (bodyStart == std::string::npos) bodyStart = text.size(); @@ -183,8 +186,8 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { if (sectEnd == std::string::npos) sectEnd = text.size(); std::string section = text.substr(bodyStart, sectEnd - bodyStart); for (const auto& needle : { - std::format("\n{} = ", shortName), - std::format("\n\"{}\" = ", shortName), + std::format("\n{} = ", tableShort), + std::format("\n\"{}\" = ", tableShort), }) { if (auto p = section.find(needle); p != std::string::npos) { auto absStart = bodyStart + p + 1; @@ -221,6 +224,22 @@ inline int cmd_remove(const mcpplibs::cmdline::ParsedArgs& parsed) { } } } + }; + + // Try the namespaced subtable form `[dependencies.] = `. + // After deleting the dep line, prune the `[dependencies.]` header + // if no entries remain under it. + if (!isDefaultNs) { + erase_from_subtable(ns, shortName); + } + + // Backward-compatible removal: before dotted selectors were preserved in + // the single table, `mcpp add acme.util` wrote `[dependencies.acme] util`. + // Keep `mcpp remove acme.util` able to clean that old shape. + if (!changed && !explicitNamespace) { + if (auto dot = name.find('.'); dot != std::string::npos) { + erase_from_subtable(name.substr(0, dot), name.substr(dot + 1)); + } } // Legacy: `[dependencies.] ...` — pre-namespace inline-spec subtable diff --git a/src/pm/dep_spec.cppm b/src/pm/dep_spec.cppm index 8ed54d1..173d7c9 100644 --- a/src/pm/dep_spec.cppm +++ b/src/pm/dep_spec.cppm @@ -15,6 +15,13 @@ import std; export namespace mcpp::pm { +struct DependencyCoordinate { + std::string namespace_; + std::string shortName; + + auto operator<=>(const DependencyCoordinate&) const = default; +}; + // One declared dependency. Path-based deps refer to a sibling mcpp package // on disk; version-based deps come from a registry; git-based deps clone // a remote at a fixed ref. @@ -32,6 +39,7 @@ struct DependencySpec { std::string gitRev; // commit / tag / branch (any one) std::string gitRefKind; // "rev" / "tag" / "branch" (for clarity) std::string visibility = "public"; // public / private / interface + std::vector candidates; // ordered lookup candidates bool inheritWorkspace = false; // .workspace = true bool legacyDottedKey = false; // parsed from legacy "ns.name" flat key diff --git a/src/pm/dependency_selector.cppm b/src/pm/dependency_selector.cppm new file mode 100644 index 0000000..d448b8c --- /dev/null +++ b/src/pm/dependency_selector.cppm @@ -0,0 +1,100 @@ +// mcpp.pm.dependency_selector — parse user dependency selectors into +// ordered package-coordinate candidates. + +export module mcpp.pm.dependency_selector; + +import std; +import mcpp.pm.dep_spec; + +export namespace mcpp::pm { + +enum class DependencySelectorMode { + OmittedMcpplibsPriority, +}; + +struct DependencySelector { + std::vector candidates; + std::string stableMapKey; +}; + +inline std::vector split_dependency_selector(std::string_view selector) +{ + std::vector segments; + std::size_t start = 0; + for (std::size_t i = 0; i <= selector.size(); ++i) { + if (i == selector.size() || selector[i] == '.') { + segments.emplace_back(selector.substr(start, i - start)); + start = i + 1; + } + } + return segments; +} + +inline std::string join_dependency_segments(const std::vector& segments, + std::size_t first, + std::size_t last) +{ + std::string out; + for (std::size_t i = first; i < last && i < segments.size(); ++i) { + if (!out.empty()) out += "."; + out += segments[i]; + } + return out; +} + +inline DependencySelector make_direct_dependency_selector( + std::string_view ns, + std::string_view shortName, + std::string_view stableMapKey) +{ + DependencySelector out; + out.stableMapKey = std::string(stableMapKey); + out.candidates.push_back(DependencyCoordinate{ + .namespace_ = std::string(ns), + .shortName = std::string(shortName), + }); + return out; +} + +inline DependencySelector resolve_dependency_selector( + std::string_view selector, + DependencySelectorMode) +{ + DependencySelector out; + out.stableMapKey = std::string(selector); + + auto segments = split_dependency_selector(selector); + if (segments.empty()) return out; + + if (segments.size() == 1) { + out.candidates.push_back(DependencyCoordinate{ + .namespace_ = std::string(kDefaultNamespace), + .shortName = segments.front(), + }); + return out; + } + + const auto shortName = segments.back(); + const auto nsWithoutShort = join_dependency_segments( + segments, 0, segments.size() - 1); + + if (segments.front() == kDefaultNamespace) { + out.candidates.push_back(DependencyCoordinate{ + .namespace_ = nsWithoutShort, + .shortName = shortName, + }); + return out; + } + + out.candidates.push_back(DependencyCoordinate{ + .namespace_ = std::format("{}.{}", kDefaultNamespace, nsWithoutShort), + .shortName = shortName, + }); + out.candidates.push_back(DependencyCoordinate{ + .namespace_ = nsWithoutShort, + .shortName = shortName, + }); + return out; +} + +} // namespace mcpp::pm diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index cc4104b..2d8e2ac 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.42"; +inline constexpr std::string_view MCPP_VERSION = "0.0.43"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/12_add_command.sh b/tests/e2e/12_add_command.sh index 943cfc4..199b4af 100755 --- a/tests/e2e/12_add_command.sh +++ b/tests/e2e/12_add_command.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash # requires: -# `mcpp add` modifies mcpp.toml [dependencies], including the namespaced form -# `mcpp add :@` which lands under [dependencies.] without -# any TOML key quoting. +# `mcpp add` modifies mcpp.toml [dependencies]. Dotted package selectors are +# preserved in the single table; `ns:name` remains the explicit namespace form. set -e TMP=$(mktemp -d) @@ -36,16 +35,29 @@ grep -qE '^cmdline = "0\.0\.2"$' mcpp.toml || { cat mcpp.toml; echo "cmdline ent "$MCPP" add mcpplibs:templates@0.0.1 > /dev/null grep -qE '^templates = "0\.0\.1"$' mcpp.toml || { cat mcpp.toml; echo "templates entry missing"; exit 1; } -# (5) Legacy dotted form is still accepted on input — written out as namespaced subtable. +# (5) Dotted selector input is preserved under the single [dependencies] table. "$MCPP" add acme.util@2.0.0 > /dev/null -grep -qE '^\[dependencies\.acme\]$' mcpp.toml || { cat mcpp.toml; echo "missing [dependencies.acme] section"; exit 1; } -grep -qE '^util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "util entry missing"; exit 1; } +grep -qE '^acme\.util = "2\.0\.0"$' mcpp.toml || { cat mcpp.toml; echo "acme.util selector entry missing"; exit 1; } -# (6) Reject missing version. +# (6) Colon form remains explicit namespace syntax and uses a subtable. +"$MCPP" add compat:gtest@1.15.2 > /dev/null +grep -qE '^\[dependencies\.compat\]$' mcpp.toml || { cat mcpp.toml; echo "missing [dependencies.compat] section"; exit 1; } +grep -qE '^gtest = "1\.15\.2"$' mcpp.toml || { cat mcpp.toml; echo "gtest entry missing"; exit 1; } + +# (7) Dotted remove can still clean the old subtable shape for compatibility. +cat >> mcpp.toml <<'EOF' + +[dependencies.legacy] +old = "0.1.0" +EOF +"$MCPP" remove legacy.old > /dev/null +! grep -qE '^old = "0\.1\.0"$' mcpp.toml || { cat mcpp.toml; echo "legacy.old was not removed"; exit 1; } + +# (8) Reject missing version. err=$("$MCPP" add bareword 2>&1) && { echo "expected error for missing version"; exit 1; } [[ "$err" == *"version required"* ]] || { echo "wrong error: $err"; exit 1; } -# (7) Reject empty package name (e.g. `mcpp add :foo@1.0`). +# (9) Reject empty package name (e.g. `mcpp add :foo@1.0`). err=$("$MCPP" add ":@1.0" 2>&1) && { echo "expected error for empty package name"; exit 1; } echo "OK" diff --git a/tests/e2e/62_dotted_dependency_selector_priority.sh b/tests/e2e/62_dotted_dependency_selector_priority.sh new file mode 100644 index 0000000..d7b74d7 --- /dev/null +++ b/tests/e2e/62_dotted_dependency_selector_priority.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# requires: gcc fresh-sandbox +# Dotted selectors in a single [dependencies] table use ordered candidates: +# imgui.core -> mcpplibs.imgui/core, then imgui/core. +# This test provides only the peer-root imgui/core package and verifies the +# build resolves through that fallback without network access. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +INDEX_DIR="$TMP/imgui-index" +mkdir -p "$INDEX_DIR/pkgs/i" +cat > "$INDEX_DIR/pkgs/i/imgui.core.lua" <<'EOF' +package = { + spec = "1", + namespace = "imgui", + name = "imgui.core", + description = "Dotted selector fallback test package", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/imgui-core-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/core.cppm" }, + targets = { ["core"] = { kind = "lib" } }, + deps = {}, + }, +} +EOF + +mkdir -p "$TMP/project/app/src" \ + "$TMP/project/app/.mcpp/.xlings/data/xpkgs/imgui-x-imgui.core/1.0.0/src" +cd "$TMP/project/app" + +cat > .mcpp/.xlings/data/xpkgs/imgui-x-imgui.core/1.0.0/src/core.cppm <<'EOF' +export module imgui.core; + +export int imgui_core_value() { + return 42; +} +EOF + +cat > src/main.cpp <<'EOF' +import imgui.core; + +int main() { + return imgui_core_value() == 42 ? 0 : 1; +} +EOF + +cat > mcpp.toml < build.log 2>&1 || { + cat build.log + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + exit 1 +} + +grep -q 'namespace = "imgui"' mcpp.lock || { + cat mcpp.lock + echo "expected lockfile to record resolved peer namespace imgui" + exit 1 +} + +echo "OK" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 5c32831..0734f96 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -456,7 +456,7 @@ version = "0.1.0" EXPECT_TRUE(s.legacyDottedKey); } -TEST(Manifest, DependenciesDefaultNamespaceNestedDottedKeyIsCanonical) { +TEST(Manifest, DependenciesDottedSelectorPreservesUserKeyAndCandidates) { constexpr auto src = R"( [package] name = "x" @@ -469,11 +469,16 @@ capi.lua = "0.0.3" ASSERT_TRUE(m.has_value()) << m.error().format(); ASSERT_EQ(m->dependencies.size(), 1u); - auto& s = m->dependencies.at("mcpplibs.capi.lua"); + auto& s = m->dependencies.at("capi.lua"); EXPECT_EQ(s.namespace_, "mcpplibs.capi"); EXPECT_EQ(s.shortName, "lua"); EXPECT_EQ(s.version, "0.0.3"); EXPECT_FALSE(s.legacyDottedKey); + ASSERT_EQ(s.candidates.size(), 2u); + EXPECT_EQ(s.candidates[0].namespace_, "mcpplibs.capi"); + EXPECT_EQ(s.candidates[0].shortName, "lua"); + EXPECT_EQ(s.candidates[1].namespace_, "capi"); + EXPECT_EQ(s.candidates[1].shortName, "lua"); } TEST(Manifest, DependenciesNamespacedSubtableNestedDottedKeyIsCanonical) { @@ -494,6 +499,9 @@ capi.lua = "0.0.3" EXPECT_EQ(s.shortName, "lua"); EXPECT_EQ(s.version, "0.0.3"); EXPECT_FALSE(s.legacyDottedKey); + ASSERT_EQ(s.candidates.size(), 1u); + EXPECT_EQ(s.candidates[0].namespace_, "mcpplibs.capi"); + EXPECT_EQ(s.candidates[0].shortName, "lua"); } TEST(Manifest, DependenciesInlineSpecCoexistsWithSubtable) { @@ -558,6 +566,52 @@ package = { EXPECT_EQ(b.version, "0.0.2"); } +TEST(SynthesizeFromXpkgLua, DepsDottedSelectorsUseManifestRules) { + constexpr auto src = R"( +package = { + spec = "1", + name = "consumer", + xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, + mcpp = { + sources = { "*/src/*.cppm" }, + deps = { + ["capi.lua"] = "0.0.3", + ["imgui.core"] = "0.0.1", + ["compat.gtest"] = "1.15.2", + }, + targets = { ["consumer"] = { kind = "lib" } }, + }, +} +)"; + auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "consumer", "1.0.0"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->dependencies.size(), 3u); + + auto& lua = m->dependencies.at("capi.lua"); + EXPECT_EQ(lua.namespace_, "mcpplibs.capi"); + EXPECT_EQ(lua.shortName, "lua"); + EXPECT_EQ(lua.version, "0.0.3"); + ASSERT_EQ(lua.candidates.size(), 2u); + EXPECT_EQ(lua.candidates[1].namespace_, "capi"); + EXPECT_EQ(lua.candidates[1].shortName, "lua"); + + auto& imgui = m->dependencies.at("imgui.core"); + EXPECT_EQ(imgui.namespace_, "mcpplibs.imgui"); + EXPECT_EQ(imgui.shortName, "core"); + EXPECT_EQ(imgui.version, "0.0.1"); + ASSERT_EQ(imgui.candidates.size(), 2u); + EXPECT_EQ(imgui.candidates[1].namespace_, "imgui"); + EXPECT_EQ(imgui.candidates[1].shortName, "core"); + + auto& gtest = m->dependencies.at("compat.gtest"); + EXPECT_EQ(gtest.namespace_, "mcpplibs.compat"); + EXPECT_EQ(gtest.shortName, "gtest"); + EXPECT_EQ(gtest.version, "1.15.2"); + ASSERT_EQ(gtest.candidates.size(), 2u); + EXPECT_EQ(gtest.candidates[1].namespace_, "compat"); + EXPECT_EQ(gtest.candidates[1].shortName, "gtest"); +} + TEST(Manifest, WorkspaceSectionParsed) { constexpr auto src = R"( [workspace] @@ -586,6 +640,54 @@ mbedtls = "3.6.1" EXPECT_EQ(gt.namespace_, "compat"); } +TEST(Manifest, WorkspaceDependenciesUseDottedSelectorRules) { + constexpr auto src = R"( +[workspace] +members = ["libs/core"] + +[workspace.dependencies] +capi.lua = "0.0.3" +imgui.core = "0.0.1" +compat.gtest = "1.15.2" +mcpplibs.templates = "0.0.1" +)"; + auto m = mcpp::manifest::parse_string(src); + ASSERT_TRUE(m.has_value()) << m.error().format(); + ASSERT_EQ(m->workspace.dependencies.size(), 4u); + + auto& lua = m->workspace.dependencies.at("capi.lua"); + EXPECT_EQ(lua.namespace_, "mcpplibs.capi"); + EXPECT_EQ(lua.shortName, "lua"); + EXPECT_EQ(lua.version, "0.0.3"); + ASSERT_EQ(lua.candidates.size(), 2u); + EXPECT_EQ(lua.candidates[1].namespace_, "capi"); + EXPECT_EQ(lua.candidates[1].shortName, "lua"); + + auto& imgui = m->workspace.dependencies.at("imgui.core"); + EXPECT_EQ(imgui.namespace_, "mcpplibs.imgui"); + EXPECT_EQ(imgui.shortName, "core"); + EXPECT_EQ(imgui.version, "0.0.1"); + ASSERT_EQ(imgui.candidates.size(), 2u); + EXPECT_EQ(imgui.candidates[1].namespace_, "imgui"); + EXPECT_EQ(imgui.candidates[1].shortName, "core"); + + auto& gt = m->workspace.dependencies.at("compat.gtest"); + EXPECT_EQ(gt.namespace_, "mcpplibs.compat"); + EXPECT_EQ(gt.shortName, "gtest"); + EXPECT_EQ(gt.version, "1.15.2"); + ASSERT_EQ(gt.candidates.size(), 2u); + EXPECT_EQ(gt.candidates[1].namespace_, "compat"); + EXPECT_EQ(gt.candidates[1].shortName, "gtest"); + + auto& tmpl = m->workspace.dependencies.at("mcpplibs.templates"); + EXPECT_EQ(tmpl.namespace_, "mcpplibs"); + EXPECT_EQ(tmpl.shortName, "templates"); + EXPECT_EQ(tmpl.version, "0.0.1"); + ASSERT_EQ(tmpl.candidates.size(), 1u); + EXPECT_EQ(tmpl.candidates[0].namespace_, "mcpplibs"); + EXPECT_EQ(tmpl.candidates[0].shortName, "templates"); +} + TEST(Manifest, WorkspaceTrueInDependency) { constexpr auto src = R"( [package] diff --git a/tests/unit/test_pm_compat.cpp b/tests/unit/test_pm_compat.cpp index c0e9a60..2ea4f45 100644 --- a/tests/unit/test_pm_compat.cpp +++ b/tests/unit/test_pm_compat.cpp @@ -2,6 +2,7 @@ import std; import mcpp.pm.compat; +import mcpp.pm.dependency_selector; TEST(PmCompat, InstallDirCandidatesIncludeNestedNamespaceFallback) { auto candidates = mcpp::pm::compat::install_dir_candidates( @@ -45,3 +46,37 @@ TEST(PmCompat, NormalizeNestedNamespaceSkipsCanonicalNamespacedDeps) { EXPECT_EQ(ns, "mcpplibs.capi"); EXPECT_EQ(shortName, "lua.extra"); } + +TEST(DependencySelector, DottedSelectorBuildsOmittedMcpplibsPriorityCandidates) { + auto selector = mcpp::pm::resolve_dependency_selector( + "imgui.backend.glfw_opengl3", + mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + + EXPECT_EQ(selector.stableMapKey, "imgui.backend.glfw_opengl3"); + ASSERT_EQ(selector.candidates.size(), 2u); + EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs.imgui.backend"); + EXPECT_EQ(selector.candidates[0].shortName, "glfw_opengl3"); + EXPECT_EQ(selector.candidates[1].namespace_, "imgui.backend"); + EXPECT_EQ(selector.candidates[1].shortName, "glfw_opengl3"); +} + +TEST(DependencySelector, ExplicitMcpplibsPrefixDoesNotAddPeerFallback) { + auto selector = mcpp::pm::resolve_dependency_selector( + "mcpplibs.capi.lua", + mcpp::pm::DependencySelectorMode::OmittedMcpplibsPriority); + + EXPECT_EQ(selector.stableMapKey, "mcpplibs.capi.lua"); + ASSERT_EQ(selector.candidates.size(), 1u); + EXPECT_EQ(selector.candidates[0].namespace_, "mcpplibs.capi"); + EXPECT_EQ(selector.candidates[0].shortName, "lua"); +} + +TEST(DependencySelector, ExplicitRootSelectorHasOnlyThatRoot) { + auto selector = mcpp::pm::make_direct_dependency_selector( + "compat", "gtest", "compat.gtest"); + + EXPECT_EQ(selector.stableMapKey, "compat.gtest"); + ASSERT_EQ(selector.candidates.size(), 1u); + EXPECT_EQ(selector.candidates[0].namespace_, "compat"); + EXPECT_EQ(selector.candidates[0].shortName, "gtest"); +}