Skip to content
Closed
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
33 changes: 29 additions & 4 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ on:
workflow_dispatch:

env:
# 0.0.105: `mcpp xpkg parse` now enforces INV-NAME itself (mcpp#278) — a
# descriptor whose `namespace` is declared must spell `name` as the
# fully-qualified `<namespace>.<short>`. Pinning the lint here means the
# grammar check below catches the split form directly, and the in-repo
# tests/check_package_name.lua becomes a redundant (cheap, earlier) second
# gate rather than the only one. 0.0.105 also narrows bare-name dependency
# resolution to the mcpplibs / compat / no-namespace search path — irrelevant
# to lint, but it is what the workspace jobs below now build against.
#
# NOTE: index.toml's min_mcpp/latest_mcpp deliberately stay at 0.0.102. The
# floor is a CONSUMER contract; 0.0.105 carries a breaking change (bare names
# no longer reach third-party namespaces) and nothing in this index needs
# 0.0.105 grammar, so there is no reason to force every consumer forward.
# 0.0.102: windows command-line ceiling (mcpp#261 — the clang scan rule got
# its P1689 JSON through shell redirection, which forced a `cmd /c` wrapper
# and with it cmd.exe's 8191-char limit; clang-scan-deps -o removes both, and
Expand Down Expand Up @@ -47,7 +60,7 @@ env:
# 0.0.94 fixed feature-gated `sources` under `mcpp test` (mcpp#218); 0.0.91
# added standard = "c++fly" to the resolver grammar, so c++fly descriptors
# get the lint WARN below, not a hard grammar-parse rejection.
MCPP_VERSION: "0.0.102"
MCPP_VERSION: "0.0.105"

