Skip to content

feat(xpkg): auto-stamp install_dir for wrapper packages#10

Merged
Sunrisepeak merged 1 commit intomainfrom
feat/auto-stamp
May 2, 2026
Merged

feat(xpkg): auto-stamp install_dir for wrapper packages#10
Sunrisepeak merged 1 commit intomainfrom
feat/auto-stamp

Conversation

@Sunrisepeak
Copy link
Copy Markdown
Member

Summary

Make wrapper-package idempotency a framework concern instead of a per-package idiom. After the Install hook returns success and install_dir is empty, auto-write a .xim-installed marker so xlings's catalog probe (is_directory && !is_empty) doesn't keep flagging the package as not-installed and re-triggering the hook on every dependent install.

Context

xim-pkgindex's linux-headers.lua and glibc.lua already work around this in their install hooks by writing the marker manually:

function install()
    local install_dir = pkginfo.install_dir()
    if not os.isdir(install_dir) then os.mkdir(install_dir) end
    io.writefile(path.join(install_dir, ".xim-installed"), pkginfo.version())
    return true
end

That's good for known wrapper packages but every new wrapper-shape package hits the bug until the author finds and reproduces the idiom. Move it to the framework once.

Stamp file

Format: key=value INI. Grep / cat / read all friendly. schema = 1 reserves room to add fields later.

schema = 1
name = wrapper
version = 1.0.0
platform = linux

Behavior contract

