Skip to content

Commit 940c9d2

Browse files
authored
feat: explicit namespace field + compat shim (0.0.6) (#23)
* feat: explicit namespace field + compat shim module (0.0.6) Adds first-class support for xpkg V1's `namespace` field in both mcpp.toml and xpkg .lua descriptors: [package] namespace = "mcpplibs" # NEW (0.0.6+) name = "cmdline" # now the short name only Previously, namespace was inferred by splitting on the first dot in `name = "mcpplibs.cmdline"`. That legacy form keeps working via the new `src/pm/compat.cppm` shim module, which centralises all backward- compatibility logic: * `resolve_package_name(name, ns)` — canonical (ns field) vs legacy (dotted split) vs bare (default ns "mcpp"). Rule priority: explicit field > first-dot split > default. * `qualified_name(ns, short)` — reconstruct "mcpplibs.cmdline" when needed (e.g. xpkgs directory lookup, error messages). * `xpkg_dir_name(index, ns, short)` — encapsulate the xpkgs/ directory naming convention in one place. DEPRECATION SCHEDULE (documented in compat.cppm header): The dotted-name-as-namespace convention is slated for removal in mcpp 1.0.0. Projects should migrate to the canonical `namespace` field before that point. Other changes: * manifest.cppm: Package struct gains `namespace_` field; TOML parser reads `package.namespace`; new `extract_xpkg_namespace()` reads the field from xpkg .lua descriptors. * cli.cppm: dep name matching uses compat::resolve_package_name; lua-level namespace propagates into loaded manifests. * Version bump to 0.0.6. * feat: namespace-aware install_path + design doc Extends PR #23 with: 1. Design doc (.agents/docs/2026-05-11-namespace-field-design.md) covering the full namespace scheme: mcpplibs / mcpplibs.capi / compat, with xpkgs directory naming, migration path, and deprecation timeline. 2. package_fetcher.cppm: install_path() now supports namespace-aware directory lookup. Priority order: a. New: <namespace>-x-<shortName> (e.g. "mcpplibs-x-cmdline") b. Old: <defaultIndex>-x-<fullName> (e.g. "mcpp-index-x-mcpplibs.cmdline") c. Old short: <defaultIndex>-x-<name> (e.g. "mcpp-index-x-gtest") d. Fallback scan: any directory matching *-x-<name> This ensures both new installs (namespace-aware xpkgs paths) and pre-0.0.6 cached installs (index-prefixed paths) resolve correctly, enabling a smooth migration with no forced cache invalidation.
1 parent fd50e79 commit 940c9d2

7 files changed

Lines changed: 369 additions & 37 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Namespace Field Design — mcpp 0.0.6
2+
3+
> 方案设计文档,指导 namespace 字段的实现和生态迁移。
4+
5+
## 1. 动机
6+
7+
mcpp 生态向 C++23 模块化方向发展。包索引中存在两类库:
8+
9+
- **模块化库**(mcpplibs 生态):原生 `export module`,用 `import` 消费
10+
- **非模块化库**(compat):传统 C/C++ 库,通过 Form B 描述文件 + `#include` 消费
11+
12+
需要在 namespace 层面区分这两类,让用户一眼看出某个依赖是否是模块化的,
13+
同时为未来"非模块化库迁移到模块化"提供清晰的升级路径。
14+
15+
## 2. 命名空间划分
16+
17+
| namespace | 含义 | 示例包 |
18+
|---|---|---|
19+
| `mcpplibs` | mcpplibs 生态的模块化 C++23 库 | cmdline, tinyhttps, llmapi, xpkg, templates |
20+
| `mcpplibs.capi` | mcpplibs 的 C API 模块化封装子集 | lua (封装 Lua C API 为 C++23 module) |
21+
| `compat` | 非模块化的第三方 C/C++ 库(兼容性支持,不鼓励直接使用) | gtest, mbedtls, lua(上游 C 库), ftxui |
22+
23+
### 2.1 默认 namespace
24+
25+
由于 xlings 的 `defaultNamespace = repo.name`(硬编码为索引仓库名 `"mcpp-index"`),
26+
我们采用**每个包显式指定 namespace** 的方案,不依赖默认值。
27+
28+
### 2.2 用户 mcpp.toml 写法
29+
30+
```toml
31+
# 模块化库
32+
[dependencies.mcpplibs]
33+
cmdline = "0.0.2"
34+
tinyhttps = "0.2.2"
35+
llmapi = "0.2.5"
36+
37+
# C API 封装
38+
[dependencies.mcpplibs.capi]
39+
lua = "0.0.3"
40+
41+
# 非模块化兼容库
42+
[dependencies.compat]
43+
gtest = "1.15.2"
44+
mbedtls = "3.6.1"
45+
ftxui = "6.1.9"
46+
lua = "5.4.7" # 上游 C 库(和 mcpplibs.capi.lua 是不同的包)
47+
```
48+
49+
### 2.3 迁移路径
50+
51+
当某个 compat 库完成模块化封装后:
52+
1. 在 mcpplibs 或 mcpplibs.capi 下发布新包
53+
2. compat 版本标记 deprecated(保留一段时间)
54+
3. 用户改一行依赖声明即可迁移
55+
56+
## 3. 索引文件布局
57+
58+
### 3.1 描述文件命名
59+
60+
文件名使用 `<namespace>.<name>.lua` 格式:
61+
62+
```
63+
pkgs/
64+
c/compat.gtest.lua namespace="compat", name="gtest"
65+
c/compat.mbedtls.lua namespace="compat", name="mbedtls"
66+
c/compat.lua.lua namespace="compat", name="lua"
67+
c/compat.ftxui.lua namespace="compat", name="ftxui"
68+
m/mcpplibs.cmdline.lua namespace="mcpplibs", name="cmdline"
69+
m/mcpplibs.tinyhttps.lua namespace="mcpplibs", name="tinyhttps"
70+
m/mcpplibs.llmapi.lua namespace="mcpplibs", name="llmapi"
71+
m/mcpplibs.xpkg.lua namespace="mcpplibs", name="xpkg"
72+
m/mcpplibs.templates.lua namespace="mcpplibs", name="templates"
73+
m/mcpplibs.capi.lua.lua namespace="mcpplibs.capi", name="lua"
74+
```
75+
76+
### 3.2 描述文件格式
77+
78+
```lua
79+
package = {
80+
spec = "1",
81+
namespace = "compat", -- 显式 namespace(0.0.6+)
82+
name = "gtest", -- 短名
83+
...
84+
}
85+
```
86+
87+
### 3.3 xpkgs 安装目录
88+
89+
```
90+
<namespace>-x-<name>/<version>/
91+
92+
compat-x-gtest/1.15.2/
93+
compat-x-mbedtls/3.6.1/
94+
compat-x-lua/5.4.7/
95+
compat-x-ftxui/6.1.9/
96+
mcpplibs-x-cmdline/0.0.2/
97+
mcpplibs-x-tinyhttps/0.2.2/
98+
mcpplibs.capi-x-lua/0.0.3/
99+
```
100+
101+
## 4. mcpp 实现清单
102+
103+
### 4.1 src/pm/compat.cppm (已完成 PR #23)
104+
105+
- `resolve_package_name(name, ns)` — 显式 ns 优先 > 点号拆分 > 默认
106+
- `qualified_name(ns, short)` — 重建完整名
107+
- `xpkg_dir_name(index, ns, short)` — xpkgs 目录名
108+
109+
### 4.2 src/manifest.cppm (已完成 PR #23)
110+
111+
- `Package.namespace_` 字段
112+
- TOML `[package].namespace` 解析
113+
- `extract_xpkg_namespace()` — 从 xpkg lua 读 namespace
114+
115+
### 4.3 src/pm/package_fetcher.cppm (待更新)
116+
117+
`install_path()` 查找逻辑需要同时支持:
118+
- 新路径: `<namespace>-x-<name>`(如 `compat-x-gtest`)
119+
- 老路径: `<defaultIndex>-x-<qualifiedName>`(如 `mcpp-index-x-gtest`)
120+
121+
### 4.4 src/cli.cppm (已完成 PR #23)
122+
123+
- dep 名称匹配走 compat 模块
124+
- lua namespace 传播到 manifest
125+
126+
## 5. 向后兼容
127+
128+
### 5.1 compat.cppm 的三条规则
129+
130+
1.`namespace` 字段 → 直接用(新路径)
131+
2. `name` 带点号 → 按首个点拆分(老路径,deprecated in 1.0.0)
132+
3. 纯短名 → 走 `install_path` 的 fallback 扫描
133+
134+
### 5.2 install_path 双路查找
135+
136+
```
137+
查 <xpkgs>/<namespace>-x-<name>/<version>/ ← 新路径
138+
查 <xpkgs>/<defaultIndex>-x-<qualifiedName>/<version>/ ← 老路径 fallback
139+
```
140+
141+
先找到哪个用哪个。新安装的包走新路径,老缓存继续能用。
142+
143+
## 6. 弃用时间线
144+
145+
| 版本 | 变化 |
146+
|---|---|
147+
| 0.0.6 | namespace 字段支持 + 双路 install_path |
148+
| 0.1.0 | mcpp-index 全面迁移到显式 namespace |
149+
| 1.0.0 | 移除 name 嵌点的 compat 拆分逻辑 |

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.5"
3+
version = "0.0.6"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/cli.cppm

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import mcpp.fetcher;
3333
import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm
3434
import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now
3535
import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexistence)
36+
import mcpp.pm.compat; // 0.0.6: namespace field + dotted-name compat shims
3637
import mcpp.ui;
3738
import mcpp.bmi_cache;
3839
import mcpp.dyndep;
@@ -1133,6 +1134,9 @@ prepare_build(bool print_fingerprint,
11331134
"dependency '{}': index entry not found in local clone", depName));
11341135
auto field = mcpp::manifest::extract_mcpp_field(*luaContent);
11351136

