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
149 changes: 149 additions & 0 deletions .agents/docs/2026-05-11-namespace-field-design.md
Original file line number Diff line number Diff line change
@@ -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 描述文件命名

文件名使用 `<namespace>.<name>.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 安装目录

```
<namespace>-x-<name>/<version>/

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()` 查找逻辑需要同时支持:
- 新路径: `<namespace>-x-<name>`(如 `compat-x-gtest`)
- 老路径: `<defaultIndex>-x-<qualifiedName>`(如 `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 双路查找

```
查 <xpkgs>/<namespace>-x-<name>/<version>/ ← 新路径
查 <xpkgs>/<defaultIndex>-x-<qualifiedName>/<version>/ ← 老路径 fallback
```

先找到哪个用哪个。新安装的包走新路径,老缓存继续能用。

## 6. 弃用时间线

| 版本 | 变化 |
|---|---|
| 0.0.6 | namespace 字段支持 + 双路 install_path |
| 0.1.0 | mcpp-index 全面迁移到显式 namespace |
| 1.0.0 | 移除 name 嵌点的 compat 拆分逻辑 |
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.5"
version = "0.0.6"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
50 changes: 29 additions & 21 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<mcpp::manifest::Manifest> manifest;
std::filesystem::path effRoot = verRoot;
auto loadFrom = [&](const std::filesystem::path& mcppToml)
Expand Down Expand Up @@ -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)};
};

Expand Down Expand Up @@ -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 = "<short>"` + separate `namespace` field), but
// fall back to the legacy composite form `<ns>.<short>` 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
Expand Down
33 changes: 33 additions & 0 deletions src/manifest.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -208,6 +209,10 @@ McppField extract_mcpp_field(std::string_view luaContent);
std::vector<std::string>
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/<package-tail>.cppm`, where
Expand Down Expand Up @@ -270,6 +275,11 @@ std::expected<Manifest, ManifestError> 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;
Expand Down Expand Up @@ -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<std::string>
list_xpkg_versions(std::string_view luaContent, std::string_view platform) {
// Locate `xpm = { ... <platform> = { ["X.Y.Z"] = {...}, ... } ... }`.
Expand Down
Loading
Loading