Skip to content

Commit f8ff845

Browse files
committed
fix(fetcher): retry direct xlings install after interface failure
1 parent c5a8739 commit f8ff845

3 files changed

Lines changed: 92 additions & 8 deletions

File tree

.agents/docs/2026-05-31-index-refresh-cache-labels-plan.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ The current work is intentionally limited to build-tool behavior:
1515
- make automatic package-index refresh quiet from the mcpp user's perspective;
1616
- make index freshness use a reliable mcpp-owned timestamp marker;
1717
- make dependency cache status use canonical dependency identities;
18+
- make large xpkg/toolchain installs retry through direct `xlings install`
19+
when the NDJSON interface path fails;
1820
- keep local xlings validation as the integration proof.
1921

2022
## Findings
@@ -103,6 +105,33 @@ deps/mcpplibs/xpkg@0.0.39
103105

104106
while the consumer dependency remains `mcpplibs.xpkg v0.0.41`.
105107

108+
### 5. Cold Linux CI can fail musl-gcc via xlings interface install
109+
110+
PR #91 inherited a main-branch Linux CI failure in:
111+
112+
```text
113+
Toolchain: musl-gcc - build mcpp (--target)
114+
```
115+
116+
The failing command was:
117+
118+
```text
119+
mcpp build --target x86_64-linux-musl
120+
```
121+
122+
The visible error was only:
123+
124+
```text
125+
xlings install of 'xim:musl-gcc@15.1.0' failed (exit 1)
126+
```
127+
128+
The same workflow's e2e suite had already proven that fresh-home musl-gcc
129+
installation can work, and recent CI history showed this as a cold-cache
130+
failure on main too. The weak point is that `Fetcher::resolve_xpkg_path()`
131+
uses only the xlings NDJSON interface path for regular xpkg installs, while
132+
`mcpp.xlings::install_with_progress()` already documents the direct CLI path
133+
as more reliable for large package installs.
134+
106135
## Implementation Plan
107136

108137
- [x] Add focused regression coverage for default-index refresh quietness.
@@ -113,8 +142,11 @@ while the consumer dependency remains `mcpplibs.xpkg v0.0.41`.
113142
- [x] Avoid using stale embedded package version as the user-facing resolved
114143
dependency version when the index resolution already knows the requested
115144
version.
145+
- [x] Add a direct `xlings install <target> -y` fallback after interface
146+
install failure, preserving visible direct-install output for CI
147+
diagnostics.
116148
- [x] Validate with the local xlings checkout using the new mcpp binary.
117-
- [ ] Push a draft PR and use it as the multi-commit checkpoint.
149+
- [x] Push a draft PR and use it as the multi-commit checkpoint.
118150

119151
## Verification Plan
120152

@@ -126,7 +158,7 @@ while the consumer dependency remains `mcpplibs.xpkg v0.0.41`.
126158
while the marker is inside TTL.
127159
- [x] Confirm cached dependencies display as `Cached` when their artifacts are
128160
staged.
129-
- [ ] Record CI status on the PR.
161+
- [ ] Record CI status on the PR after the second checkpoint commit.
130162

131163
## Dynamic Notes
132164