1137+
// 0.0.6+: read explicit namespace from xpkg lua if present.
1138+
auto luaNs = mcpp::manifest::extract_xpkg_namespace(*luaContent);
1139+
11361140
std::optional<mcpp::manifest::Manifest> manifest;
11371141
std::filesystem::path effRoot = verRoot;
11381142
auto loadFrom = [&](const std::filesystem::path& mcppToml)
@@ -1179,6 +1183,13 @@ prepare_build(bool print_fingerprint,
11791183
depName, matches.size()));
11801184
if (auto r = loadFrom(matches.front()); !r) return std::unexpected(r.error());
11811185
}
1186+
// Propagate lua-level namespace into the loaded manifest when
1187+
// the manifest itself doesn't carry one (Form A descriptors
1188+
// whose upstream mcpp.toml predates the namespace field).
1189+
if (manifest->package.namespace_.empty() && !luaNs.empty()) {
1190+
manifest->package.namespace_ = luaNs;
1191+
}
1192+
11821193
return std::pair{effRoot, std::move(*manifest)};
11831194
};
11841195

@@ -1636,27 +1647,24 @@ prepare_build(bool print_fingerprint,
16361647
dep_manifest = std::move(loaded->second);
16371648
}
16381649

1639-
// Name match: prefer the dep's *short* name (the new xpkg-style
1640-
// `[package].name = "<short>"` + separate `namespace` field), but
1641-
// fall back to the legacy composite form `<ns>.<short>` so existing
1642-
// index descriptors that still embed the namespace in the name
1643-
// string (`name = "mcpplibs.cmdline"`) keep resolving until the
1644-
// mcpp-index repo is migrated.
1645-
const std::string& expectedShort =
1646-
spec.shortName.empty() ? name : spec.shortName;
1647-
std::string expectedComposite;
1648-
if (!spec.namespace_.empty()
1649-
&& spec.namespace_ != mcpp::manifest::kDefaultNamespace) {
1650-
expectedComposite = std::format("{}.{}", spec.namespace_, expectedShort);
1651-
}
1652-
const bool nameOk =
1653-
dep_manifest->package.name == expectedShort
1654-
|| (!expectedComposite.empty()
1655-
&& dep_manifest->package.name == expectedComposite);
1656-
if (!nameOk) {
1657-
return std::unexpected(std::format(
1658-
"dependency '{}' resolved to package '{}' (mismatch with declared name '{}')",
1659-
name, dep_manifest->package.name, expectedShort));
1650+
// Name match via compat::resolve_package_name — handles both
1651+
// canonical (explicit namespace field) and legacy (dotted name)
1652+
// forms transparently.
1653+
{
1654+
auto resolved = mcpp::pm::compat::resolve_package_name(
1655+
dep_manifest->package.name, dep_manifest->package.namespace_);
1656+
const std::string& expectedShort =
1657+
spec.shortName.empty() ? name : spec.shortName;
1658+
const bool nameOk =
1659+
resolved.shortName == expectedShort
1660+
|| dep_manifest->package.name == expectedShort
1661+
|| dep_manifest->package.name ==
1662+
mcpp::pm::compat::qualified_name(spec.namespace_, expectedShort);
1663+
if (!nameOk) {
1664+
return std::unexpected(std::format(
1665+
"dependency '{}' resolved to package '{}' (mismatch with declared name '{}')",
1666+
name, dep_manifest->package.name, expectedShort));
1667+
}
16601668
}
16611669

16621670
// Propagate dep's [build].include_dirs to the main manifest. The

src/manifest.cppm

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ inline constexpr auto kDefaultNamespace = mcpp::pm::kDefaultNamespace;
1919

2020
struct Package {
2121
std::string name;
22+
std::string namespace_; // xpkg V1 namespace field (0.0.6+); empty = infer from name
2223
std::string version;
2324
std::string standard = "c++23"; // C++ standard (M5.0: moved from [language])
2425
std::string description;
@@ -208,6 +209,10 @@ McppField extract_mcpp_field(std::string_view luaContent);
208209
std::vector<std::string>
209210
list_xpkg_versions(std::string_view luaContent, std::string_view platform);
210211

212+
// Extract the `namespace` field from an xpkg .lua's `package = { ... }` block.
213+
// Returns empty string if the field is absent (legacy descriptors).
214+
std::string extract_xpkg_namespace(std::string_view luaContent);
215+
211216
// Resolve the lib-root path for a manifest:
212217
// 1. `[lib].path` if explicitly set (cargo-style override),
213218
// 2. otherwise the convention `src/<package-tail>.cppm`, where
@@ -270,6 +275,11 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
270275
if (!name) return std::unexpected(error(origin, "missing required field 'package.name'"));
271276
m.package.name = *name;
272277

278+
// 0.0.6+: explicit namespace field (xpkg V1 style).
279+
// If present, [package].name is the short name.
280+
// If absent, compat.cppm::resolve_package_name infers from dotted name.
281+
if (auto v = doc->get_string("package.namespace")) m.package.namespace_ = *v;
282+
273283
auto version = doc->get_string("package.version");
274284
if (!version) return std::unexpected(error(origin, "missing required field 'package.version'"));
275285
m.package.version = *version;
@@ -923,6 +933,29 @@ McppField extract_mcpp_field(std::string_view luaContent) {
923933
return extract_mcpp_field_impl(luaContent);
924934
}
925935

936+
std::string extract_xpkg_namespace(std::string_view luaContent) {
937+
// Look for `namespace = "..."` inside the `package = { ... }` block.
938+
// Use sanitized text (comments/strings stripped) for key search,
939+
// then read the quoted value from the original text.
940+
auto sanitized = strip_lua_comments_and_strings(luaContent);
941+
auto pos = sanitized.find("namespace");
942+
if (pos == std::string::npos) return {};
943+
// Walk past "namespace" + optional whitespace + "="
944+
auto p = pos + 9; // strlen("namespace")
945+
while (p < sanitized.size() && (sanitized[p] == ' ' || sanitized[p] == '\t')) ++p;
946+
if (p >= sanitized.size() || sanitized[p] != '=') return {};
947+
++p;
948+
while (p < sanitized.size() && (sanitized[p] == ' ' || sanitized[p] == '\t')) ++p;
949+
// Read the quoted string from ORIGINAL text at the same offset.
950+
if (p >= luaContent.size() || luaContent[p] != '"') return {};
951+
++p;
952+
std::string result;
953+
while (p < luaContent.size() && luaContent[p] != '"') {
954+
result.push_back(luaContent[p++]);
955+
}
956+
return result;
957+
}
958+
926959
std::vector<std::string>
927960
list_xpkg_versions(std::string_view luaContent, std::string_view platform) {
928961
// Locate `xpm = { ... <platform> = { ["X.Y.Z"] = {...}, ... } ... }`.

0 commit comments

Comments
 (0)