Skip to content

fix(build): declare compatibility packages exactly - #521

Draft
Sunrisepeak wants to merge 1 commit into
mainfrom
fix/explicit-ftxui-namespace
Draft

fix(build): declare compatibility packages exactly#521
Sunrisepeak wants to merge 1 commit into
mainfrom
fix/explicit-ftxui-namespace

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 9, 2026

Copy link
Copy Markdown
Member

概要

  • ftxui@6.1.9 精确声明为 compat.ftxui
  • 把仅测试使用的 gtest@1.15.2 精确声明为 compat.gtest
  • 刷新 mcpp.lock,使 FTXUI 和传递 Lua C 库保留 canonical package identity

原因

mcpp-community/mcpp#400 的 exact-selector 契约明确规定:省略命名空间只等于 mcpplibs,不再按短名称猜测另一个命名空间。xlings 仍使用裸 ftxuigtest,因此新 mcpp 会正确尝试 (mcpplibs, ftxui) / (mcpplibs, gtest),并在进入平台专属验证前停止。

不可变 mcpplibs.capi.lua@0.0.3 中的传递裸 Lua 依赖已经由 mcpplibs/mcpp-index#197 用 index-side Form-B bridge 精确声明为 compat.lua@5.4.7;该 PR 已正常合并,10/10 CI 通过。

验证

  • 使用 exact-selector mcpp 分支的隔离构建通过
  • dependency trace 使用 compat.ftxuicompat.gtestmcpplibs.capi.lua 和精确 compat.lua
  • 终端策略标准化后的 xlings 完整 unit:36/36
  • GitHub Actions:Linux build/test、Linux E2E、Linux root、macOS、Windows、aarch64 cross/QEMU 和 native aarch64 contract 共 8/8 通过
  • diff 隐私扫描未发现本地用户名、绝对 workspace/home 路径、缓存/临时目录或凭据

Draft 边界

  • 本 PR 保持 Draft,等待用户明确 review
  • 不使用 amend/rebase/force-push,不使用 admin/bypass/squash 合并
  • review 通过后走普通合并
  • 当前已发布的 xlings 2026.8.9.2 不包含本修复;合并后需要独立 version bump、全平台 release CI 和新稳定 release
  • 新 xlings release 完成后,feat: converge template identity, runtime contracts, and release state mcpp-community/mcpp#400 需要用独立 commit 更新 pin 并重新跑 latest-head 全矩阵

Fixes #520

Related: mcpp-community/mcpp#400, mcpplibs/mcpp-index#197

Move ftxui and the gtest development dependency under the compat namespace, and refresh the lockfile so ftxui and the transitive Lua C library retain their canonical identities.\n\nRefs #520
@Sunrisepeak

Copy link
Copy Markdown
Member Author

Draft checkpoint: normalized full test gate

The full suite now passes with terminal policy normalized for this non-interactive agent shell (NO_COLOR unset, TERM=xterm):

  • result: 36 passed, 0 failed
  • elapsed: 37.54s (build 2.67s + run 34.20s)
  • exact dependency trace: compat.ftxui, compat.gtest, mcpplibs.capi.lua with compat.lua
  • validation-only local index override was removed after the run; the branch is clean and contains no local path

The earlier single failure was reproduced as the shell policy (TERM=dumb), not a product regression; its focused test is green 3/3 under the same normalized policy.

@Sunrisepeak

Copy link
Copy Markdown
Member Author

Cross-repository checkpoint

@speak-agent

Copy link
Copy Markdown
Collaborator

中文状态同步 checkpoint

PR 正文已由 speak-agent 更新为当前真实状态:

本 PR 继续保持 Draft/REVIEW_REQUIRED,等待用户明确 review;不会使用 admin/bypass/squash 或改写历史。

Sunrisepeak pushed a commit to mcpp-community/mcpp that referenced this pull request Aug 9, 2026
Two independent bumps that happen to land on the same date string.

The xlings pin moves from 2026.8.9.2 to the current stable 2026.8.10.1
across all 16 pin sites; check_version_pins.sh verifies them against
src/xlings.cppm rather than trusting a comment. Note that 2026.8.10.1
still declares a bare `ftxui`, so the four cross-repo CI failures on this
branch are resolved by the bare-name migration window in this PR, not by
the pin — openxlings/xlings#521 remains the correct upstream fix and is
no longer on the critical path.

The release version moves from 2026.8.9.1 to 2026.8.10.1 because the
version is a DATE and the date changed while this branch was in review.
2026.8.9.1 was never tagged, so nothing depends on the old string.

