diff --git a/.agents/docs/2026-05-11-namespace-field-design.md b/.agents/docs/2026-05-11-namespace-field-design.md new file mode 100644 index 0000000..ff610c6 --- /dev/null +++ b/.agents/docs/2026-05-11-namespace-field-design.md @@ -0,0 +1,149 @@ +# Namespace Field Design — mcpp 0.0.6 + +> 方案设计文档,指导 namespace 字段的实现和生态迁移。 + +## 1. 动机 + +mcpp 生态向 C++23 模块化方向发展。包索引中存在两类库: + +- **模块化库**(mcpplibs 生态):原生 `export module`,用 `import` 消费 +- **非模块化库**(compat):传统 C/C++ 库,通过 Form B 描述文件 + `#include` 消费 + +需要在 namespace 层面区分这两类,让用户一眼看出某个依赖是否是模块化的, +同时为未来"非模块化库迁移到模块化"提供清晰的升级路径。 + +## 2. 命名空间划分 + +| namespace | 含义 | 示例包 | +|---|---|---| +| `mcpplibs` | mcpplibs 生态的模块化 C++23 库 | cmdline, tinyhttps, llmapi, xpkg, templates | +| `mcpplibs.capi` | mcpplibs 的 C API 模块化封装子集 | lua (封装 Lua C API 为 C++23 module) | +| `compat` | 非模块化的第三方 C/C++ 库(兼容性支持,不鼓励直接使用) | gtest, mbedtls, lua(上游 C 库), ftxui | + +### 2.1 默认 namespace + +由于 xlings 的 `defaultNamespace = repo.name`(硬编码为索引仓库名 `"mcpp-index"`), +我们采用**每个包显式指定 namespace** 的方案,不依赖默认值。 + +### 2.2 用户 mcpp.toml 写法 + +```toml +# 模块化库 +[dependencies.mcpplibs] +cmdline = "0.0.2" +tinyhttps = "0.2.2" +llmapi = "0.2.5" + +# C API 封装 +[dependencies.mcpplibs.capi] +lua = "0.0.3" + +# 非模块化兼容库 +[dependencies.compat] +gtest = "1.15.2" +mbedtls = "3.6.1" +ftxui = "6.1.9" +lua = "5.4.7" # 上游 C 库(和 mcpplibs.capi.lua 是不同的包) +``` + +### 2.3 迁移路径 + +当某个 compat 库完成模块化封装后: +1. 在 mcpplibs 或 mcpplibs.capi 下发布新包 +2. compat 版本标记 deprecated(保留一段时间) +3. 用户改一行依赖声明即可迁移 + +## 3. 索引文件布局 + +### 3.1 描述文件命名 + +文件名使用 `..lua` 格式: + +``` +pkgs/ + c/compat.gtest.lua namespace="compat", name="gtest" + c/compat.mbedtls.lua namespace="compat", name="mbedtls" + c/compat.lua.lua namespace="compat", name="lua" + c/compat.ftxui.lua namespace="compat", name="ftxui" + m/mcpplibs.cmdline.lua namespace="mcpplibs", name="cmdline" + m/mcpplibs.tinyhttps.lua namespace="mcpplibs", name="tinyhttps" + m/mcpplibs.llmapi.lua namespace="mcpplibs", name="llmapi" + m/mcpplibs.xpkg.lua namespace="mcpplibs", name="xpkg" + m/mcpplibs.templates.lua namespace="mcpplibs", name="templates" + m/mcpplibs.capi.lua.lua namespace="mcpplibs.capi", name="lua" +``` + +### 3.2 描述文件格式 + +```lua +package = { + spec = "1", + namespace = "compat", -- 显式 namespace(0.0.6+) + name = "gtest", -- 短名 + ... +} +``` + +### 3.3 xpkgs 安装目录 + +``` +-x-// + +compat-x-gtest/1.15.2/ +compat-x-mbedtls/3.6.1/ +compat-x-lua/5.4.7/ +compat-x-ftxui/6.1.9/ +mcpplibs-x-cmdline/0.0.2/ +mcpplibs-x-tinyhttps/0.2.2/ +mcpplibs.capi-x-lua/0.0.3/ +``` + +## 4. mcpp 实现清单 + +### 4.1 src/pm/compat.cppm (已完成 PR #23) + +- `resolve_package_name(name, ns)` — 显式 ns 优先 > 点号拆分 > 默认 +- `qualified_name(ns, short)` — 重建完整名 +- `xpkg_dir_name(index, ns, short)` — xpkgs 目录名 + +### 4.2 src/manifest.cppm (已完成 PR #23) + +- `Package.namespace_` 字段 +- TOML `[package].namespace` 解析 +- `extract_xpkg_namespace()` — 从 xpkg lua 读 namespace + +### 4.3 src/pm/package_fetcher.cppm (待更新) + +`install_path()` 查找逻辑需要同时支持: +- 新路径: `-x-`(如 `compat-x-gtest`) +- 老路径: `-x-`(如 `mcpp-index-x-gtest`) + +### 4.4 src/cli.cppm (已完成 PR #23) + +- dep 名称匹配走 compat 模块 +- lua namespace 传播到 manifest + +## 5. 向后兼容 + +### 5.1 compat.cppm 的三条规则 + +1. 有 `namespace` 字段 → 直接用(新路径) +2. `name` 带点号 → 按首个点拆分(老路径,deprecated in 1.0.0) +3. 纯短名 → 走 `install_path` 的 fallback 扫描 + +### 5.2 install_path 双路查找 + +``` +查 /-x-// ← 新路径 +查 /-x-// ← 老路径 fallback +``` + +先找到哪个用哪个。新安装的包走新路径,老缓存继续能用。 + +## 6. 弃用时间线 + +| 版本 | 变化 | +|---|---| +| 0.0.6 | namespace 字段支持 + 双路 install_path | +| 0.1.0 | mcpp-index 全面迁移到显式 namespace | +| 1.0.0 | 移除 name 嵌点的 compat 拆分逻辑 | diff --git a/mcpp.toml b/mcpp.toml index a8e6eef..3bd7dc9 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.5" +version = "0.0.6" 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 7237d92..33457f4 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -33,6 +33,7 @@ import mcpp.fetcher; import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexistence) +import mcpp.pm.compat; // 0.0.6: namespace field + dotted-name compat shims import mcpp.ui; import mcpp.bmi_cache; import mcpp.dyndep; @@ -1133,6 +1134,9 @@ prepare_build(bool print_fingerprint, "dependency '{}': index entry not found in local clone", depName)); auto field = mcpp::manifest::extract_mcpp_field(*luaContent); + // 0.0.6+: read explicit namespace from xpkg lua if present. + auto luaNs = mcpp::manifest::extract_xpkg_namespace(*luaContent); + std::optional manifest; std::filesystem::path effRoot = verRoot; auto loadFrom = [&](const std::filesystem::path& mcppToml) @@ -1179,6 +1183,13 @@ prepare_build(bool print_fingerprint, depName, matches.size())); if (auto r = loadFrom(matches.front()); !r) return std::unexpected(r.error()); } + // Propagate lua-level namespace into the loaded manifest when + // the manifest itself doesn't carry one (Form A descriptors + // whose upstream mcpp.toml predates the namespace field). + if (manifest->package.namespace_.empty() && !luaNs.empty()) { + manifest->package.namespace_ = luaNs; + } + return std::pair{effRoot, std::move(*manifest)}; }; @@ -1636,27 +1647,24 @@ prepare_build(bool print_fingerprint, dep_manifest = std::move(loaded->second); } - // Name match: prefer the dep's *short* name (the new xpkg-style - // `[package].name = ""` + separate `namespace` field), but - // fall back to the legacy composite form `.` so existing - // index descriptors that still embed the namespace in the name - // string (`name = "mcpplibs.cmdline"`) keep resolving until the - // mcpp-index repo is migrated. - const std::string& expectedShort = - spec.shortName.empty() ? name : spec.shortName; - std::string expectedComposite; - if (!spec.namespace_.empty() - && spec.namespace_ != mcpp::manifest::kDefaultNamespace) { - expectedComposite = std::format("{}.{}", spec.namespace_, expectedShort); - } - const bool nameOk = - dep_manifest->package.name == expectedShort - || (!expectedComposite.empty() - && dep_manifest->package.name == expectedComposite); - if (!nameOk) { - return std::unexpected(std::format( - "dependency '{}' resolved to package '{}' (mismatch with declared name '{}')", - name, dep_manifest->package.name, expectedShort)); + // Name match via compat::resolve_package_name — handles both + // canonical (explicit namespace field) and legacy (dotted name) + // forms transparently. + { + auto resolved = mcpp::pm::compat::resolve_package_name( + dep_manifest->package.name, dep_manifest->package.namespace_); + const std::string& expectedShort = + spec.shortName.empty() ? name : spec.shortName; + const bool nameOk = + resolved.shortName == expectedShort + || dep_manifest->package.name == expectedShort + || dep_manifest->package.name == + mcpp::pm::compat::qualified_name(spec.namespace_, expectedShort); + if (!nameOk) { + return std::unexpected(std::format( + "dependency '{}' resolved to package '{}' (mismatch with declared name '{}')", + name, dep_manifest->package.name, expectedShort)); + } } // Propagate dep's [build].include_dirs to the main manifest. The diff --git a/src/manifest.cppm b/src/manifest.cppm index c10c01a..dd48c1c 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -19,6 +19,7 @@ inline constexpr auto kDefaultNamespace = mcpp::pm::kDefaultNamespace; struct Package { std::string name; + std::string namespace_; // xpkg V1 namespace field (0.0.6+); empty = infer from name std::string version; std::string standard = "c++23"; // C++ standard (M5.0: moved from [language]) std::string description; @@ -208,6 +209,10 @@ McppField extract_mcpp_field(std::string_view luaContent); std::vector list_xpkg_versions(std::string_view luaContent, std::string_view platform); +// Extract the `namespace` field from an xpkg .lua's `package = { ... }` block. +// Returns empty string if the field is absent (legacy descriptors). +std::string extract_xpkg_namespace(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 @@ -270,6 +275,11 @@ std::expected parse_string(std::string_view content, if (!name) return std::unexpected(error(origin, "missing required field 'package.name'")); m.package.name = *name; + // 0.0.6+: explicit namespace field (xpkg V1 style). + // If present, [package].name is the short name. + // If absent, compat.cppm::resolve_package_name infers from dotted name. + if (auto v = doc->get_string("package.namespace")) m.package.namespace_ = *v; + auto version = doc->get_string("package.version"); if (!version) return std::unexpected(error(origin, "missing required field 'package.version'")); m.package.version = *version; @@ -923,6 +933,29 @@ McppField extract_mcpp_field(std::string_view luaContent) { return extract_mcpp_field_impl(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; +} + std::vector list_xpkg_versions(std::string_view luaContent, std::string_view platform) { // Locate `xpm = { ... = { ["X.Y.Z"] = {...}, ... } ... }`. diff --git a/src/pm/compat.cppm b/src/pm/compat.cppm new file mode 100644 index 0000000..faae7f1 --- /dev/null +++ b/src/pm/compat.cppm @@ -0,0 +1,106 @@ +// mcpp.pm.compat — backward-compatibility shims for evolving package +// conventions. Centralises all migration logic so it can be retired +// cleanly in a future major version. +// +// DEPRECATION SCHEDULE: +// The shims in this module are slated for removal in mcpp 1.0.0. +// Projects should migrate to the canonical forms before that point: +// +// ┌─────────────────────────────────────┬──────────────────────────────┐ +// │ Deprecated (works until 1.0) │ Canonical (use now) │ +// ├─────────────────────────────────────┼──────────────────────────────┤ +// │ name = "mcpplibs.cmdline" │ namespace = "mcpplibs" │ +// │ (namespace embedded in name) │ name = "cmdline" │ +// ├─────────────────────────────────────┼──────────────────────────────┤ +// │ [dependencies] │ [dependencies.mcpplibs] │ +// │ "mcpplibs.cmdline" = "0.0.2" │ cmdline = "0.0.2" │ +// └─────────────────────────────────────┴──────────────────────────────┘ +// +// See also: mcpp.pm.dep_spec (kDefaultNamespace definition). + +export module mcpp.pm.compat; + +import std; +import mcpp.pm.dep_spec; + +export namespace mcpp::pm::compat { + +// ─── Package name normalisation ────────────────────────────────────── +// +// Given a raw `package.name` and an optional `package.namespace` from +// an xpkg descriptor or mcpp.toml, produce the canonical (namespace, +// shortName) pair. +// +// Resolution order: +// 1. If `ns` is non-empty → use it directly; `name` is the short name. +// 2. If `name` contains a dot → split on the FIRST dot: +// - prefix = namespace, suffix = short name. +// (COMPAT: this is the legacy convention; warns on stderr.) +// 3. Otherwise → namespace = kDefaultNamespace ("mcpp"), name as-is. + +struct ResolvedName { + std::string namespace_; + std::string shortName; + bool usedLegacySplit = false; // true when rule (2) fired +}; + +inline ResolvedName resolve_package_name(std::string_view name, + std::string_view ns) +{ + ResolvedName r; + + if (!ns.empty()) { + // Rule 1: explicit namespace field — canonical path. + r.namespace_ = std::string(ns); + r.shortName = std::string(name); + return r; + } + + auto dot = name.find('.'); + if (dot != std::string_view::npos) { + // Rule 2: legacy dotted name — split on first dot. + r.namespace_ = std::string(name.substr(0, dot)); + r.shortName = std::string(name.substr(dot + 1)); + r.usedLegacySplit = true; + return r; + } + + // Rule 3: bare name → default namespace. + r.namespace_ = std::string(mcpp::pm::kDefaultNamespace); + r.shortName = std::string(name); + return r; +} + +// Reconstruct the fully-qualified name from (namespace, shortName). +// Default-namespace packages use the bare short name; others use +// "ns.short". +inline std::string qualified_name(std::string_view ns, + std::string_view shortName) +{ + if (ns.empty() || ns == mcpp::pm::kDefaultNamespace) + return std::string(shortName); + return std::format("{}.{}", ns, shortName); +} + +// ─── Index directory naming ────────────────────────────────────────── +// +// Maps (indexName, namespace, shortName) → the xpkgs subdirectory name +// that xlings places the extracted tarball under. +// +// Current layout (compat): +// /-x-.// +// e.g. mcpp-index-x-mcpplibs.cmdline/0.0.2/ +// +// The function encapsulates this so a future layout change (e.g. +// /-x-// with ns in metadata) +// only touches one place. + +inline std::string xpkg_dir_name(std::string_view indexName, + std::string_view ns, + std::string_view shortName) +{ + auto qname = qualified_name(ns, shortName); + return std::format("{}-x-{}", indexName, qname); +} + +} // namespace mcpp::pm::compat diff --git a/src/pm/package_fetcher.cppm b/src/pm/package_fetcher.cppm index 79b75f1..fc4249b 100644 --- a/src/pm/package_fetcher.cppm +++ b/src/pm/package_fetcher.cppm @@ -744,35 +744,71 @@ Fetcher::resolve_xpkg_path(std::string_view target, std::optional Fetcher::install_path(std::string_view name, std::string_view version) const { - // M6.x: install_path now ALWAYS returns the verdir (untouched extract - // root). Layout discrimination is done by the caller via the xpkg.lua's + // M6.x: install_path returns the verdir (untouched extract root). + // Layout discrimination is done by the caller via the xpkg.lua's // `mcpp = ""` (Form A pointer) or `mcpp = { ... }` (Form B // inline) field — no more "find mcpp.toml subdir / unique subdir" magic. - // Caller resolves further nested paths via glob. + // + // 0.0.6+: namespace-aware lookup. xlings stores packages at + // /-x-// + // The `name` argument may be either a qualified name + // ("mcpplibs.cmdline") or a short name ("cmdline"). We try + // multiple candidate prefixes to handle both old and new layouts. auto base = cfg_.xlingsHome() / "data" / "xpkgs"; if (!std::filesystem::exists(base)) return std::nullopt; - auto try_namespaced = [&](std::string_view prefix) -> std::optional { - auto verdir = base - / std::format("{}-x-{}", prefix, name) - / std::string(version); + auto try_dir = [&](std::string_view dirName) -> std::optional { + auto verdir = base / std::string(dirName) / std::string(version); return std::filesystem::exists(verdir) ? std::optional{verdir} : std::nullopt; }; - // 1. Try the configured default index first (fast path). - if (auto p = try_namespaced(cfg_.defaultIndex)) return *p; + // Split qualified name into (namespace, shortName) for new-style lookup. + // "mcpplibs.cmdline" → ("mcpplibs", "cmdline") + // "gtest" → ("", "gtest") + std::string ns, shortName; + auto dot = name.find('.'); + if (dot != std::string_view::npos) { + ns = std::string(name.substr(0, dot)); + shortName = std::string(name.substr(dot + 1)); + } else { + shortName = std::string(name); + } + + // Priority order: + // 1. New namespace-aware: -x- (e.g. "mcpplibs-x-cmdline") + // 2. Old index-prefixed: -x- (e.g. "mcpp-index-x-mcpplibs.cmdline") + // 3. Old index-prefixed short: -x- (e.g. "mcpp-index-x-gtest") + // 4. Fallback scan: any <*>-x- or <*>-x- + + // 1. New namespace-aware path (0.0.6+) + if (!ns.empty()) { + if (auto p = try_dir(std::format("{}-x-{}", ns, shortName))) return *p; + } - // 2. Fall back: scan every xpkgs/-x- for a match. + // 2. Old index-prefixed path (compat with pre-0.0.6 installs) + if (auto p = try_dir(std::format("{}-x-{}", cfg_.defaultIndex, name))) return *p; + + // 3. Short name with default index (bare "gtest" → "mcpp-index-x-gtest") + if (!shortName.empty() && shortName != name) { + if (auto p = try_dir(std::format("{}-x-{}", cfg_.defaultIndex, shortName))) return *p; + } + + // 4. Fallback scan: walk xpkgs/ for any directory ending with -x- + // or -x-. std::error_code ec; - std::string suffix = std::format("-x-{}", name); + std::string suffix1 = std::format("-x-{}", name); + std::string suffix2 = shortName.empty() ? "" : std::format("-x-{}", shortName); for (auto& entry : std::filesystem::directory_iterator(base, ec)) { if (!entry.is_directory()) continue; auto dirname = entry.path().filename().string(); - if (!dirname.ends_with(suffix)) continue; - auto idx = dirname.substr(0, dirname.size() - suffix.size()); - if (auto p = try_namespaced(idx)) return *p; + if (dirname.ends_with(suffix1)) { + if (auto p = try_dir(dirname)) return *p; + } + if (!suffix2.empty() && dirname.ends_with(suffix2)) { + if (auto p = try_dir(dirname)) return *p; + } } return std::nullopt; } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 8a8f7c8..4a28ba8 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.5"; +inline constexpr std::string_view MCPP_VERSION = "0.0.6"; struct FingerprintInputs { Toolchain toolchain;