Skip to content

Commit 4a2c1a8

Browse files
committed
fix(toolchain): resolve .exe tool names on Windows; collapse the host/target gate
Two changes to one code region, which is why they share a commit — splitting them would leave a middle state that does not compile. B1 — the frontend was unfindable on a Windows host. musl payload candidates were { "<triple>-g++", "g++" }, resolved with filesystem::exists. On Windows the file is `<triple>-g++.exe`, so exists() said no and a payload that installed perfectly was then unusable. The mingw branch has carried the .exe spelling since it shipped; this branch never did, because nothing had ever installed a musl payload on a Windows host. Same omission in archive_tool's musl branch (`<triple>-ar`), fixed alongside. .exe is listed first: on a case-insensitive filesystem both spellings match and the executable is the one we want. host_can_serve — one derivation instead of two. "Can this host serve that target" was computed independently in two places: registry.cppm picking the xim payload, and lifecycle.cppm deciding whether `toolchain list` may show a target as available. They had already drifted — the payload side would resolve a windows-hosted musl package that the availability side declared impossible. The predicate now lives once, in registry.cppm next to the payload resolution it has to agree with, and lifecycle calls it. It also gains the case this series exists for: a non-Linux host may serve a Linux target when the payload is self-contained (musl) and built for that host's own arch. Deliberately narrow — the canadian-cross payload is built per host arch, and macOS has no Linux-targeting payload at all, so both stay unserviceable rather than failing at install time. available_toolchain_indexes now lists the windows-hosted cross under the same name to_xim_package() derives (`<triple>-gcc`), so the Available listing and the install path cannot disagree. Refs .agents/docs/2026-08-03-windows-host-linux-cross-design.md §1.2, §1.3 (B1)
1 parent 568bbc8 commit 4a2c1a8

2 files changed

Lines changed: 73 additions & 12 deletions

File tree