jobs:
lint:
Expand Down Expand Up @@ -101,6 +114,18 @@ jobs:
if ! lua5.4 tests/check_package_name.lua "$f"; then
fail=1
fi
# 5b. The descriptor must live at the canonical path for its
# identity. The filename is not part of identity (mcpp resolves
# identity-first), but two files CAN carry the same identity
# without anyone noticing — `pkgs/c/capi.lua.lua` and
# `pkgs/m/mcpplibs.capi.lua.lua` were byte-identical duplicates
# of `(mcpplibs.capi, lua)`, and edits to whichever the scan
# reached second were simply dead. Canonical paths also keep a
# package on mcpp's first filename probe instead of a COMPAT
# fallback slated for removal in 1.0.0.
if ! lua5.4 tests/check_package_filename.lua "$f"; then
fail=1
fi
# 6. c++fly admission policy (mcpp design 2026-07-14 §11-Q2, v1):
# c++fly means "toolchain's latest level + every experimental
# gate" — deliberately toolchain-dependent, so a published
Expand Down Expand Up @@ -207,21 +232,21 @@ jobs:
ext: tar.gz
mcpp: bin/mcpp
xlings: registry/bin/xlings
mcpp_version: "0.0.102" # keep in sync with env.MCPP_VERSION
mcpp_version: "0.0.105" # keep in sync with env.MCPP_VERSION
- platform: macos
os: macos-15
suffix: macosx-arm64
ext: tar.gz
mcpp: bin/mcpp
xlings: registry/bin/xlings
mcpp_version: "0.0.102" # keep in sync with env.MCPP_VERSION
mcpp_version: "0.0.105" # keep in sync with env.MCPP_VERSION
- platform: windows
os: windows-latest
suffix: windows-x86_64
ext: zip
mcpp: bin/mcpp.exe
xlings: registry/bin/xlings.exe
mcpp_version: "0.0.102" # keep in sync with env.MCPP_VERSION
mcpp_version: "0.0.105" # keep in sync with env.MCPP_VERSION
env:
MCPP_EFFECTIVE: ${{ matrix.mcpp_version }}
steps:
Expand Down
50 changes: 49 additions & 1 deletion docs/repository-and-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ tests/examples/<member>/ 每库测试工程(workspace 成员;<member> 为包
([target.'cfg(...)'])
tests/*.cpp 行为断言(独立 main,退出码非 0 即失败)
tests/check_mirror_urls.lua lint:GLOBAL+CN 表完整性,以及 CN 指向 mcpp-res
tests/check_package_name.lua lint:身份规则 1+2(FQN 形式;层级在 namespace,short 原子)
tests/check_package_filename.lua lint:身份规则 3(描述符位于其身份的规范路径)
tests/list_cn_urls.lua 抽取 CN url,供 mirror-cn-reachable 使用
README.md 索引说明与贡献入口
.github/workflows/validate.yml CI:lint / mirror-cn-reachable / workspace(3 平台矩阵)
Expand All @@ -37,6 +39,47 @@ tools/compat-ffmpeg/ 等 compat 大包的描述符再生成流水线

`package` 必填字段:`spec`、`namespace`、`name`、`description`、`licenses`、`repo`、`type="package"`、`xpm`、`mcpp`。

### 包身份:`(namespace, name)` 的三条规则

包的身份是一个二元组 —— **`namespace` 是带层级的点分路径,`name` 是单一原子段**(mcpp 设计 2026-06-20 §4.2)。三条规则由 `tests/check_package_name.lua` 与 `tests/check_package_filename.lua` 机械强制,CI lint 秒级拦截。

**规则 1 —— `name` 必须写成完全限定名 `<namespace>.<short>`。**

```lua
namespace = "chriskohlhoff",
name = "chriskohlhoff.asio", -- ✅
-- name = "asio", -- ❌ split 形式
```

split 形式能通过解析、也能过 mcpp 的身份闸门,但**装不上**:xlings/libxpkg 用 `package.name` 的**字面值**给索引建键(`build_index` → `entries[package.name]`),而 mcpp 按消费端写法重建 `<ns>.<short>` 去要 —— 两者永不相交,`E_NOT_FOUND`。这个缺陷曾让三个平台的 workspace job 各跑满 20~58 分钟才炸(mcpp#278,已于 mcpp 0.0.105 在引擎侧修复)。

**规则 2 —— 层级放在 `namespace` 里,`short` 必须是单一原子段。**

```lua
namespace = "mcpplibs.capi", name = "mcpplibs.capi.lua", -- ✅ 身份 = (mcpplibs.capi, lua)
-- namespace = "mcpplibs", name = "mcpplibs.capi.lua", -- ❌ short = "capi.lua"
```

两种写法都能被 mcpp 的归一化器接受,因为它按**最后一个点**切分 FQN —— 于是第二种会静默解析成 `(mcpplibs.capi, lua)`,一个描述符从未声明过的命名空间。规则 2 消灭这种自相矛盾:**声明的 namespace 与归一化算出的 namespace 永远是同一个字符串。**

**规则 3 —— 描述符放在其身份对应的规范路径上。**

| 命名空间 | 规范路径 |
|---|---|
| 空 或 `mcpplibs`(默认命名空间) | `pkgs/<short 首字母>/<short>.lua` |
| 其他 | `pkgs/<FQN 首字母>/<FQN>.lua` |

```
(mcpplibs, cmdline) → pkgs/c/cmdline.lua
(compat, zlib) → pkgs/c/compat.zlib.lua
(aimol, tensorvia-cpu) → pkgs/a/aimol.tensorvia-cpu.lua
(mcpplibs.capi, lua) → pkgs/m/mcpplibs.capi.lua.lua
```

文件名**不是**身份的一部分(mcpp 身份优先解析,每个命中都要用文件自身的 `package.{namespace,name}` 复核),非规范命名的文件照样能解析。仍然要钉住它,是因为:① mcpp 的**发现**受候选文件名列表约束而非全索引扫描,规范路径让包命中第一个探测项,而不是落到 1.0.0 计划删除的 COMPAT 回退上;② 两个文件可以承载**同一个身份**而无人察觉 —— `pkgs/c/capi.lua.lua` 与 `pkgs/m/mcpplibs.capi.lua.lua` 曾是 `(mcpplibs.capi, lua)` 的字节级重复,目录扫描先到哪个哪个生效,对另一个的编辑是死的。

**零命名空间包是合法的一等形态。** 公开的默认命名空间模块包(`imgui` / `ffmpeg` / `opencv`)显式声明 `namespace = ""`,其**裸名就是 FQN**,消费端照常写 `imgui = "0.0.3"`。规则 2 对它们同样生效:一个带点却不声明命名空间的 `name` 会解析到一个哪儿都没写下来的命名空间。

`xpm.<linux|macosx|windows>.<裸版本>`:

- `url`:字符串,或 `{ GLOBAL=…, CN=… }` 表(本仓统一使用表形式)。
Expand Down Expand Up @@ -76,7 +119,10 @@ mcpp 跑 `xpkg parse`(strict:未知键即失败),所以需要更新文法/键的
- 触发条件:PR(改动 `pkgs/**/*.lua`、`tests/**`、`README.md` 或本 workflow)、push 至 main、nightly cron、手动触发。
- `env.MCPP_VERSION` 为全部 job 使用的 mcpp 版本,本地验证应与之对齐。
- `lint`(始终运行):lua 语法 `loadfile(f,'t')`;须含 `spec=`/`name=`/`xpm=`;禁止前导 v 版本;执行
`check_mirror_urls.lua`;再用 CI pin 的 mcpp 对每个描述符跑 `mcpp xpkg parse`(strict,未知键即失败)。
`check_mirror_urls.lua`;执行 `check_package_name.lua` + `check_package_filename.lua`(身份三规则,见
上文「包身份」);再用 CI pin 的 mcpp 对每个描述符跑 `mcpp xpkg parse`(strict,未知键即失败)。
自 `MCPP_VERSION = 0.0.105` 起,`mcpp xpkg parse` **自身**也强制规则 1(INV-NAME,mcpp#278),
两个 lua lint 因此成为更早、更便宜的冗余闸门,而不再是唯一防线。
- `mirror-cn-reachable`(始终运行):逐个 `curl` CN url,均须返回 200。
- `workspace (linux|macos|windows)`:整个测试面就是一个 mcpp workspace,**唯一的构建/运行通道**——
没有任何 shell 驱动的例外(公开模块包 imgui/ffmpeg/opencv/tinyhttps 也是普通成员,经成员级
Expand All @@ -96,6 +142,8 @@ for f in pkgs/*/*.lua; do
for n in 'spec *=' 'name *=' 'xpm *='; do grep -q "$n" "$f" || { echo "MISS $n $f"; fail=1; }; done
grep -nqE '\["v[0-9]+|\["[^"]+"\][[:space:]]*=[[:space:]]*"v[0-9]+' "$f" && { echo "LEADING-V $f"; fail=1; }
lua5.4 tests/check_mirror_urls.lua "$f" >/dev/null 2>&1 || { echo "MIRROR $f"; fail=1; }
lua5.4 tests/check_package_name.lua "$f" || fail=1
lua5.4 tests/check_package_filename.lua "$f" || fail=1
done
[ $fail -eq 0 ] && echo "ALL LINT PASS"
```
Expand Down
File renamed without changes.
42 changes: 0 additions & 42 deletions pkgs/c/capi.lua.lua

This file was deleted.

78 changes: 78 additions & 0 deletions tests/check_package_filename.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
-- Lint a descriptor's PATH against its declared identity.
--
-- The filename is NOT part of a package's identity — mcpp resolves
-- identity-first and verifies every candidate against the descriptor's declared
-- `package.{namespace,name}` (mcpp design 2026-06-26, "Filename Is Not a Key").
-- A non-canonically-named file still resolves.
--
-- It is still worth pinning, for two reasons this index has actually hit:
--
-- 1. Discovery is bounded by a candidate-filename list, not by an index-wide
-- scan (mcpp `compat::xpkg_lua_candidates`). Canonical names keep a
-- package on the first, cheapest probe instead of a COMPAT fallback that
-- is scheduled for removal in mcpp 1.0.0.
-- 2. Two files can carry the SAME identity without anyone noticing —
-- `pkgs/c/capi.lua.lua` and `pkgs/m/mcpplibs.capi.lua.lua` were
-- byte-identical duplicates of `(mcpplibs.capi, lua)`. Whichever the
-- directory scan reached first won; edits to the other were dead.
--
-- Canonical layout (mirrors mcpp `compat::xpkg_lua_candidates`):
--
-- namespace "" or "mcpplibs" → pkgs/<short[1]>/<short>.lua
-- any other namespace → pkgs/<fqn[1]>/<fqn>.lua
--
-- (mcpplibs, cmdline) → pkgs/c/cmdline.lua
-- (compat, zlib) → pkgs/c/compat.zlib.lua
-- (aimol, tensorvia-cpu) → pkgs/a/aimol.tensorvia-cpu.lua
-- (mcpplibs.capi, lua) → pkgs/m/mcpplibs.capi.lua.lua
--
-- Run check_package_name.lua first; this lint assumes identity is already
-- well-formed and reports nothing when it is not.
--
-- Usage: lua5.4 tests/check_package_filename.lua <pkgs/x/file.lua>

local DEFAULT_NS = "mcpplibs"

function import(...)
return setmetatable({}, {__index = function() return function() end end})
end

local path = assert(arg[1], "usage: check_package_filename.lua <file>")
package = nil
local chunk = assert(loadfile(path, "t"))
chunk()

local p = package
if type(p) ~= "table" then os.exit(0) end

local name = p.name
local ns = p.namespace or ""
if type(name) ~= "string" or name == "" then os.exit(0) end
if type(ns) ~= "string" then os.exit(0) end

-- Identity must already be well-formed; check_package_name.lua owns that.
local short = name
if ns ~= "" then
local prefix = ns .. "."
if name:sub(1, #prefix) ~= prefix then os.exit(0) end
short = name:sub(#prefix + 1)
if short == "" or short:find(".", 1, true) then os.exit(0) end
elseif name:find(".", 1, true) then
os.exit(0)
end

local basename = (ns == "" or ns == DEFAULT_NS) and short or name
local expected = string.format("pkgs/%s/%s.lua", basename:sub(1, 1):lower(), basename)

if path ~= expected then
io.stderr:write(string.format(
"::error file=%s::descriptor for identity (%s, %s) should live at %q, " ..
"not %q. The filename is only a lookup hint — mcpp verifies identity " ..
"from the file's own package.{namespace,name} — but the canonical path " ..
"keeps it on the first probe and makes duplicate descriptors for one " ..
"identity impossible to add unnoticed.\n",
path, ns == "" and "<none>" or ns, short, expected, path))
os.exit(1)
end

os.exit(0)
57 changes: 53 additions & 4 deletions tests/check_package_name.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,33 @@
-- the workspace job has already burned an hour. No consumer-side spelling can
-- work around it; the descriptor is the only place it can be fixed.
--
-- Upstream: mcpp-community/mcpp#278 (mcpp should either use the declared name
-- or reject the split form in `mcpp xpkg parse`). Until that lands, this lint
-- is the index's guard.
-- Upstream: mcpp-community/mcpp#278 — LANDED in mcpp 0.0.105, which enforces
-- the same rule inside `mcpp xpkg parse` (INV-NAME) and again on the install
-- path. This lint is now a redundant-but-cheap second gate: it runs before the
-- pinned mcpp is even downloaded, and it keeps the rule readable in-repo.
--
-- ── Rule 2: the short segment must be ATOMIC ────────────────────────
--
-- Identity is a 2-tuple `(namespace, name)` where `namespace` is a
-- HIERARCHICAL, dotted path and `name` is a SINGLE atomic segment (mcpp design
-- 2026-06-20 §4.2). Any depth therefore belongs in `namespace`, never in the
-- short half of `name`:
--
-- ✅ namespace = "mcpplibs.capi", name = "mcpplibs.capi.lua" → (mcpplibs.capi, lua)
-- ❌ namespace = "mcpplibs", name = "mcpplibs.capi.lua" → declared ns
-- disagrees with
-- canonical ns
--
-- Both spellings survive mcpp's normalizer (it splits the FQN on its LAST dot,
-- so the second one silently resolves to `(mcpplibs.capi, lua)` — a namespace
-- the descriptor never declared). That silent disagreement is what this rule
-- removes: after it, the declared namespace and the canonical one are always
-- the same string, and `a.b.c` can only ever live in `namespace`.
--
-- Zero-namespace packages (the public default-namespace module packages —
-- imgui / ffmpeg / opencv) are unaffected: their bare `name` IS the FQN.
-- imgui / ffmpeg / opencv) are a deliberate, legal form: their bare `name` IS
-- the FQN. Rule 2 still applies to them — a dotted name with no declared
-- namespace would resolve to a namespace that isn't written down anywhere.
--
-- Usage: lua5.4 tests/check_package_name.lua <file.lua>

Expand Down Expand Up @@ -57,6 +78,8 @@ if type(ns) ~= "string" then
os.exit(fail)
end

-- ── Rule 1: `name` is the fully-qualified `<namespace>.<short>` ─────
local short = name
if ns ~= "" then
local prefix = ns .. "."
if name:sub(1, #prefix) ~= prefix then
Expand All @@ -67,10 +90,36 @@ if ns ~= "" then
"consumer request can ever resolve (E_NOT_FOUND at install). " ..
"See mcpp-community/mcpp#278.",
ns, name, prefix .. name, name))
os.exit(fail)
elseif #name == #prefix then
err(string.format(
"package.name = %q has an empty short name after the %q prefix",
name, prefix))
os.exit(fail)
end
short = name:sub(#prefix + 1)
end

-- ── Rule 2: the short segment is a SINGLE atomic segment ───────────
-- Depth belongs in `namespace`. When the short half carries dots, mcpp's
-- split-on-last-dot normalizer attributes the package to a namespace the
-- descriptor never declared — the identity silently disagrees with itself.
if short:find(".", 1, true) then
local canonicalNs = name:sub(1, #name - #short:match("[^.]*$") - 1)
if ns == "" then
err(string.format(
"package.name = %q is dotted but declares no namespace: it would " ..
"resolve to namespace %q, which is written down nowhere. Put the " ..
"hierarchy in `namespace` — namespace = %q, name = %q.",
name, canonicalNs, canonicalNs, name))
else
err(string.format(
"package.name = %q has a non-atomic short name %q under namespace " ..
"%q. Identity is (namespace, name) where `namespace` is the dotted " ..
"path and `name` is ONE segment, so this resolves to namespace %q " ..
"— not the %q you declared. Move the depth into `namespace`: " ..
"namespace = %q.",
name, short, ns, canonicalNs, ns, canonicalNs))
end
end

Expand Down
Loading