feat(xpkg): files assets (src/dst) and injected args — 0.0.47 - #31
Merged
Conversation
Two fields the version manager cannot express today, both found while making
libraries and headers actually follow the release they belong to in xlings.
**`src` / `dst` for `type = "files"`.** `includedir` can only say "this one
directory becomes sysroot include". It cannot express a destination, an asset
that is not a header, or a source and destination that differ in name --
openssl's `lib64/` -> `usr/lib/` is all three at once. With no way to say it,
a package index grows its own file-placing helpers instead: the xlings index
has seven, in three languages, with two contradictory conflict policies, and
four of them are invisible to the tool that is supposed to switch versions.
Both ends are relative, and that is a requirement rather than a convention.
A payload is shared between subos and reference-counted, so an absolute
destination recorded against it would be correct for exactly the subos that
installed it and wrong for every other. `src` is relative to the payload
root, `dst` to the subos root; consumers resolve them and reject anything
absolute or escaping.
`xvm.files{src, dst}` is sugar over `xvm.add` for the case where the entry is
a file rather than something to dispatch, deriving the target name from the
package so a release can declare several without the caller inventing names
that might collide.
**`args`, separate from `alias`.** The only way to inject arguments today is
to append them to the alias string, which consumers split on the first space.
That breaks on any path containing one, and it makes every reader of `alias`
-- version listings, diagnostics -- report a command line where a name
belongs. On one real installation nine entries are in that state. `args` is
an ordered list, so quoting is not a question.
Both fields are additive: a recipe that sets neither produces exactly the
same ops as before, which the last test pins.
Verified: 4 new cases. `test_executor` also reports 4 pre-existing failures
in the elfpatch suite -- they fail identically with these changes stashed
(the fixtures resolve their fake tools through PATH, which `mcpp test` does
not provide) and are unrelated to this change.
CI was pinned to mcpp 0.0.67 (the workflow comments said 0.0.7, which had
been stale for a while) and xlings 0.4.25. The mcpplibs index has since
raised `min_mcpp` past both, so dependency resolution now dies before
anything is built:
error: fetch 'mcpplibs.capi.lua@0.0.3' failed (exit 1)
That message reads like a network problem and is not one -- it is what a
client below the index's floor reports, because the floor check postdates it.
main was last green on 2026-07-25 and the floor moved on the 26th, so this is
pre-existing breakage that the next push was always going to surface; it
happens to be this one. xlings hit the identical failure on the same day and
cost a full day to diagnose.
mcpp 0.0.67 -> 0.0.109, xlings 0.4.25 -> 0.4.69, cache path and key follow
the mcpp version, and the stale 0.0.7 comments are corrected.
Sunrisepeak
added a commit
to mcpplibs/mcpp-index
that referenced
this pull request
Jul 26, 2026
Adds `src`/`dst` for `type = "files"` assets and an `args` list separate from `alias`, both needed by xlings to place and switch files that are not programs. See openxlings/libxpkg#31. Artifact is the GitHub tag tarball; the CN entry is the same bytes under the gitcode mirror, verified by downloading both and comparing: fe879e8f52ea5a7f316ca54bca1fa393febb8e2a23a3de852b0c2be1918a0be9
Sunrisepeak
added a commit
to openxlings/xlings
that referenced
this pull request
Jul 26, 2026
**File assets.** A package that ships something which is neither a program nor a library -- headers under their own directory, pkg-config files, certificates -- had no way to say so. `includedir` could only mean "this directory becomes sysroot include". So index recipes grew their own file-placing helpers instead: seven of them, in three languages, with two contradictory conflict policies, and four invisible to the tool that is meant to switch and remove what they write. `type = "files"` with a `src`/`dst` pair says it, using the fields libxpkg 0.0.47 added (openxlings/libxpkg#31, published via mcpplibs/mcpp-index#128). Both ends stay relative and the resolver enforces it: a payload is shared between subos and reference-counted, so an absolute destination recorded against one would be right for the subos that installed it and wrong for every other. Destinations are also constrained to `usr/`, `etc/` and `share/` -- `bin/` belongs to the shims, and anything walking upward leaves the subos entirely. A recipe that trips either gets a refusal naming the field, not a surprising placement. Placement reuses the library path: same rename-based replacement, same skip-when-already-correct, so `use` re-materializing on every invocation costs a stat rather than a window with the file absent. **Date-based release identity.** `x.y.z` is retired; this release is `2026.7.27.0` -- year, month, day, same-day sequence, no leading zeros. The scheme is safe for already-released clients, and that is verified rather than assumed. `self update` expands to `install xlings@latest` plus `use xlings latest`, and the latter sorts through `version_key_greater`, which compares components numerically: [2026,7,27,0] against [0,4,68] decides on the first one. A new test pins that against every 0.x a user might be on. Writing that test found the reason leading zeros must not be used, which is sharper than the tidiness argument the design doc gave. When every component ties, the comparator falls back to a lexicographic tiebreak to keep the order total -- `sort` requires that. So `2026.07.27.0` and `2026.7.27.0` are not one key spelled two ways; they are two distinct keys, numerically equal and ordered by string bytes. A database holding both would have two entries for one version and `latest` would pick by accident. The test now pins that they are distinguishable, which is exactly why only one spelling may be written. Verified: 452 unit tests across 11 binaries; E2E-34 extended with a declared file asset that must move with the release at every step; group switch, multi-version removal, home-config lock, subos install and idempotent install all still pass.
Sunrisepeak
added a commit
to openxlings/xlings
that referenced
this pull request
Jul 26, 2026
…s mixing versions (#410) * fix(xvm): make libraries follow the release they belong to A library was already a first-class entry: registered with a payload path, a source name and a destination name, carried through the binding group like any other member, and materialized correctly at install. What it never had was materialization on *switch*. `plan_use_switch` decided library work by reading `VData::libdir`. Nothing in the tree writes that field. Not the registration batch -- `RegistrationNode` has no such member -- and not any other path; the only mention outside the struct is the JSON reader. On a real installation it is absent from all 372 entries. So the planner emitted no library work at all and `xlings use` was a silent no-op for libraries, while `install_libs()` sat at zero call sites. What that produced, concretely: install openssl 3.1.5, then 3.2.0. The install path overwrites the sysroot link, so the library is at 3.2.0. The headers travel a different route entirely and stay at 3.1.5. `use` in either direction moves neither and reports success both times. The user compiles against one version and links against another, and nothing says so until it fails at run time. Every fact needed to place a library was already in the entry -- `path` + `sourceName` + `destinationName`, exactly what the install path uses. The planner was reading the wrong field. `xvm::library_placement` now resolves an entry to `{source, name}` through accessors moved into xvm.types, so registration and the planner cannot drift; `MemberSwitch` carries the placement instead of a directory; and the two installer call sites that were also gated on `libdir` go through the same resolver. Placement replaces by rename rather than remove-then-link. Two versions of a library share a soname, so a switch overwrites the same name, and `use` re-materializes the active release on every invocation to repair a drifted sysroot -- remove-then-link would open a window with the library absent on every one of those calls. Only a genuinely different soname is unlinked. Legacy state is covered: entries written before 0.4.70 have no per-version `kind`, so the resolver falls back to the target-level `type`, which those 246 targets do have. Without that an upgrade would quietly stop switching every library the user already had. `install_libs` / `install_libdir` / `remove_libdir` are gone -- the first had no callers, the other two were reachable only through the field that is never written. Verified: 6 new cases (XvmLibrarySwitch) + two existing switch-plan cases reworked to cover a real library member; 441 tests, 11 binaries green. Restoring the `libdir` read fails the new suite. * fix(doctor): a release anchor is not a broken payload A library-only package has no program of its own, so its recipe registers the package name with no bindir purely to give the libraries something to bind to -- `cairo.lua` writes `xvm.add(package.name)` and nothing else. With `type` unset the C++ side defaults it to "program", so the entry claims to be an executable that will never exist, and `self doctor` reports it as a broken payload with a hint (`xlings install <pkg>@<ver>`) that cannot fix it because nothing is wrong. `xvm::is_binding_root` recognises the shape: some other entry names this one as the root of its release. Both forms count -- the provider group written since 0.4.70, and the legacy pairwise edge where a member records `bindings[root][memberVersion] = rootVersion`. An upgraded installation is full of the second, so recognising only the first would leave every existing anchor misreported. Reported, but as what it is, rather than suppressed. Staying silent would hide the rarer case of a genuine program whose payload directory survived while its executable did not: that entry is also a binding root, so it lands on the same line and the user still sees it. Measured against a real 372-entry installation: `self doctor` went from 9 broken payloads to 8 plus 2 release anchors, with `binutils@2.42` and `cairo@1.18.0` reclassified. (Two further findings became visible only because the panel is a fixed 24 lines and reclassifying freed the slots -- they were always there.) Correcting a number I put in the design doc and in #408: I claimed 31 false positives. That was my own script's count of program-typed entries with no same-named executable in the payload root, not doctor's output. doctor resolves through `resolve_executable`, which searches `bin/` as well, so most of those 31 resolve fine and were never reported. The real figure is 2. Both documents are corrected in follow-up commits. Verified: 3 new cases (XvmBindingRoot) covering the provider-group form, the legacy pairwise form, and a standalone program that must stay reportable. 444 tests, 11 binaries green. * fix(xim): stop a non-active version from overwriting the active library Two defects, both found by putting the real CLI in front of a package that ships a program, a library and a header -- the shape of openssl, which is where the original report came from. **Libraries were being placed in a directory nothing reads.** `cmd_use` resolved the sysroot library directory as `<subos>/usr/lib`, while the install path has always used `<subos>/lib` (`Config::paths().libDir`). On a real installation that is 94 entries against one. The two never disagreed in practice only because the switch side emitted no library work at all -- `VData::libdir`, the field it keyed on, has no writer -- so making libraries switch would have quietly started filling a second, unused directory. **Installing a second version clobbered the first one's library.** The `Library` filesystem effect had no `active` gate. `InstallHeaders` grew one in 0.4.70 for exactly this reason and `Library` was missed, so `xlings install pkg@2.0.0` while 1.0.0 was active moved the library to 2.0.0 and left the headers on 1.0.0. The sysroot then held two releases at once: compile against one, link against the other, and nothing reports it until run time. E2E-34 walks the whole sequence and asserts the library and the header agree at every step: 1. install v1 → both materialize at v1 2. install v2 → v1 stays active, the sysroot does not move 3. use v2 → both move 4. use v1 → both move back 4b. use from the library → the program follows too 5. remove, empty uninstall() → the whole release deregisters Scenario 5 is the other thing this pins. Removal has been provider-scoped since 0.4.70 -- with no `xvm.remove` ops the installer finds every entry whose `bindingGroup.provider` matches and takes the release out -- but that was read out of the code and never tested. The fixture's `uninstall()` returns true and does nothing, so if the mechanism does not work the entries survive and the scenario fails. It is the prerequisite for deleting the hand-written teardown boilerplate every recipe currently carries. Both defects were reproduced by the test before being fixed; scenario 1 failed on the wrong directory and scenario 2 on the missing gate. Verified: E2E-34 green; xvm_group_switch, remove_multi_version, subos_xpkg_install, home_config_lock all still pass; 444 unit tests, 11 binaries green. * docs(release): record the library-switching fixes in the 0.4.70 notes * test(xvm): build the expected library path with `path`, not a POSIX literal The planner joins the payload directory and the source name with `std::filesystem::path`, which yields native separators. Comparing the result against "/pkg/openssl/3.1.5/lib64/libssl.so.3" therefore passes on POSIX and can never pass on Windows, where the value is "/pkg/openssl/3.1.5/lib64\\libssl.so.3". Windows CI caught it. Five other assertions in the same suite use substring matches and were unaffected. Build the expectation the same way the value is built. * docs(release): adopt the compound release identity `(0.4.70) 2026.07.27.0` -- semantic version plus release date and same-day sequence. The semantic version stays machine-readable: `self update` compares it and both indexes key on it, so a date-only identity would leave already-released clients unable to tell whether an upgrade exists. * feat(xvm): declared file assets, and date-based release identity **File assets.** A package that ships something which is neither a program nor a library -- headers under their own directory, pkg-config files, certificates -- had no way to say so. `includedir` could only mean "this directory becomes sysroot include". So index recipes grew their own file-placing helpers instead: seven of them, in three languages, with two contradictory conflict policies, and four invisible to the tool that is meant to switch and remove what they write. `type = "files"` with a `src`/`dst` pair says it, using the fields libxpkg 0.0.47 added (openxlings/libxpkg#31, published via mcpplibs/mcpp-index#128). Both ends stay relative and the resolver enforces it: a payload is shared between subos and reference-counted, so an absolute destination recorded against one would be right for the subos that installed it and wrong for every other. Destinations are also constrained to `usr/`, `etc/` and `share/` -- `bin/` belongs to the shims, and anything walking upward leaves the subos entirely. A recipe that trips either gets a refusal naming the field, not a surprising placement. Placement reuses the library path: same rename-based replacement, same skip-when-already-correct, so `use` re-materializing on every invocation costs a stat rather than a window with the file absent. **Date-based release identity.** `x.y.z` is retired; this release is `2026.7.27.0` -- year, month, day, same-day sequence, no leading zeros. The scheme is safe for already-released clients, and that is verified rather than assumed. `self update` expands to `install xlings@latest` plus `use xlings latest`, and the latter sorts through `version_key_greater`, which compares components numerically: [2026,7,27,0] against [0,4,68] decides on the first one. A new test pins that against every 0.x a user might be on. Writing that test found the reason leading zeros must not be used, which is sharper than the tidiness argument the design doc gave. When every component ties, the comparator falls back to a lexicographic tiebreak to keep the order total -- `sort` requires that. So `2026.07.27.0` and `2026.7.27.0` are not one key spelled two ways; they are two distinct keys, numerically equal and ordered by string bytes. A database holding both would have two entries for one version and `latest` would pick by accident. The test now pins that they are distinguishable, which is exactly why only one spelling may be written. Verified: 452 unit tests across 11 binaries; E2E-34 extended with a declared file asset that must move with the release at every step; group switch, multi-version removal, home-config lock, subos install and idempotent install all still pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
实现 #30 的 P0 + P1。纯增量,不设 recipe 没有的字段就完全不变行为。
来自 xlings 侧 sysroot / 多版本切换的工作(openxlings/xlings#410、设计 #409)。
1.
src/dst——type = "files"includedir只能说"这一个目录变成 sysroot 的 include"。说不了:目标位置、非头文件的资产、源与目标不同名 —— openssl 的lib64/→usr/lib/三样全占。没有办法表达,索引就自己长手段:xlings 的索引现在有 7 种写文件的方式、3 种语言、2 套互相矛盾的冲突策略,其中 4 种版本管理器完全看不见,既搬不动也清不掉。
两端都相对是硬要求,不是风格问题。 payload 跨 subos 共享且有引用计数 —— 记绝对目标路径,只对安装它的那个 subos 正确,对其余全错。消费方负责解析并拒绝绝对路径与
../逃逸。xvm.files从包名派生 target 名,所以一个 release 声明多个资产时调用方不需要自己编名字、也不会撞。2.
args—— 与alias分开今天注入参数的唯一办法是拼进 alias 字符串,消费方按第一个空格切分:
alias的地方(版本列表、诊断)都会把一条命令行当成名字显示 —— 某台真实机器上有 9 条这种脏数据有序列表,不存在引号问题。
兼容性
两个字段都是增量的。 不设它们的 recipe 产出的 ops 与之前逐字段相同 —— 最后一个测试钉住这一点。
验证
4 个新测试:
files的 src/dst 透传、args的顺序与含空格路径、xvm.files派生名不碰撞、不设新字段时行为不变。还没做的(#30 的 P2/P3)
use()hook +use_deps、spec = "3"门禁。它们要等 xlings 侧先把物化收敛完,而且 P0/P1 已经足够让索引开始迁移。