Skip to content

Tools declared when = "run" are installed but the program cannot call them: mcpp test on Windows and mcpp run on every host start it without them on PATH #926

Open

Description

@Sunrisepeak

Observation

A workspace declares two tools for the verbs that run something:

# mcpp.toml (workspace root)
[workspace.xlings.workspace]
"xim:esbuild"    = { version = "0.25.12", when = "run" }
"xim:typescript" = { version = "7.0.2",   when = "run" }

[workspace]
members = ["app"]

app has one program (src/main.cpp) and one test (tests/tools_test.cpp), the same file: it prints its PATH, then calls esbuild --version and tsc --version through std::system.

Both tools are provisioned on every host (the payload and its shims exist afterwards: ~/.mcpp/registry/data/xpkgs/xim-x-esbuild/0.25.12/bin/esbuild.exe, ~/.mcpp/registry/subos/default/bin/{esbuild,tsc}). Whether the program can call them depends on the verb and the host:

host mcpp mcpp test -p app mcpp run -p app
ubuntu-24.04 2026.11.8.1, 2026.11.12.1 found — PATH starts with ~/.mcpp/registry/subos/default/bin not found — PATH is the caller's (sh: 1: esbuild: not found)
windows-2022 2026.11.8.1, 2026.11.12.1 not found — PATH starts with the llvm lib and the VC redist directories, then the caller's ('esbuild' is not recognized as an internal or external command) not found — the same PATH

All four rows, with the PATH each program printed: https://github.com/Sunrisepeak/mui/actions/runs/43327987828 (the workflow that makes the project is in that commit: .github/workflows/probe-windows-tools.yml).

(On the Ubuntu rows tsc answers under mcpp run only because the runner image has a TypeScript of its own on PATH; esbuild, which it has not, shows the case.)

Cause

src/build/execute.cppm, where the test runner composes a test's environment:

// Prepend the toolchain's environment's bin to the CHILD PATH so test
// binaries that shell out to bootstrapped tools (patchelf, ninja) find
// them — applied to the child only, not via a leaky shell prefix.
if constexpr (!mcpplibs::os::is_windows) {
    if (auto environment = mcpp::provider::environment_of(ctx->tc.binaryPath)) {
        auto sandboxBin  = *environment / "bin";
        ...
        if (!pathVal.empty()) childEnv.emplace_back("PATH", pathVal);
  • On POSIX a test gets the environment's bin, and a when = "run" tool is reachable as a side effect of a prefix written for the toolchain's own tools.
  • On Windows the block is compiled out, so nothing puts the declared tools on a test's PATH.
  • mcpp run (build_run_target) sets ov.will_run = true — "the when = "run" tool tier is part of what has to exist" — and provisions the tier, but composes only the runtime-library path (compute_run_env): the program it starts cannot find what was installed for it, on any host.

So the tier decides which verbs install a tool (chapter 23 §5), and nothing states — or does, uniformly — how the program those verbs start reaches it. For build.mcpp the chapter does say it: "the declared environment's bin goes to the front of the PATH that build.mcpp runs with".

The Windows exclusion looks deliberate after #726/#727: on Windows that directory also holds the shims xim:llvm registers (cl, link, lib, rc), and a test that starts a compiler or vcpkg would meet them instead of MSVC's. Prefixing the whole directory is therefore not the fix there.

What a consumer sees

openmui's tests call esbuild and tsc (TypeScript sources are stripped by both and compared). Declared when = "run" in the workspace, they are found on the Linux and macOS rows and not on the Windows row — with no message from mcpp, since provisioning succeeded. We first read it as a download that never completed, because of the second point below.

Proposal

  1. State the contract and hold it on every host and both verbs: a program started by mcpp test or mcpp run can call the tools its graph declares for the run tier (and the unconditional ones), by name.
  2. Give it the declared tools, not the whole environment bin: e.g. a directory holding only the shims of this graph's [xlings.workspace] entries (per project or per plan, next to the run closure), prepended to the child's PATH on POSIX and Windows alike. That keeps Windows: installing a payload during mcpp build leaves the registry's shim directory at the front of PATH for every action, so llvm's cl/link/lib shims shadow MSVC #726 fixed — llvm's cl/link/lib shims are not declared tools and stay off the path — and makes the POSIX test path say what it means rather than expose everything installed in the environment.
  3. Alternatively or additionally, the run contract's environment (run_contract_env) could carry each declared tool's resolved program path (the way [xlings.workspace] program = already resolves a name), for programs that prefer not to search PATH.

A smaller thing seen on the way

When two entries are fetched at once, one Downloading … done line is printed for the pair:

      Source xim:esbuild@0.25.12 registry.npmjs.org (attempt 1)
      Source xim:typescript@7.0.2 registry.npmjs.org (attempt 1)
 Downloading xim:esbuild@0.25.12 done, 4.3 MB in 0.2s
     Running bin/tools_test

(on the Ubuntu rows the one line is typescript's). Both are installed; the log reads as if one were not.

Versions

mcpp 2026.11.8.1 and 2026.11.12.1; llvm 23.1.3; windows-2022 and ubuntu-24.04 GitHub runners.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions