Skip to content

feat(xim): recipe packaging revision and the install_targets record (2026.9.27.1) - #622

Merged
Sunrisepeak merged 1 commit into
mainfrom
feat/install-targets-revision
Sep 27, 2026
Merged

Sunrisepeak merged 1 commit into
mainfrom
feat/install-targets-revision

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Summary

xlings 2026.9.27.1: a recipe's packaging revision (#620), and a record of what each install request resolved to.

Revision (#620)

A version entry may state revision = N (libxpkg 0.0.58), counting changes to what a recipe installs under an unchanged upstream version. Until now the store could only answer "is 2.44.3 here", so a fixed recipe never reached a machine that had installed the broken payload.

  • Stamp: the payload stamp records the revision it was built from, always written as "revision": N in .xpkg-install.json.
  • Verdict: one function, payload_revision_verdict (install_state), decides whether a payload is current.
    • A payload is current if and only if its recorded revision equals the recipe's.
    • A stamp without the field reads as revision 0, so only a recipe that states revision 1 or higher reinstalls.
    • No stamp, or a stamp recording a failed install, gives no verdict.
  • Planning: the resolver plans a stale payload as not installed. The installer asks the same verdict again and states the reason, for example reinstalling glibc@2.44.3: recipe revision 1, installed revision 0.
  • Replacement: the version is never left without a payload.
    • The old tree is renamed to <data>/stale/ and the new one is installed at the real path.
    • On success the old tree is deleted; on any failure it is renamed back (PayloadReplacement).
  • Plan entries: install_plan entries carry the revision as a third element and the reinstall reason as their note.

Recovery of an interrupted reinstall

The rollback above is a destructor, so a killed process (SIGKILL, power loss) used to leave the old tree parked under <data>/stale/ forever.

  • Before the rename, a marker <parked>.origin records the payload path and the parking process, written atomically.
  • At the start of every install command, recover_parked_payloads visits <data>/stale/: an entry whose process is gone is moved back when the payload path is missing or is the empty placeholder, and deleted with its marker otherwise; an entry of a live process is left alone.
  • On Windows, a rename refused because a file inside is held open names that file.

install_targets (interface protocol 1.1, additive)

  • The top-level install reports {request, namespace, name, version, revision, status, payload_dir} per request, in request order, with status installed, already_present or failed.
  • It is emitted on every path: fresh installs, "everything already installed" (which previously put nothing on the wire), and failures.
  • Nested installs and dry runs do not report.
  • install_summary.success now counts only what the run installed, as its comment already stated. This is the one observable change for existing clients.
  • Documented in docs/spec/interface-ndjson-v1.md and docs/spec/xpkg-manifest-v1.md.

Version-grammar conformance vectors

tests/data/semver-vectors.tsv lists request, available versions, active version and expected result. tests/unit/test_semver_vectors.cpp drives every vector through the resolver. mcpp vendors the file and runs the vectors without an active version against its own implementation.

Tests

  • Unit: test_install_state, test_interface_protocol, test_payload_remove (including ParkedPayloadMarker and RecoverParkedPayloads), test_semver_vectors. The full mcpp test, 58 test binaries, passes locally as a non-root user against the published libxpkg 0.0.58.
  • e2e: tests/e2e/install_revision_test.sh (E2E-121), cases R1 to R9; R9 fabricates a parked tree with a dead process's marker and a missing payload path and checks that install restores it and empties stale/.

Dependencies

Refs #620, #621. They are commented on and closed after the release is verified in an xlings subos sandbox.

…2026.9.27.1)

A version entry may now state `revision = N` (libxpkg 0.0.58): a change to
what a recipe installs under an unchanged upstream version. Until now the
store could only answer "is 2.44.3 here", so a fixed recipe never reached a
machine that had installed the broken payload: `install` of that version
returned on the strength of the `installed` hook or the version database.