src/toolchain/lifecycle.cppm

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -368,15 +368,12 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) {
368368

369369
// Vocabulary rows not covered by an installed payload. Only list a
370370
// verified target as "available" when this host can actually install it.
371+
//
372+
// The predicate lives in registry.cppm next to the payload resolution it
373+
// must agree with — this used to be a second, independent derivation of
374+
// the same question and the two had already drifted apart.
371375
auto installable_here = [&](const mcpp::toolchain::triple::Triple& t) {
372-
if (t.os == "linux")
373-
return mcpp::platform::is_linux
374-
&& (t.is_musl() || t.arch == hostT.arch);
375-
if (t.is_windows_gnu())
376-
return mcpp::platform::is_linux || mcpp::platform::is_windows;
377-
if (t.os == "windows") return bool(mcpp::platform::is_windows);
378-
if (t.os == "macos") return bool(mcpp::platform::is_macos);
379-
return false;
376+
return mcpp::toolchain::host_can_serve(t);
380377
};
381378
for (auto& info : mcpp::toolchain::triple::known_targets()) {
382379
bool covered = std::any_of(targetRows.begin(), targetRows.end(),

src/toolchain/registry.cppm

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ bool spec_matches_payload(const ToolchainSpec& def,
113113
// (`[toolchain] … = "system"`) is a separate, older mechanism.
114114
bool is_system_toolchain(const ToolchainSpec& spec);
115115

116+
// Can THIS host serve that target — is there an installable payload for the
117+
// (host, target) pair? Empty target = host target, always serviceable.
118+
//
119+
// THE single derivation of that question. It used to be worked out twice and
120+
// independently: here, when picking the xim payload, and again in
121+
// lifecycle.cppm when deciding whether `toolchain list` may show a target as
122+
// `available`. Two derivations of one decision is how adding a target turns
123+
// into a build failure instead of a missing row — the pair had already drifted
124+
// once (the payload side would happily resolve a windows-hosted musl package
125+
// that the availability side declared impossible).
126+
bool host_can_serve(const triple::Triple& target);
127+
116128
// xim index names to query for the Available section, with the family each
117129
// one contributes versions to. Host-conditional: a host only lists payloads
118130
// it can install.
@@ -225,7 +237,19 @@ XimToolchainPackage to_xim_package(const ToolchainSpec& spec) {
225237
bool native = mcpp::platform::is_linux
226238
&& t.arch == mcpp::platform::host_arch;
227239
pkg.ximName = native ? "musl-gcc" : t.str() + "-gcc";
228-
pkg.frontendCandidates = { t.str() + "-g++", "g++" };
240+
// Frontend candidates are resolved with filesystem::exists, so on a
241+
// Windows host the bare name never matches — the file on disk is
242+
// `<triple>-g++.exe`. The mingw branch below has carried the `.exe`
243+
// spelling since it shipped; this one had not, which made a
244+
// windows-hosted musl payload install fine and then be unusable.
245+
// `.exe` first: on a case-insensitive filesystem both would match, and
246+
// the executable is the one we want.
247+
if constexpr (mcpp::platform::is_windows) {
248+
pkg.frontendCandidates = { t.str() + "-g++.exe", t.str() + "-g++",
249+
"g++.exe", "g++" };
250+
} else {
251+
pkg.frontendCandidates = { t.str() + "-g++", "g++" };
252+
}
229253
return pkg;
230254
}
231255

@@ -299,6 +323,32 @@ bool is_system_toolchain(const ToolchainSpec& spec) {
299323
return spec.family == Family::Msvc;
300324
}
301325

326+
bool host_can_serve(const triple::Triple& target) {
327+
if (target.empty()) return true; // host target
328+
329+
if (target.os == "linux") {
330+
if constexpr (mcpp::platform::is_linux) {
331+
// musl payloads are self-contained, so any arch is reachable; a
332+
// glibc target additionally needs the host-native sysroot payloads
333+
// (xim:glibc / xim:linux-headers), which only exist for this arch.
334+
return target.is_musl() || target.arch == mcpp::platform::host_arch;
335+
}
336+
// Non-Linux host: only the self-contained musl payloads can work at
337+
// all (nothing else would find a C library). Today exactly one such
338+
// payload exists — the windows-hosted canadian cross, built per host
339+
// arch — so an arch-crossing combination stays unserviceable until one
340+
// is published. macOS has no Linux-targeting payload at all.
341+
return mcpp::platform::is_windows
342+
&& target.is_musl()
343+
&& target.arch == mcpp::platform::host_arch;
344+
}
345+
if (target.is_windows_gnu())
346+
return mcpp::platform::is_linux || mcpp::platform::is_windows;
347+
if (target.os == "windows") return bool(mcpp::platform::is_windows);
348+
if (target.os == "macos") return bool(mcpp::platform::is_macos);
349+
return false;
350+
}
351+
302352
std::vector<AvailableIndex> available_toolchain_indexes() {
303353
std::vector<AvailableIndex> out{
304354
{ "gcc", Family::Gcc },
@@ -307,10 +357,16 @@ std::vector<AvailableIndex> available_toolchain_indexes() {
307357
};
308358
// The Windows-PE gcc payload is host-split at the distribution layer
309359
// (§4.3); each host lists the package it would actually install.
310-
if constexpr (mcpp::platform::is_windows)
360+
if constexpr (mcpp::platform::is_windows) {
311361
out.push_back({ "mingw-gcc", Family::Gcc });
312-
else if constexpr (mcpp::platform::is_linux)
362+
// The windows-hosted canadian cross to Linux. Named by triple, exactly
363+
// as to_xim_package() derives it (`<triple>-gcc`), so the Available
364+
// listing and the install path cannot disagree about the package name.
365+
out.push_back({ std::string(mcpp::platform::host_arch) + "-linux-musl-gcc",
366+
Family::Gcc });
367+
} else if constexpr (mcpp::platform::is_linux) {
313368
out.push_back({ "mingw-cross-gcc", Family::Gcc });
369+
}
314370
return out;
315371
}
316372

@@ -354,7 +410,15 @@ std::filesystem::path archive_tool(const Toolchain& tc) {
354410
std::string arName = !tc.targetTriple.empty()
355411
? tc.targetTriple + "-ar"
356412
: "x86_64-linux-musl-ar";
357-
auto muslAr = tc.binaryPath.parent_path() / arName;
413+
auto dir = tc.binaryPath.parent_path();
414+
// Same `.exe` reasoning as the frontend candidates above: a windows-hosted
415+
// musl cross payload ships `<triple>-ar.exe`. Try it first, then the bare
416+
// name (which is what every ELF host has).
417+
if constexpr (mcpp::platform::is_windows) {
418+
auto muslArExe = dir / (arName + ".exe");
419+
if (std::filesystem::exists(muslArExe)) return muslArExe;
420+
}
421+
auto muslAr = dir / arName;
358422
if (std::filesystem::exists(muslAr)) return muslAr;
359423
return {};
360424
}

0 commit comments

Comments
 (0)