Skip to content

feat(xpkg): files assets (src/dst) and injected args — 0.0.47 - #31

Merged
Sunrisepeak merged 2 commits into
mainfrom
feat/files-assets-and-args
Jul 26, 2026
Merged

feat(xpkg): files assets (src/dst) and injected args — 0.0.47#31
Sunrisepeak merged 2 commits into
mainfrom
feat/files-assets-and-args

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

实现 #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 种版本管理器完全看不见,既搬不动也清不掉。

xvm.add("pkg.files.1", { type = "files",
    src = "include/openssl",     -- 相对 payload 根
    dst = "usr/include/openssl", -- 相对 subos 根
})

xvm.files({ src = "lib64", dst = "usr/lib" })   -- 语法糖,自动派生 target 名

两端都相对是硬要求,不是风格问题。 payload 跨 subos 共享且有引用计数 —— 记绝对目标路径,只对安装它的那个 subos 正确,对其余全错。消费方负责解析并拒绝绝对路径与 ../ 逃逸。

xvm.files 从包名派生 target 名,所以一个 release 声明多个资产时调用方不需要自己编名字、也不会撞。

2. args —— 与 alias 分开

今天注入参数的唯一办法是拼进 alias 字符串,消费方按第一个空格切分:

  • 路径里有空格就废
  • 每个读 alias 的地方(版本列表、诊断)都会把一条命令行当成名字显示 —— 某台真实机器上有 9 条这种脏数据
xvm.add("clang", { args = { "-isystem", "/a b/include", "--sysroot=/root" } })

有序列表,不存在引号问题。

兼容性

两个字段都是增量的。 不设它们的 recipe 产出的 ops 与之前逐字段相同 —— 最后一个测试钉住这一点。

验证

4 个新测试:files 的 src/dst 透传、args 的顺序与含空格路径、xvm.files 派生名不碰撞、不设新字段时行为不变

⚠️ test_executor 另有 4 个 elfpatch 失败。把本 PR 的改动 stash 掉后完全一样地失败 —— 那些 fixture 通过 PATH 找它们伪造的 patchelf/install_name_tool,而 mcpp test 不提供。与本改动无关,是既有问题,我没有顺手改(它值得单独一个 issue)。

还没做的(#30 的 P2/P3)

use() hook + use_depsspec = "3" 门禁。它们要等 xlings 侧先把物化收敛完,而且 P0/P1 已经足够让索引开始迁移。

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
Sunrisepeak merged commit 3e29fe6 into main Jul 26, 2026
1 check passed
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.
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.

1 participant