@@ -157,3 +189,12 @@ while the consumer dependency remains `mcpplibs.xpkg v0.0.41`.
157189
`mcpplibs.tinyhttps` / `mcpplibs.xpkg` displayed as `Cached`.
158190
- Marker was written at
159191
`~/.mcpp/registry/data/mcpplibs/.mcpp-index-updated`.
192+
- CI follow-up:
193+
- Linux CI on PR #91 failed in `Toolchain: musl-gcc - build mcpp
194+
(--target)`.
195+
- The same step had already failed on `origin/main` run `26691717542`, so
196+
this is not introduced solely by PR #91.
197+
- Added direct-install fallback in `src/xlings.cppm` and
198+
`src/pm/package_fetcher.cppm`.
199+
- Re-ran local `mcpp build --no-cache`, `mcpp test --
200+
--gtest_filter=XlingsIndexFreshness.*`, and the three related e2e tests.

src/pm/package_fetcher.cppm

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -725,15 +725,39 @@ Fetcher::resolve_xpkg_path(std::string_view target,
725725
mcpp::fallback::clean_incomplete_install(verdir);
726726
}
727727
if (inst->exitCode != 0) {
728-
// xlings install actually failed (network, missing package,
729-
// half-extracted, etc.). Do NOT try copy fallback: the global
730-
// ~/.xlings/ state may itself be residue from the same failure,
731-
// and looks_complete_legacy() can't tell residue from complete.
728+
// xlings interface install actually failed (network, missing
729+
// package, extraction subprocess issue, etc.). Retry once through
730+
// direct `xlings install`: xlings' own CLI path is more reliable
731+
// for large toolchain packages and keeps the real failure output
732+
// visible to CI/users when it still fails.
732733
mcpp::fallback::clean_incomplete_install(verdir);
734+
mcpp::log::verbose("fetcher",
735+
std::format("interface install failed for {}; retrying direct xlings install",
736+
targets[0]));
737+
mcpp::xlings::Env directEnv{ cfg_.xlingsBinary, cfg_.xlingsHome() };
738+
int directRc = mcpp::xlings::install_direct(directEnv, targets[0]);
739+
if (directRc == 0 && std::filesystem::exists(verdir)) {
740+
mcpp::fallback::mark_install_complete(verdir);
741+
return make_payload();
742+
}
743+
if (directRc == 0 && !std::filesystem::exists(verdir)) {
744+
bool copyOk = mcpp::fallback::copy_xpkg_from_global(verdir);
745+
if (copyOk && mcpp::fallback::looks_complete_legacy(verdir)) {
746+
mcpp::fallback::mark_install_complete(verdir);
747+
mcpp::log::verbose("fetcher", "resolved via copy fallback after direct install");
748+
return make_payload();
749+
}
750+
mcpp::fallback::clean_incomplete_install(verdir);
751+
}
752+
753+
// If both interface and direct install failed, do NOT try copy
754+
// fallback: the global ~/.xlings/ state may itself be residue from
755+
// the same failure, and looks_complete_legacy() can't tell residue
756+
// from complete.
733757
std::string installError = std::format(
734-
"xlings install of '{}:{}@{}' failed (exit {})",
758+
"xlings install of '{}:{}@{}' failed (interface exit {}, direct exit {})",
735759
parsed.indexName, parsed.packageName, parsed.version,
736-
inst->exitCode);
760+
inst->exitCode, directRc);
737761
if (inst->error)
738762
installError += ": " + inst->error->message;
739763
return std::unexpected(CallError{std::format(

src/xlings.cppm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ using BootstrapProgressCallback = std::function<void(const BootstrapProgress&)>;
194194
int install_with_progress(const Env& env, std::string_view target,
195195
const BootstrapProgressCallback& cb);
196196

197+
// Run direct `xlings install <target> -y`.
198+
// Used as a fallback when the NDJSON interface install path fails.
199+
int install_direct(const Env& env, std::string_view target, bool quiet = false);
200+
197201
// ─── Sandbox lifecycle ──────────────────────────────────────────────
198202

199203
// Write .xlings.json seed file.
@@ -835,6 +839,21 @@ int install_with_progress(const Env& env, std::string_view target,
835839
return (resultExitCode != -1) ? resultExitCode : closeRc;
836840
}
837841

842+
int install_direct(const Env& env, std::string_view target, bool quiet) {
843+
auto cmd = build_command_prefix(env)
844+
+ std::format(" install {} -y", shq(target));
845+
if (quiet) {
846+
cmd += " ";
847+
cmd += std::string(mcpp::platform::shell::silent_redirect);
848+
}
849+
if constexpr (mcpp::platform::is_windows) {
850+
cmd += " <NUL";
851+
} else {
852+
cmd += " </dev/null";
853+
}
854+
return mcpp::platform::process::extract_exit_code(std::system(cmd.c_str()));
855+
}
856+
838857
// ─── Sandbox lifecycle ──────────────────────────────────────────────
839858

840859
void seed_xlings_json(const Env& env,

0 commit comments

Comments
 (0)