Skip to content

feat(xim): runtime / build deps split (libxpkg schema + resolver/installer kind propagation)#249

Merged
Sunrisepeak merged 5 commits intomainfrom
feat/build-runtime-deps
May 1, 2026
Merged

feat(xim): runtime / build deps split (libxpkg schema + resolver/installer kind propagation)#249
Sunrisepeak merged 5 commits intomainfrom
feat/build-runtime-deps

Conversation

@Sunrisepeak
Copy link
Copy Markdown
Member

Summary

  • libxpkg (mcpplibs/libxpkg#... TBD): schema gains runtime_deps + build_deps. Loader fan-out keeps legacy deps = {...} array form working unchanged; new deps = { runtime = ..., build = ... } table form keeps the two lists separate. pkginfo.build_dep(name) Lua API resolves a build-time dep's payload at hook time via injected env or xpkgs scan. 8/8 LoaderTest pass (incl. 2 new fixtures for both shapes).
  • xlings: PlanNode gains kind (Runtime/Build) + per-kind dep lists. Resolver propagates kind through DFS — Runtime parent forks build_deps to Build, Build subtree stays Build, "Runtime wins" upgrade on second encounter so user-requested packages always activate. Installer skips run_config_hook_ for kind=Build (no shim, no workspace mutation, no version DB add) so build deps land in xpkgs/ but never pollute the user's PATH. For Runtime nodes with build_deps, installer injects XLINGS_BUILDDEP_<UPPER>_PATH env vars and prepends the build dep's bin/ to PATH around the install hook subprocess. cmd_info shows runtime/build deps separately when the new schema form is declared.
  • Joint-debug ergonomics: xmake f --local_libxpkg=/path/to/libxpkg pulls libxpkg in via includes() so in-flight schema edits show up in xlings without going through xrepo's released-version cache.
  • New e2e: tests/e2e/build_deps_split_test.sh (E2E-20) installs a fixture pkg with both kinds and asserts payload presence, shim-vs-no-shim, env injection, and pkginfo.build_dep() return values. Wired into xlings-ci-linux.yml.

Why

Today every dep declared by a package becomes part of the active workspace, polluting the user's tool versions even if the dep was only needed at install time. Build-time tools (compilers, patchelf, code generators) are per-consumer concerns — different consumers can require different versions without conflict if treated as build-only. This unblocks several real diamond-conflict scenarios where two packages want incompatible versions of the same tool just for their own builds.

Design doc: docs/plans/2026-05-01-build-runtime-deps-split.md.

Cross-repo coordination

This consumer change requires mcpplibs-xpkg >= 0.0.32 (or whichever version contains the runtime_deps/build_deps fields). Until released, joint-debug via --local_libxpkg=. After libxpkg releases the new version, bump add_requires("mcpplibs-xpkg X.Y.Z") in xmake.lua.

Test plan

  • libxpkg unit tests (8/8 LoaderTest, including the two new schema fixtures)
  • xlings local build (blocked by env: gcc15.1.0 sysroot misconfig + xrepo cache glibc/musl mix; CI will exercise the link path on this PR)
  • CI E2E-20 build_deps_split_test.sh (will run on push)
  • All other CI E2E checks still pass

… kind

Wires the libxpkg schema split through the resolver and installer:

  * `PlanNode` gains `runtime_deps`, `build_deps`, and a `kind`
    discriminator. Resolver propagates DepKind through the DFS:
      - Runtime parent's runtime_deps stay Runtime.
      - Runtime parent's build_deps fork to Build.
      - Build subtree stays Build (transitive deps inherit).
      - "Runtime wins" upgrade: a node first reached via Build that's
        later reached via Runtime is upgraded so the installer activates
        it, otherwise the user-requested package would be missing from
        their workspace.
  * `Installer::execute()` now skips run_config_hook_ for kind=Build
    nodes, so the payload lands in xpkgs but no shim/version-DB/workspace
    mutation happens. For Runtime nodes with build_deps the installer
    injects `XLINGS_BUILDDEP_<UPPER_NAME>_PATH` env vars and prepends
    each build dep's bin/ dir to PATH for the install hook subprocess
    (and any process it spawns), then restores the env after.
  * `cmd_info` shows runtime/build deps separately when the package
    declares the new split form, falls back to the legacy "deps" line
    otherwise.
  * `xmake.lua` adds `--local_libxpkg=PATH` for cross-repo joint
    debugging: when set, the libxpkg targets are pulled in via
    `includes()` instead of through xrepo's released-version cache,
    so in-flight schema changes show up immediately.

Adds e2e/E2E-20: build_deps_split_test.sh (+ fixtures under
tests/e2e/fixtures/build_deps_split/) which installs a fixture pkg
that has both runtime_deps and build_deps and verifies:
  - both payloads land in xpkgs/
  - runtime dep gets a PATH shim, build dep doesn't
  - workspace map doesn't list the build dep
  - the install hook saw the env var and could exec the build dep
  - pkginfo.build_dep("bdtool") returns the expected fields
…s schema)

This commit's resolver/installer/commands changes reference the
runtime_deps + build_deps fields on PlatformMatrix that were just
added in mcpplibs-xpkg 0.0.32 (mcpplibs-index entry committed in
mcpplibs/mcpplibs-index@24309c2). The fixture xpkgs in
tests/e2e/fixtures/build_deps_split/ exercise both the legacy array
form (loader fan-out) and the new table form via the consumer's
xpm.<platform>.deps = { runtime = ..., build = ... } shape.
…xlings sync