The release-manifest and AUR fixtures derive "some other version" from
VERSION; both halves move so they stay distinct instead of collapsing
onto the release version and silently asserting nothing.

Bootstrap pin stays at the released 2026.8.8.4 — it is the self-hosting
starting point, not a function of what this release will be.
Sunrisepeak pushed a commit to mcpp-community/mcpp that referenced this pull request Aug 9, 2026
The design that drove this round of work, plus an appendix of outcomes
written after the fact — including the two places reality diverged from
the plan.

D2 resolved as "proven, keep", but for a different reason than the
commit it was defending claimed: not a Windows short-name alias, but
that anchoring must not relocate an index out of the tree the workspace
author addressed. D5's invariant turned out to already hold; it simply
had no test.

Two things arrived unplanned: #401, and the discovery that every
identity/index-routing e2e was gated behind a capability Windows does
not have — so the platform where path semantics differ was the one
platform never asserting them.

And main line C's bottleneck moved. The migration window took
openxlings/xlings#521 off the critical path as intended, but pinning to
the newest xlings put a harder one in its place: a cold home cannot
install gcc at all (openxlings/xlings#524). Recorded with the A/B,
because the lesson generalizes — "pin to latest" is an action that needs
verifying, not a text substitution, and it only surfaced here because
changing the pin also changed the CI cache key.
speak-agent added a commit to mcpp-community/mcpp that referenced this pull request Aug 9, 2026
#400)

* docs: plan focused ecosystem convergence

* feat: make package selectors exact

* feat: make template selection exact

* feat: make project scaffolding transactional

* feat: make runtime selection root-local

* feat: validate Linux runtime closure

* feat: carry provider-neutral runtime provenance

* feat: publish immutable release manifests

* feat: reconcile mcpp-bin releases

* fix: reconcile runtime bindings with SubOS views

* chore: prepare 2026.8.9.1 release

* test: recognize Python E2E capability

* test: keep ELF parser fixtures on Linux

* fix(pm): inherit declared index namespaces exactly

* fix(pm): retain index ownership in dependency selection

* fix(runtime): model ELF SONAME reuse

* test(e2e): align fixtures with exact identities

* test(e2e): preserve runtime contracts in fake xlings

* fix(index): retain floor cause on exact misses

* fix(pm): surface malformed exact descriptors

* fix(pm): derive mangling from authored modules

* test(e2e): scope BMI settling to its edge

* test(scaffold): close fixtures before cleanup

* docs: make Draft PR trace privacy-safe

* test(e2e): honor isolated mcpp home in libc poison

* fix(workspace): anchor inherited indices lexically

* docs: add Chinese PR handoff

* docs: refresh PR validation handoff

* fix(aur): use privacy-safe release identity

* docs: record privacy-safe AUR checkpoint

* fix(pm): explain exact index route misses

* docs: record Windows index route evidence

* test(e2e): write fixture paths in host spelling, not shell spelling

The Windows E2E 2/2 failure is not the workspace index inheritance the
last three commits were aimed at. It is step (15) of 12_add_command.sh,
added by this branch, which writes an MSYS path into a manifest:

    TMP=$(mktemp -d)                       # Git Bash -> /tmp/tmp.XXXXXXXX
    acme = { path = "$TMP/myapp/index" }   # written as FILE CONTENT

MSYS converts POSIX paths on the way into argv and the environment; it
does not convert file content. A native mcpp.exe reads the literal
leading `/` as "root of the current drive", so the fixture index
resolves to C:\tmp\... and is absent. That is exactly what the route
diagnostic reported: `local index 'acme': root absent, pkgs absent`.

Four independent signals place the failure at step (15), not (14):
exit code 2 is mcpp's own status forwarded by `set -e`, while step (14)
is wrapped in `|| { …; exit 1; }`; step (14)'s failure banner never
appears; the last uncaptured output before it belongs to step (12); and
step (14) predates this branch, on which the Windows suite was green.

The Wine replay that "reproduced it as passing" was a false negative:
Wine maps Z: to /, so \tmp\... lands back on the real /tmp. Wine can
show a PE runs. It cannot show anything about path semantics.

Fixed as a class rather than a line. `_host_path.sh` provides
`host_path` (cygpath -m on MSYS, identity elsewhere, relative paths
untouched), 00_fixture_path_hygiene.sh fails any fixture that
interpolates a non-`*_HOST` variable into a TOML `path =` value, and the
40 pre-existing sites across 24 files are migrated. Most were masked
only because those tests skip on Windows for a missing capability.

* test(pm): pin why an inherited index is anchored lexically

`inherit_workspace_indices` was switched from `weakly_canonical` to
`lexically_normal` in ed4cf64 to fix the red Windows suite. It did not:
the same failure appeared identically before (run 31318089536) and after
(run 31321961040), and the actual cause was a fixture writing an MSYS
path into mcpp.toml. So the change was landed against a misdiagnosis,
against a behaviour that was green on main, with no test able to tell
the two spellings apart.

It turns out to be right for a different reason, and now it says so.
Anchoring answers "which directory did the workspace author mean", and
the answer must stay inside the tree the author addressed.
`weakly_canonical` answers "what is this path once every symlink is
resolved", so a workspace reached through a symlinked parent gets
relocated into a tree the author never wrote — observable, because
prepare reports a missing descriptor as "not found in local index at
'<path>'". Verified as a real RED: restoring `weakly_canonical` fails
`unit/test_pm_index_route` (71 passed, 1 failed).

The existing test also stops asserting a path SPELLING. Comparing the
stored path against `(root / "index").lexically_normal()` passes for any
rule that happens to produce that string and says nothing about whether
the index can be read — which is how it stayed green through a red
Windows suite. It now asserts the capability: route description, then a
descriptor lookup that actually resolves.

* feat(pm): give namespace omission a one-release exit ramp

Namespace omission means exactly `mcpplibs`. But every published
`compat.*` package and every manifest written before exact identity
spells its dependency bare — `gtest = "1.15.2"`, `ftxui = "6.1.9"` — so
turning that into an immediate hard error makes an mcpp upgrade break
builds against data that is already published and cannot be edited
retroactively. Four CI jobs on this branch are that breakage, and this
repository's own mcpp.toml had to move gtest under
[dev-dependencies.compat] to keep bootstrapping: a change that breaks
its own project is breaking everyone's.

It is the mirror of the rule that keeps a raised index floor from
bricking older clients. Published data must not break the program; a
published program must not break existing data either. Both degrade.

The #278 defect was the SILENCE, not the reach: mcpp continued with a
namespace the user never wrote and never said so. So the rungs come
back, and cannot be quiet. After the exact coordinate misses, and only
for a selector whose namespace was OMITTED, `(compat, name)` and the
namespace-less discovery rung are tried; a hit is announced with the
canonical selector and the exact manifest edit, and the canonical
identity — not the ambiguous spelling — is what reaches the lock, the
install layer and the cache label. `mcpp add` goes further and performs
the edit, so touching a dependency migrates it.

`mcpplibs.gtest` states an identity and still misses. A third-party
namespace is still unreachable by a bare name. Removed in 2026.9, named
once as kBareNameFallbackRemovedIn so the warning, the docs and the
removal cannot drift apart.

Test 162 now asserts both branches and that an already-exact selector is
not nagged, and it serves the compat fixture from its own index instead
of whatever gtest happened to be installed.

* fix(runtime): keep the private libc out of the process environment (#401)

`mcpp run` put the private glibc directory on LD_LIBRARY_PATH. That
variable is inherited by every process the program ever spawns, and a
child like /bin/sh is loaded by the HOST loader — PT_INTERP is baked in
and no environment variable overrides it. glibc's libc.so.6 and its
ld.so are version-locked through GLIBC_PRIVATE, so the child dies during
relocation, before main:

  sh: symbol lookup error: …/xim-x-glibc/2.44/lib64/libc.so.6:
      undefined symbol: __pointer_chk_guard, version GLIBC_PRIVATE

The reporter hit it through an application probing the desktop theme
with gsettings: popen() returned nothing, the app silently used the
wrong theme, and mcpp said nothing. Verified locally — the 2.44 payload
kills /bin/sh, the 2.39 payload does not, which is why this survived as
long as it did and why "it works here" proved nothing.

The directory was published to serve a dlopen() the executable's
DT_NEEDED closure does not cover, and the artifact's RUNPATH already
covers exactly that: the link model emits -Wl,-rpath,<glibc> next to
--dynamic-linker, and the built binary's RUNPATH is byte-identical
before and after this change. So the environment entry bought nothing
and cost every child process. It is gone.

Measured against the released mcpp with the same fixture:
  before  LDLP=[…/runtime:…/xim-x-glibc/2.39/lib64]
  after   LDLP=[…/runtime]

The decision lives in mcpp.platform.runtime_env_contract as a scope,
not a condition: no build-level predicate can make an inherited variable
safe for a process mcpp did not launch and cannot see. mcpp#291 was the
same mistake one hop closer in.

208 pins all three halves — the dir is in the artifact RUNPATH, it is
not in the program's environment, and both the /bin/sh child and the
dlopen still work. The RUNPATH assertion deliberately checks coverage
mcpp does not itself emit, so losing it is a red test rather than a
silent loss of resolution.

* test(runtime): a proven mismatch must not go green on a re-run

The Linux closure verdict is cached against the artifact stat and the
RuntimeBinding contract hash, and caching a FAILURE is the half worth
pinning: an unchanged artifact that was proven wrong is still wrong, and
reporting success the second time tells the user a problem went away
when nothing about the program changed.

It is easy to lose by accident, because the project fast path exists to
skip work when nothing changed — including, unless something stops it,
the check that would have failed. `validated_artifact_snapshot` stops
it by refusing to engage unless every stored verdict is a PASS; that
rule had no test.

209 poisons one stored verdict without touching a source file and
requires the next build to fail with the recorded diagnostics, requires
`mcpp self doctor` to explain it from storage, and then requires a
rebuilt artifact to be re-validated — a mismatch is cached, not a
sentence, or the first real failure would wedge the project until
someone deleted target/.

* ci(aur): require arming before an unattended publish

`schedule` fires every six hours off the default branch and set
publish=true unconditionally. Merging this workflow was therefore enough
to make mcpp start writing to the AUR on its own, within six hours, on a
path that had never completed a real push — only a makepkg --verifysource
dry-run. Merging is a decision about code; publishing to a third-party
service is a decision about the outside world, and they should not be
the same act.

Both automatic triggers now plan and report but withhold the push unless
the repository variable AUR_AUTOPUBLISH is exactly "true", so a typo
fails closed. Dry runs still validate payloads, render .SRCINFO, query
the AUR and print the diff — only the push is withheld, so nothing is
lost but the surprise. workflow_dispatch keeps its explicit per-run
switch, which is how the first watched publish is meant to happen, and
unsetting the variable is a kill switch that needs no revert.

The contract test runs the workflow's own decision shell rather than a
paraphrase of it, so the guarantee cannot drift from the YAML.

* test(e2e): assert local-index addressing on every host

Every test covering local-index addressing needed `gcc` or
`fresh-sandbox`, and Windows has neither capability. So the one platform
where path semantics actually differ was the one platform never
asserting them — which is why a fixture could write an MSYS path into
mcpp.toml and the failure surfaced four steps away, as "package not
found in any configured index", and was chased through the workspace
inheritance code that was not involved.

210 needs no compiler, no sandbox bootstrap and no network: it asserts
at the resolution layer, which is the layer that was broken. An absolute
[indices] path resolves; a workspace member inherits a root-relative one
and reads it; a miss in both reports the index as a READABLE root, since
"root absent" is the fingerprint of a path written in the wrong
spelling; and the route diagnostic that makes any of this findable stays
free of filesystem paths.

* chore(release): 2026.8.10.1 on xlings 2026.8.10.1

Two independent bumps that happen to land on the same date string.

The xlings pin moves from 2026.8.9.2 to the current stable 2026.8.10.1
across all 16 pin sites; check_version_pins.sh verifies them against
src/xlings.cppm rather than trusting a comment. Note that 2026.8.10.1
still declares a bare `ftxui`, so the four cross-repo CI failures on this
branch are resolved by the bare-name migration window in this PR, not by
the pin — openxlings/xlings#521 remains the correct upstream fix and is
no longer on the critical path.

The release version moves from 2026.8.9.1 to 2026.8.10.1 because the
version is a DATE and the date changed while this branch was in review.
2026.8.9.1 was never tagged, so nothing depends on the old string.

The release-manifest and AUR fixtures derive "some other version" from
VERSION; both halves move so they stay distinct instead of collapsing
onto the release version and silently asserting nothing.

Bootstrap pin stays at the released 2026.8.8.4 — it is the self-hosting
starting point, not a function of what this release will be.

* docs: the bare-name window, the runtime env scope, and AUR arming

Three things a user or a maintainer cannot discover from the code:

- The one-release exit ramp for namespace-omitted selectors: what still
  resolves, what the warning says, that the canonical identity is what
  reaches the lock, that `mcpp add` performs the migration, and that a
  stated namespace is not eligible. Written with the reason attached —
  every published `compat.*` package spells its dependency bare, so
  failing them outright would make a program release invalidate data
  that is already published and cannot be edited.

- Why the private libc directory is binary-scoped and never
  environment-scoped, as a table of blast radius rather than a rule to
  memorize: DT_RUNPATH reaches one object, LD_LIBRARY_PATH reaches every
  descendant process forever, and a GLIBC_PRIVATE-coupled libc handed to
  the host loader kills /bin/sh before main (#401).

- How to arm the AUR automatic triggers, including that the kill switch
  is unsetting a repository variable rather than reverting a commit.

English and Chinese both, plus the changelog entry for 2026.8.10.1.

* chore(xlings): hold the pin at 2026.8.9.2 until openxlings/xlings#524

Pinning to the current stable 2026.8.10.1 turned every Linux job red at
the same place: a COLD home cannot install xim:gcc@16.1.0 at all.

  Downloading xim:glibc@2.44 …
  integrity: cleaning incomplete install: …/xpkgs/xim-x-gcc/16.1.0
  [gcc] failed: config hook failed
  [xim:xpkg]: glibc payload not found, but gcc needs it to rewrite its
              ELF interpreter away from the build machine's path.
              install it first: xlings install xim:glibc@2.39

The version it installs and the version the hook asks for disagree, and
gcc.lua declares `xim:glibc@>=2.39` so resolving 2.44 is legal — the
failure is that the consumer's config hook cannot find the dependency
payload that was just fetched.

A/B on the same workflow, same runner image, both with a confirmed
cache miss, so this is not a long-standing cold-start bug that warm
caches were hiding:

  2026.8.9.2  cold  → gcc installs      (run 31317627461)
  2026.8.10.1 cold  → the failure above (run 31335075557, 4/4 Linux jobs)

Nothing in mcpp can route around it: it is upstream install ordering or
store-root lookup, and it breaks every new machine, every cold CI cache
and every fresh install. Reported with the evidence as
openxlings/xlings#524; the pin moves once that ships.

The release version stays 2026.8.10.1 — it is a date, and this is a
different project's version that happens to share the string.

* docs: record the completion design and what executing it actually found

The design that drove this round of work, plus an appendix of outcomes
written after the fact — including the two places reality diverged from
the plan.

D2 resolved as "proven, keep", but for a different reason than the
commit it was defending claimed: not a Windows short-name alias, but
that anchoring must not relocate an index out of the tree the workspace
author addressed. D5's invariant turned out to already hold; it simply
had no test.

Two things arrived unplanned: #401, and the discovery that every
identity/index-routing e2e was gated behind a capability Windows does
not have — so the platform where path semantics differ was the one
platform never asserting them.

And main line C's bottleneck moved. The migration window took
openxlings/xlings#521 off the critical path as intended, but pinning to
the newest xlings put a harder one in its place: a cold home cannot
install gcc at all (openxlings/xlings#524). Recorded with the A/B,
because the lesson generalizes — "pin to latest" is an action that needs
verifying, not a text substitution, and it only surfaced here because
changing the pin also changed the CI cache key.

* test(e2e): 166 asserted the mechanism, not the capability

166 exists for mcpp#291 and had two halves. The negative half — a plain
binary must not be handed the private glibc — is still exactly right and
is unchanged. The positive half required the opposite in the presence of
a dlopen-reachable dependency: the payload dir HAD to be on
LD_LIBRARY_PATH, "because dlopen'd libraries do not consult the
executable's RUNPATH".

The guard was right to exist. It stopped anyone from "fixing" #291 by
deleting the entry and quietly breaking dlopen. But it pinned the
mechanism rather than the capability, and the mechanism's premise was
false: a dlopen() performed by the executable does consult the
executable's DT_RUNPATH, and the link model already puts the payload
glibc there — the artifact's RUNPATH is byte-identical with and without
the environment entry. So the entry bought nothing while reaching every
descendant process, which is how it became mcpp#401.

The capability it was protecting now lives in 208, which asserts the
dlopen actually resolves. 166 keeps the rule it is named for and now
applies it to both shapes, plus one new assertion: the project's own
[runtime] library_dirs entry must still be there. Only the private libc
is binary-scoped; dropping ordinary runtime dirs too would be a
different regression that the negative assertion alone would not catch.

Full local Linux e2e on this HEAD: 202 passed, 0 failed, 8 skipped.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(build): declare ftxui under its canonical namespace

2 participants