Trigger Auto-stamp behavior
Install hook succeeded + install_dir empty ✅ writes .xim-installed
Install hook succeeded + install_dir already non-empty ❌ no-op (don't stomp on hook-laid content)
Install hook failed ❌ no-op (failures shouldn't leave stamps)
Non-Install hooks (Config, Uninstall, …) ❌ no-op (auto-stamp is Install-specific)

Compatibility with existing manual stamps

Existing wrapper packages (linux-headers / glibc) that already write their own stamp continue to work. Their install hook populates install_dir first; the auto-stamp's is_empty gate sees non-empty, so no double-write or format conflict.

Stamp file format differs (manual = single-line version string; auto = key=value INI) but the file is a marker — content isn't load-bearing for the catalog probe. Future tooling that wants to parse it should handle both formats.

Decision NOT made: catalog xvm fallback

Originally proposed adding a "catalog falls back to xvm::has_version when install_dir is empty" as a belt-and-suspenders. Rejected on review:

install_dir 非空 should remain the single source of truth. If a stamp goes missing for any reason (manual cleanup, fs corruption), we want the catalog to truthfully report "not installed" so xlings re-runs the install, regenerating the stamp. Adding xvm DB as a secondary signal would mask such bugs and introduce dual-authority drift.

Self-healing via re-install > silent fallback.

Test plan

ExecutorTest: 26/26 pass (4 new auto-stamp cases):

  • AutoStamp_WritesStampWhenInstallDirEmpty — wrapper case, stamp content fields verified
  • AutoStamp_SkipsWhenInstallDirNonEmpty — hook populated install_dir → no double-write
  • AutoStamp_SkipsWhenInstallHookFailed — failures don't leave a stamp
  • AutoStamp_NotWrittenForNonInstallHooks — Config/Uninstall hooks don't trigger

LoaderTest 11/11, ModelTest 5/5, IndexTest 18/18 also still pass.

Cross-repo follow-up

After merge + v0.0.34 tag + mcpplibs-index 0.0.34 entry: a separate xlings PR will bump add_requires("mcpplibs-xpkg 0.0.33")"0.0.34". xlings has zero code changes — same executor.run_hook(HookType::Install, ctx) call point now picks up the new auto-stamp behavior transparently.

Wrapper packages (linux-headers, etc.) leave install_dir empty because
the real payload lives elsewhere — subos sysroot via config(), or a
sub-dependency. Without anything in install_dir, xlings's catalog probe
(`is_directory && !is_empty`) flags them as not-installed on every
dependent install, looping forever.

Today this is worked-around per-package: linux-headers/glibc each
remember to drop a manual stamp in their install hook. New wrapper
packages still hit the bug until the author finds and reproduces the
idiom. Move it to the framework: run_hook(Install) auto-writes
`.xim-installed` (key=value INI: schema/name/version/platform) when:

  1. The Install hook returned success (don't stamp failures), AND
  2. install_dir is empty (don't stomp on hook-laid content).

Existing packages that already write their own stamp continue to work
unchanged: their hook populates install_dir first, the auto-stamp's
`is_empty` check sees non-empty, no double-write. Format compatibility
is irrelevant because the file is a marker — content isn't load-bearing
for the catalog probe.

This is the framework-side fix for the wrapper-package idempotency
issue. Bug #1 surface (UI re-listing linux-headers on every
`xlings install gcc`) is already worked around in xim-pkgindex via
manual stamps; this PR makes the framework no longer require that
workaround for new wrapper packages.

Tests: 26/26 ExecutorTest pass (4 new auto-stamp cases):
  - WritesStampWhenInstallDirEmpty
  - SkipsWhenInstallDirNonEmpty
  - SkipsWhenInstallHookFailed
  - NotWrittenForNonInstallHooks

Decision NOT to add a catalog xvm fallback (originally proposed): the
install_dir non-empty check should remain the single source of truth.
If a stamp goes missing for any reason (manual cleanup, fs corruption),
we want the catalog to truthfully report "not installed" so xlings
re-runs the install, regenerating the stamp. Adding xvm DB as a
secondary signal would mask such bugs.
@Sunrisepeak Sunrisepeak merged commit c8fa199 into main May 2, 2026
7 of 9 checks passed
Sunrisepeak added a commit to openxlings/xlings that referenced this pull request May 2, 2026
…#256)

* feat(deps): bump mcpplibs-xpkg 0.0.34 → 0.0.35 (auto-stamp framework)

Picks up libxpkg's auto-stamp executor framework: after a successful
install hook returns and install_dir is non-empty, the executor writes
a `.xim-installed` stamp file (INI key=value: schema, name, version,
platform) into install_dir if the dir is empty.

This unblocks thin delegator packages — those whose actual payload lives
in a separately-installed dep (e.g. linux-headers wraps
scode:linux-headers, fromsource:* aliases) — from xlings's
"installed: yes/no" probe. Without a stamp, the probe checks install_dir
for content; an empty install_dir reports "no" → every dependent install
re-triggers the wrapper's install hook + config hook unnecessarily.

No version bump in this PR (deferring 0.4.13 release until other in-flight
work lands). The 0.0.35 → 0.4.x consumption path is just an internal
xrepo dependency bump; existing xlings 0.4.12 binaries are unaffected.

libxpkg PR: openxlings/libxpkg#10 (merged)
libxpkg tag: v0.0.35
mcpplibs-index: mcpplibs/mcpplibs-index@20e7543

* fixup: bump 0.0.35 → 0.0.36 + add explicit apply_install_stamp_if_empty call

PR #256's first push (0.0.35) regressed E2E-02 because libxpkg's
auto-stamp ran inside run_hook, writing .xim-installed before xlings's
stage_extracted_payload_ fallback could probe "is install_dir empty?".
patchelf's tarball has no top-level dir, so its install hook silently
no-ops; without the fallback, install_dir ended up containing only
the stamp file → "patchelf binary not found after self install".

libxpkg PR #12 / v0.0.36 moved auto-stamp into a separate
apply_install_stamp_if_empty(ctx) method that consumers call after
their full install pipeline. This commit:

  1. Bumps add_requires 0.0.35 → 0.0.36 (carries the API split)
  2. Adds the explicit apply_install_stamp_if_empty call in
     installer.cppm right after stage_extracted_payload_ +
     normalize_file_install_, so:
       hook → stage extracted payload → normalize → stamp
     The stamp now sees the FINAL install_dir state and only fires
     for genuinely-empty wrapper packages (linux-headers etc.).

Local E2E verified: `xlings self install` populates
xim-x-patchelf/0.18.0/bin/patchelf correctly.

libxpkg PR: openxlings/libxpkg#12 (merged, v0.0.36)
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