Symptom: E2E-20 on PR #249 fails on Linux with
  '[error] failed to build catalog: pkgs/ directory not found'
  '[error] package bdconsumer not found'

Root cause: the previous version of this script cp'd fixtures into the
shared $FIXTURE_INDEX_DIR/pkgs/ tree. xlings install internally calls
sync_all_repos when the catalog isn't loaded yet, and sync_all_repos
runs 'git fetch + reset --hard' against the index repo — clobbering
the fixtures we just dropped in. The catalog rebuild then sees only
the original 81 entries (no bdconsumer/bdtool/rttool), so install
fails with 'package not found'.

Fix: follow the same pattern as remove_multi_version_test.sh /
xlings_self_replace_test.sh — clone-copy the shared fixture into a
runtime-private directory, neutralise its xim-indexrepos.lua to skip
sub-index fetching, inject the test-only fixtures there, and have
.xlings.json point 'xim' at this private dir. sync_all_repos sees
a non-git path and leaves it alone.
# Conflicts:
#	.github/workflows/xlings-ci-linux.yml
@Sunrisepeak Sunrisepeak merged commit 0bebd3a into main May 1, 2026
3 checks passed
@Sunrisepeak Sunrisepeak deleted the feat/build-runtime-deps branch May 1, 2026 14:00
Sunrisepeak added a commit that referenced this pull request May 1, 2026
…split + .tar.xz fix (#252)

Bumps VERSION from 0.4.9 to 0.4.10. Highlights since 0.4.9:

  * #250 — `xlings remove xim:xlings` no longer silently terminates on
    Windows when xlings has only one version installed. Three layered
    fixes: (1) installer.cppm switches both `remove_shim_if_present`
    call sites to the error_code overload + log::warn (was throwing
    filesystem_error from ERROR_SHARING_VIOLATION when DeleteFile hit
    the running xlings.exe shim, escaping cli::run with no top-level
    catch → std::terminate, silent CI fail). (2) cmd_remove detects
    "target is xlings + only one version installed" upfront and
    refuses with rc=2 + redirect to `xlings self uninstall`.
    (3) cli::run wraps app.run in a top-level catch for
    filesystem_error / std::exception / ... so any future stray throw
    surfaces as a logged error + non-zero rc.

    Bonus while threading rc properly: cmdline lib stores actions as
    std::function<void(...)>, so action lambda's int returns were
    being silently discarded by every cmd_*. Captured via a wrap_rc
    helper so `xlings install <bad-pkg>` etc actually return non-zero
    to scripts now.

  * #249 — Build vs runtime deps schema split (xpkg/libxpkg side at
    mcpplibs-xpkg 0.0.32). Packages can now declare:
      `xpm.<platform>.deps = { runtime = {...}, build = {...} }`
    Runtime deps activate in the workspace as before; build deps
    land in xpkgs but are NOT activated — the consumer's install
    hook accesses them via `pkginfo.build_dep("name")` (Lua) or via
    injected env `XLINGS_BUILDDEP_<UPPER>_PATH` + bin on PATH for the
    hook subprocess. Legacy `deps = { ... }` array form is preserved
    via loader fan-out (populates both kinds identically). Resolver
    DFS propagates DepKind through the dep graph; installer skips
    `run_config_hook_` for kind=Build so build deps don't pollute the
    user's PATH or version DB. `xlings info` shows runtime/build deps
    separately when the new form is used.

  * #251 — `.tar.xz` / `.tar.lzma` extraction now uses internal
    liblzma instead of fork-exec'ing the system `xz` binary at
    runtime. xmake-repo's libarchive package def lists
    `add_deps("zlib", "bzip2", "lz4", "zstd", "lzma")`, but xrepo's
    `lzma` is the 7-Zip LZMA SDK (LzmaLib.h, LzmaCompress) while
    libarchive's CMakeLists actually probes for xz-utils' liblzma
    (lzma.h, lzma_code) — the right xrepo package is `xz`. Result on
    main: cmake find_package(LibLZMA) silently failed → libarchive
    fell back to `archive_read_support_filter_xz_external` →
    fork-exec'd `xz` at runtime → silent failure on musl-static
    minimal containers and Windows (no xz binary on PATH). Concrete
    affected xpkgs: node@* (linux .tar.xz), llvm (macosx-arm64
    .tar.xz). Project-private libarchive override in
    `xmake/packages/libarchive.lua` (set_base("libarchive") + swap
    deps lzma → xz + force -DENABLE_LZMA=ON). Verified via `nm`:
    libarchive-xlings.a now has `U lzma_code / lzma_alone_decoder /
    lzma_crc32 / lzma_end / lzma_properties_decode`; no more "Using
    external xz program" string in the .a.

Migration: 0.4.9 → 0.4.10 is binary-compatible; xlings's own
self-replace path handles the upgrade. The new build_deps schema is
opt-in (legacy array form keeps working).

Drop-the-override criterion (for #251): when xmake-repo merges a
fix changing libarchive's `add_deps("lzma")` → `add_deps("xz")`,
`xmake/packages/libarchive.lua` can be deleted and the root xmake.lua
reverted to `add_requires("libarchive 3.8.7")` + `add_packages("libarchive")`.
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