From 06840d4eb67f1ee9ca0ce803f3664a1a3e18546c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sat, 25 Jul 2026 11:16:20 +0800 Subject: [PATCH] chore: normalize xpkg identity layout, enforce it in lint, pin CI to mcpp 0.0.105 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The index was already clean on identity: all 49 descriptors declare a fully-qualified `name` and an atomic short segment, so mcpp#278's split form appears nowhere here. What was NOT pinned was the layer below it — where a descriptor lives — and that had let two real defects in. Normalization (no identity/namespace/name value changes at all): * pkgs/c/capi.lua.lua deleted. It was a BYTE-IDENTICAL duplicate of pkgs/m/mcpplibs.capi.lua.lua — same md5, same identity (mcpplibs.capi, lua). Whichever the directory scan reached first won; edits to the other were dead code nobody would notice. * pkgs/t/tensorvia-cpu.lua -> pkgs/a/aimol.tensorvia-cpu.lua, the canonical path for identity (aimol, tensorvia-cpu). Off-canonical names still resolve, but only via a COMPAT filename fallback that mcpp schedules for removal in 1.0.0. Lint now enforces all three identity rules mechanically: 1. `name` == `.` (existing check, kept) 2. `short` is a SINGLE atomic segment — depth belongs in `namespace`. NEW. Both spellings of a nested package survive mcpp's normalizer because it splits the FQN on its LAST dot, so `namespace="mcpplibs", name="mcpplibs.capi.lua"` silently resolves to `(mcpplibs.capi, lua)` — a namespace the descriptor never declared. After this rule the declared and canonical namespace are always the same string. 3. descriptor sits at the canonical path for its identity. NEW (tests/check_package_filename.lua) — this is the check that found both defects above. Zero-namespace packages (imgui/ffmpeg/opencv) stay exactly as they are: their bare `name` IS the FQN and that is a deliberate, legal form. Rule 2 still guards them — a dotted name with no declared namespace would resolve into a namespace written down nowhere. CI pinned to mcpp 0.0.105 (env.MCPP_VERSION + all three matrix entries), where `mcpp xpkg parse` enforces rule 1 itself (INV-NAME, mcpp#278). The lua lints therefore become an earlier, cheaper second gate rather than the only one. 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 here needs 0.0.105 grammar — so there is no reason to force every consumer forward. Verified locally against real mcpp 0.0.105: all 48 descriptors pass every lint plus `mcpp xpkg parse`; the three counterexamples (split form, non-atomic short, dotted-name-without-namespace) are each rejected with the exact spelling to write instead. --- .github/workflows/validate.yml | 33 +++++++- docs/repository-and-schema.md | 50 +++++++++++- .../aimol.tensorvia-cpu.lua} | 0 pkgs/c/capi.lua.lua | 42 ---------- tests/check_package_filename.lua | 78 +++++++++++++++++++ tests/check_package_name.lua | 57 +++++++++++++- 6 files changed, 209 insertions(+), 51 deletions(-) rename pkgs/{t/tensorvia-cpu.lua => a/aimol.tensorvia-cpu.lua} (100%) delete mode 100644 pkgs/c/capi.lua.lua create mode 100644 tests/check_package_filename.lua diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index aba168e..afeb5a1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 `.`. 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 @@ -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: @@ -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 @@ -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: diff --git a/docs/repository-and-schema.md b/docs/repository-and-schema.md index b35c72b..51fa132 100644 --- a/docs/repository-and-schema.md +++ b/docs/repository-and-schema.md @@ -13,6 +13,8 @@ tests/examples// 每库测试工程(workspace 成员; 为包 ([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 平台矩阵) @@ -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` 必须写成完全限定名 `.`。** + +```lua +namespace = "chriskohlhoff", +name = "chriskohlhoff.asio", -- ✅ +-- name = "asio", -- ❌ split 形式 +``` + +split 形式能通过解析、也能过 mcpp 的身份闸门,但**装不上**:xlings/libxpkg 用 `package.name` 的**字面值**给索引建键(`build_index` → `entries[package.name]`),而 mcpp 按消费端写法重建 `.` 去要 —— 两者永不相交,`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//.lua` | +| 其他 | `pkgs//.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..<裸版本>`: - `url`:字符串,或 `{ GLOBAL=…, CN=… }` 表(本仓统一使用表形式)。 @@ -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 也是普通成员,经成员级 @@ -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" ``` diff --git a/pkgs/t/tensorvia-cpu.lua b/pkgs/a/aimol.tensorvia-cpu.lua similarity index 100% rename from pkgs/t/tensorvia-cpu.lua rename to pkgs/a/aimol.tensorvia-cpu.lua diff --git a/pkgs/c/capi.lua.lua b/pkgs/c/capi.lua.lua deleted file mode 100644 index ad22940..0000000 --- a/pkgs/c/capi.lua.lua +++ /dev/null @@ -1,42 +0,0 @@ --- Form A descriptor: the upstream repo ships its own mcpp.toml from --- 0.0.3 onwards, so we omit the `mcpp` field — mcpp default-look-up --- finds /lua-/mcpp.toml inside the GitHub tarball wrap. -package = { - spec = "1", - namespace = "mcpplibs.capi", - name = "mcpplibs.capi.lua", - description = "C++23 module wrapping the Lua 5.4 C API — `import mcpplibs.capi.lua;`", - licenses = {"Apache-2.0"}, - repo = "https://github.com/mcpplibs/lua", - type = "package", - - xpm = { - linux = { - ["0.0.3"] = { - url = { - GLOBAL = "https://github.com/mcpplibs/lua/archive/refs/tags/0.0.3.tar.gz", - CN = "https://gitcode.com/mcpp-res/capi.lua/releases/download/0.0.3/capi.lua-0.0.3.tar.gz", - }, - sha256 = "f7f46c3cd193dc4527be5f3e5cfc29d7e322d5d3db56b9bdb060f289090088d6", - }, - }, - macosx = { - ["0.0.3"] = { - url = { - GLOBAL = "https://github.com/mcpplibs/lua/archive/refs/tags/0.0.3.tar.gz", - CN = "https://gitcode.com/mcpp-res/capi.lua/releases/download/0.0.3/capi.lua-0.0.3.tar.gz", - }, - sha256 = "f7f46c3cd193dc4527be5f3e5cfc29d7e322d5d3db56b9bdb060f289090088d6", - }, - }, - windows = { - ["0.0.3"] = { - url = { - GLOBAL = "https://github.com/mcpplibs/lua/archive/refs/tags/0.0.3.tar.gz", - CN = "https://gitcode.com/mcpp-res/capi.lua/releases/download/0.0.3/capi.lua-0.0.3.tar.gz", - }, - sha256 = "f7f46c3cd193dc4527be5f3e5cfc29d7e322d5d3db56b9bdb060f289090088d6", - }, - }, - }, -} diff --git a/tests/check_package_filename.lua b/tests/check_package_filename.lua new file mode 100644 index 0000000..03b65d9 --- /dev/null +++ b/tests/check_package_filename.lua @@ -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//.lua +-- any other namespace → pkgs//.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 + +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 ") +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 "" or ns, short, expected, path)) + os.exit(1) +end + +os.exit(0) diff --git a/tests/check_package_name.lua b/tests/check_package_name.lua index f34f783..dca41c4 100644 --- a/tests/check_package_name.lua +++ b/tests/check_package_name.lua @@ -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 @@ -57,6 +78,8 @@ if type(ns) ~= "string" then os.exit(fail) end +-- ── Rule 1: `name` is the fully-qualified `.` ───── +local short = name if ns ~= "" then local prefix = ns .. "." if name:sub(1, #prefix) ~= prefix then @@ -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