Revision (#620)
- The payload stamp records the revision it was built from
  (`"revision": N` in `.xpkg-install.json`, always written).
- One verdict, `payload_revision_verdict` (install_state): a payload is
  current iff its recorded revision equals the recipe's. A stamp without the
  field reads as revision 0, so only a recipe that states revision >= 1
  reinstalls, and it reaches every older payload. No stamp at all, or a stamp
  that records a failed install, gives no verdict.
- The resolver plans a stale payload as not installed (its artifact is
  downloaded and verified first); the installer asks the same verdict again
  after its own fast paths and says why:
  `reinstalling glibc@2.44.3: recipe revision 1, installed revision 0`.
- The replacement never leaves the version without a payload: the old tree
  is renamed to `<data>/stale/`, the new one is installed at the real path
  (recipes embed install_dir()), and the old tree is deleted on success or
  renamed back on any failure (`PayloadReplacement`, payload.cppm). Files
  other tools wrote into the old tree go with it.
- `install_plan` entries carry the revision as a third element and the
  reinstall reason as their note.

install_targets (interface protocol 1.1, additive)
- The top-level install reports, per request and in request order,
  `{request, namespace, name, version, revision, status, payload_dir}` with
  status `installed | already_present | failed`, on every path: fresh
  installs, "everything already installed" (which previously put nothing on
  the wire), and failures. Nested installs requested by recipes and dry runs
  do not report. Registered as interface-only for the terminal.
- A node whose payload was present and current is recorded as present
  rather than installed, so `install_summary.success` counts what the run
  installed, as its comment already stated.
- docs/spec/interface-ndjson-v1.md documents install_plan, install_summary
  (as emitted) and install_targets; docs/spec/xpkg-manifest-v1.md documents
  `revision`.

Crash recovery for an interrupted reinstall
- `PayloadReplacement`'s rollback is a C++ destructor, so it only runs
  within the same process. A kill (SIGKILL, power loss) between the rename
  into `<data>/stale/` and that destructor left the parked directory there
  forever: nothing else ever revisited it, and every crash during a
  revision reinstall leaked one directory.
- Fixed with a marker written next to the parked directory before the
  rename that creates it (`<parked>.origin`, atomic write-temp-then-rename;
  `write_parked_payload_marker`), recording the payload's original path and
  the pid doing the parking. At the start of every `install` -- once per
  command, where `Installer::execute` first touches the store, not once per
  package -- `recover_parked_payloads` sweeps `<data>/stale/`: an entry
  whose pid is no longer running is moved back when the original path is
  missing or is the empty placeholder `set_aside_payload` leaves behind, or
  discarded with its marker when the original path already holds something
  else. An entry whose pid is still alive is left alone. Recovered and
  discarded entries are logged at the same level as other install
  diagnostics.
- The in-process rollback is unchanged; a successful replacement deletes the
  marker along with the parked directory.
- The message for a rename that fails on a file held open (Windows only --
  POSIX `rename(2)` on a directory does not depend on what is open inside
  it) now names the specific file, not only the two directories being
  exchanged.
- docs/spec/xpkg-manifest-v1.md gets one sentence on the recovery.

Version-grammar conformance vectors
- tests/data/semver-vectors.tsv: request, available versions, active
  version, expected result; driven through the resolver by
  tests/unit/test_semver_vectors.cpp. Self-describing for vendoring.

Also: xlings 2026.9.27.1, mcpplibs.xpkg 0.0.58, E2E-121
(install_revision_test.sh, now with a crash-recovery case).

Refs: #620

mcpp.lock is regenerated against the published libxpkg 0.0.58 with mcpp
2026.9.26.2 (the earlier edit only renamed the version and kept 0.0.57's
hash).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Sunrisepeak
Sunrisepeak force-pushed the feat/install-targets-revision branch from 818b191 to 8fb9a43 Compare September 27, 2026 05:45
@Sunrisepeak
Sunrisepeak merged commit 44c1c54 into main Sep 27, 2026
9 checks passed
speak-agent added a commit to mcpp-community/mcpp that referenced this pull request Sep 27, 2026
kXlingsVersion and every .github pin check_version_pins.sh reads move to
xlings 2026.9.27.1 (openxlings/xlings#622), released on GitHub and GitCode
and published in the xim index. It is the release that emits install_targets
and records a payload's revision, which this mcpp reads first. The vendored
semver-vectors.tsv already matches that release's file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

2 participants