The asymmetry (source-confirmed)
src/xlings/xlings.cppm, build_command_prefix, on the same env.projectDir.empty() condition:
if constexpr (mcpp::platform::is_windows) {
mcpp::platform::env::set("XLINGS_HOME", env.home.string());
mcpp::platform::env::set("XLINGS_PROJECT_DIR",
env.projectDir.empty() ? "" : env.projectDir.string()); // ← set to ""
mcpp::platform::windows::prepend_path(xvmBin);
return env.binary.string();
} else {
if (env.projectDir.empty()) {
// Global mode: unset XLINGS_PROJECT_DIR (existing behavior).
return std::format(
"cd {} && env -u XLINGS_PROJECT_DIR PATH={}:\"$PATH\" XLINGS_HOME={} {}", …);
}
…
}
The POSIX comment names the intent — "Global mode: unset XLINGS_PROJECT_DIR" — and the Windows branch does not implement it. Absent and present-but-empty are not the same answer to "which scope is this", and xlings resolves its subos scope from that variable.
The same shape appears at the self init call site (env::set("XLINGS_PROJECT_DIR", "") on Windows, env -u XLINGS_PROJECT_DIR on POSIX).
A second, smaller difference in the same block: the Windows branch mutates the process environment with env::set, so the value persists past the invocation, while the POSIX branch scopes it to the one command via env.
Where it surfaced (circumstantial — I cannot prove causation without a Windows box)
xim:vulkan-loader's Windows config() declared one subos.env{ var = "PATH", op = "prepend", … }. Same descriptor, two environments, opposite results:
| environment |
result |
full xlings (openxlings/xim-pkgindex windows-test) |
✓ local:vulkan-loader@1.4.313 done, [PASS] new shim, subos default present |
| mcpp project sandbox (mcpplibs/mcpp-index#391, windows leg) |
E_INTERNAL: [vulkan-loader] failed: config hook failed |
In #391 every other leg is green (12 pass / 1 fail) and the loader is downloaded and installed before the hook runs, so the dependency edge and the payload are both fine.
What I could establish from source: the Lua side cannot throw — subos.env and xvm.add only table.insert into _XVM_OPS and return. The failure is therefore in xlings' C++ consumer (apply_subos_env_ops_, src/core/xim/installer.cpp), whose early exits all involve the subos manifest it locates via Config::xvm_artifact_subos_dir() → the scope resolved from XLINGS_PROJECT_DIR. prepend itself is implemented (src/core/subos.cpp discusses its semantics), so the mode is not the problem.
That is where the trail ends without a Windows machine. The asymmetry above is certain; its connection to this failure is a hypothesis.
Diagnosability, which cost most of the time here
xlings logs the precise rejection — log::error("[xim] …") with the variable, the binding and the reason. None of it reaches mcpp's output: the CI log contains no [xim] line at all, only
xlings reported: E_INTERNAL: [vulkan-loader] failed: config hook failed; [vulkan-loader] failed: config hook failed
Surfacing xlings' error-level lines (or an opt-in flag) would have turned this into a one-look diagnosis instead of a source read across three repos.
Status
Not blocking anyone: openxlings/xim-pkgindex#819 routes around it by declaring exports.runtime.libdirs instead, which needs no subos — that is the mechanism the package should have used anyway (mcpp copies a dependency's *.dll beside the executable, src/build/plan.cppm). Filing so the asymmetry is recorded rather than rediscovered.
The asymmetry (source-confirmed)
src/xlings/xlings.cppm,build_command_prefix, on the sameenv.projectDir.empty()condition:The POSIX comment names the intent — "Global mode: unset XLINGS_PROJECT_DIR" — and the Windows branch does not implement it. Absent and present-but-empty are not the same answer to "which scope is this", and xlings resolves its subos scope from that variable.
The same shape appears at the
self initcall site (env::set("XLINGS_PROJECT_DIR", "")on Windows,env -u XLINGS_PROJECT_DIRon POSIX).A second, smaller difference in the same block: the Windows branch mutates the process environment with
env::set, so the value persists past the invocation, while the POSIX branch scopes it to the one command viaenv.Where it surfaced (circumstantial — I cannot prove causation without a Windows box)
xim:vulkan-loader's Windowsconfig()declared onesubos.env{ var = "PATH", op = "prepend", … }. Same descriptor, two environments, opposite results:windows-test)✓ local:vulkan-loader@1.4.313 done,[PASS] new shim, subosdefaultpresentE_INTERNAL: [vulkan-loader] failed: config hook failedIn #391 every other leg is green (12 pass / 1 fail) and the loader is downloaded and installed before the hook runs, so the dependency edge and the payload are both fine.
What I could establish from source: the Lua side cannot throw —
subos.envandxvm.addonlytable.insertinto_XVM_OPSand return. The failure is therefore in xlings' C++ consumer (apply_subos_env_ops_,src/core/xim/installer.cpp), whose early exits all involve the subos manifest it locates viaConfig::xvm_artifact_subos_dir()→ the scope resolved fromXLINGS_PROJECT_DIR.prependitself is implemented (src/core/subos.cppdiscusses its semantics), so the mode is not the problem.That is where the trail ends without a Windows machine. The asymmetry above is certain; its connection to this failure is a hypothesis.
Diagnosability, which cost most of the time here
xlings logs the precise rejection —
log::error("[xim] …")with the variable, the binding and the reason. None of it reaches mcpp's output: the CI log contains no[xim]line at all, onlySurfacing xlings' error-level lines (or an opt-in flag) would have turned this into a one-look diagnosis instead of a source read across three repos.
Status
Not blocking anyone: openxlings/xim-pkgindex#819 routes around it by declaring
exports.runtime.libdirsinstead, which needs no subos — that is the mechanism the package should have used anyway (mcpp copies a dependency's*.dllbeside the executable,src/build/plan.cppm). Filing so the asymmetry is recorded rather than rediscovered.